From 83f2fb5ff0516c8d130f6c94564431b119ba6e5c Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 03:25:09 -0300 Subject: [PATCH 01/78] chore(project): update dependencies --- package.json | 50 +- yarn.lock | 2857 +++++++++++++++++++++++++++----------------------- 2 files changed, 1576 insertions(+), 1331 deletions(-) diff --git a/package.json b/package.json index e5aff771..de1d3fdd 100644 --- a/package.json +++ b/package.json @@ -7,34 +7,32 @@ "author": "Leonardo Apiwan (@homer0) ", "license": "MIT", "dependencies": { - "wootils": "^2.0.0", - "jimple": "1.5.0", - "express": "4.16.4", - "body-parser": "1.18.3", - "compression": "1.7.3", - "extend": "3.0.2", - "node-fetch": "2.3.0", - "urijs": "1.19.1", - "statuses": "1.5.0", - "fs-extra": "7.0.1", - "multer": "1.4.1", - "mime": "2.4.0" + "wootils": "^2.3.0", + "jimple": "^1.5.0", + "express": "^4.17.1", + "body-parser": "^1.19.0", + "compression": "^1.7.4", + "extend": "^3.0.2", + "node-fetch": "^2.6.0", + "urijs": "^1.19.1", + "statuses": "^1.5.0", + "fs-extra": "^8.0.1", + "multer": "^1.4.1", + "mime": "^2.4.4" }, "devDependencies": { - "eslint": "5.13.0", - "eslint-config-airbnb-base": "13.1.0", - "eslint-plugin-import": "2.16.0", - "eslint-plugin-node": "8.0.1", - "jest-ex": "6.0.0", - "jest-cli": "24.0.0", - "jasmine-expect": "4.0.1", - "@babel/preset-env": "7.0.0", - "@babel/core": "7.2.2", - "esdoc": "1.1.0", - "esdoc-standard-plugin": "1.0.0", - "esdoc-node": "1.0.4", - "leasot": "7.2.0", - "coveralls": "3.0.2" + "eslint": "^5.16.0", + "eslint-config-airbnb-base": "^13.1.0", + "eslint-plugin-import": "^2.17.3", + "eslint-plugin-node": "^9.1.0", + "jest-ex": "^6.1.0", + "jest-cli": "^24.8.0", + "jasmine-expect": "^4.0.2", + "esdoc": "^1.1.0", + "esdoc-standard-plugin": "^1.0.0", + "esdoc-node": "^1.0.4", + "leasot": "^7.4.0", + "coveralls": "^3.0.4" }, "engine-strict": true, "engines": { diff --git a/yarn.lock b/yarn.lock index 0ab7574a..a07273d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,34 +9,54 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@7.2.2", "@babel/core@^7.1.0": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.2.2.tgz#07adba6dde27bb5ad8d8672f15fde3e08184a687" - integrity sha512-59vB0RWt09cAct5EIe58+NzGP4TFSD3Bz//2/ELy3ZeTeKF6VTD1AXlH8BGGbCX0PuobZBsIzO7IAI9PH67eKw== +"@babel/core@7.4.3": + version "7.4.3" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.3.tgz#198d6d3af4567be3989550d97e068de94503074f" + integrity sha512-oDpASqKFlbspQfzAE7yaeTmdljSH2ADIvBlb0RwbStltTuWa0+7CCI1fYVINNv9saHPa1W7oaKeuNuKj+RQCvA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.4.0" + "@babel/helpers" "^7.4.3" + "@babel/parser" "^7.4.3" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.3" + "@babel/types" "^7.4.0" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.11" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/core@^7.1.0": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a" + integrity sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.2.2" - "@babel/helpers" "^7.2.0" - "@babel/parser" "^7.2.2" - "@babel/template" "^7.2.2" - "@babel/traverse" "^7.2.2" - "@babel/types" "^7.2.2" + "@babel/generator" "^7.4.4" + "@babel/helpers" "^7.4.4" + "@babel/parser" "^7.4.5" + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.5" + "@babel/types" "^7.4.4" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" - lodash "^4.17.10" + lodash "^4.17.11" resolve "^1.3.2" semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.2.2": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.2.tgz#fff31a7b2f2f3dad23ef8e01be45b0d5c2fc0132" - integrity sha512-f3QCuPppXxtZOEm5GWPra/uYUjmNQlu9pbAD8D/9jze4pTY83rTtB1igTBSwvkeNlC5gR24zFFkz+2WHLFQhqQ== +"@babel/generator@^7.4.0", "@babel/generator@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041" + integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ== dependencies: - "@babel/types" "^7.3.2" + "@babel/types" "^7.4.4" jsesc "^2.5.1" - lodash "^4.17.10" + lodash "^4.17.11" source-map "^0.5.0" trim-right "^1.0.1" @@ -55,23 +75,23 @@ "@babel/helper-explode-assignable-expression" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-call-delegate@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz#6a957f105f37755e8645343d3038a22e1449cc4a" - integrity sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ== +"@babel/helper-call-delegate@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz#87c1f8ca19ad552a736a7a27b1c1fcf8b1ff1f43" + integrity sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ== dependencies: - "@babel/helper-hoist-variables" "^7.0.0" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/helper-hoist-variables" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" -"@babel/helper-define-map@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz#3b74caec329b3c80c116290887c0dd9ae468c20c" - integrity sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg== +"@babel/helper-define-map@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz#6969d1f570b46bdc900d1eba8e5d59c48ba2c12a" + integrity sha512-IX3Ln8gLhZpSuqHJSnTNBWGDE9kdkTEWl21A/K7PQ00tseBwbqCHTvNLHSBd9M0R5rER4h5Rsvj9vw0R5SieBg== dependencies: "@babel/helper-function-name" "^7.1.0" - "@babel/types" "^7.0.0" - lodash "^4.17.10" + "@babel/types" "^7.4.4" + lodash "^4.17.11" "@babel/helper-explode-assignable-expression@^7.1.0": version "7.1.0" @@ -97,12 +117,12 @@ dependencies: "@babel/types" "^7.0.0" -"@babel/helper-hoist-variables@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz#46adc4c5e758645ae7a45deb92bab0918c23bb88" - integrity sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w== +"@babel/helper-hoist-variables@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz#0298b5f25c8c09c53102d52ac4a98f773eb2850a" + integrity sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w== dependencies: - "@babel/types" "^7.0.0" + "@babel/types" "^7.4.4" "@babel/helper-member-expression-to-functions@^7.0.0": version "7.0.0" @@ -118,17 +138,17 @@ dependencies: "@babel/types" "^7.0.0" -"@babel/helper-module-transforms@^7.1.0": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.2.2.tgz#ab2f8e8d231409f8370c883d20c335190284b963" - integrity sha512-YRD7I6Wsv+IHuTPkAmAS4HhY0dkPobgLftHp0cRGZSdrRvmZY8rFvae/GVu3bD00qscuvK3WPHB3YdNpBXUqrA== +"@babel/helper-module-transforms@^7.1.0", "@babel/helper-module-transforms@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz#96115ea42a2f139e619e98ed46df6019b94414b8" + integrity sha512-3Z1yp8TVQf+B4ynN7WoHPKS8EkdTbgAEy0nU0rs/1Kw4pDgmvYH3rz3aI11KgxKCba2cn7N+tqzV1mY2HMN96w== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-simple-access" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/template" "^7.2.2" - "@babel/types" "^7.2.2" - lodash "^4.17.10" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/template" "^7.4.4" + "@babel/types" "^7.4.4" + lodash "^4.17.11" "@babel/helper-optimise-call-expression@^7.0.0": version "7.0.0" @@ -142,12 +162,12 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== -"@babel/helper-regex@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.0.0.tgz#2c1718923b57f9bbe64705ffe5640ac64d9bdb27" - integrity sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg== +"@babel/helper-regex@^7.0.0", "@babel/helper-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.4.4.tgz#a47e02bc91fb259d2e6727c2a30013e3ac13c4a2" + integrity sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q== dependencies: - lodash "^4.17.10" + lodash "^4.17.11" "@babel/helper-remap-async-to-generator@^7.1.0": version "7.1.0" @@ -160,15 +180,15 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-replace-supers@^7.1.0": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.2.3.tgz#19970020cf22677d62b3a689561dbd9644d8c5e5" - integrity sha512-GyieIznGUfPXPWu0yLS6U55Mz67AZD9cUk0BfirOWlPrXlBcan9Gz+vHGz+cPfuoweZSnPzPIm67VtQM0OWZbA== +"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz#aee41783ebe4f2d3ab3ae775e1cc6f1a90cefa27" + integrity sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg== dependencies: "@babel/helper-member-expression-to-functions" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/traverse" "^7.2.3" - "@babel/types" "^7.0.0" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" "@babel/helper-simple-access@^7.1.0": version "7.1.0" @@ -178,12 +198,12 @@ "@babel/template" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-split-export-declaration@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz#3aae285c0311c2ab095d997b8c9a94cad547d813" - integrity sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag== +"@babel/helper-split-export-declaration@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" + integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== dependencies: - "@babel/types" "^7.0.0" + "@babel/types" "^7.4.4" "@babel/helper-wrap-function@^7.1.0": version "7.2.0" @@ -195,14 +215,14 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.2.0" -"@babel/helpers@^7.2.0": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.3.1.tgz#949eec9ea4b45d3210feb7dc1c22db664c9e44b9" - integrity sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA== +"@babel/helpers@^7.4.3", "@babel/helpers@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5" + integrity sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A== dependencies: - "@babel/template" "^7.1.2" - "@babel/traverse" "^7.1.5" - "@babel/types" "^7.3.0" + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" "@babel/highlight@^7.0.0": version "7.0.0" @@ -213,12 +233,12 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.2.3": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.2.tgz#95cdeddfc3992a6ca2a1315191c1679ca32c55cd" - integrity sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ== +"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872" + integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew== -"@babel/plugin-proposal-async-generator-functions@^7.0.0", "@babel/plugin-proposal-async-generator-functions@^7.2.0": +"@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" integrity sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ== @@ -227,7 +247,7 @@ "@babel/helper-remap-async-to-generator" "^7.1.0" "@babel/plugin-syntax-async-generators" "^7.2.0" -"@babel/plugin-proposal-json-strings@^7.0.0", "@babel/plugin-proposal-json-strings@^7.2.0": +"@babel/plugin-proposal-json-strings@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317" integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg== @@ -235,15 +255,15 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-json-strings" "^7.2.0" -"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.3.1": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz#6d1859882d4d778578e41f82cc5d7bf3d5daf6c1" - integrity sha512-DjeMS+J2+lpANkYLLO+m6GjoTMygYglKmRe6cDTbFv3L9i6mmiE8fe6B8MtCSLZpVXscD5kn7s6SgtHrDoBWoA== +"@babel/plugin-proposal-object-rest-spread@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.4.4.tgz#1ef173fcf24b3e2df92a678f027673b55e7e3005" + integrity sha512-dMBG6cSPBbHeEBdFXeQ2QLc5gUpg4Vkaz8octD4aoW/ISO+jBOcsuxYL7bsb5WSu8RLP6boxrBIALEHgoHtO9g== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" -"@babel/plugin-proposal-optional-catch-binding@^7.0.0", "@babel/plugin-proposal-optional-catch-binding@^7.2.0": +"@babel/plugin-proposal-optional-catch-binding@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5" integrity sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g== @@ -251,16 +271,16 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" -"@babel/plugin-proposal-unicode-property-regex@^7.0.0", "@babel/plugin-proposal-unicode-property-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz#abe7281fe46c95ddc143a65e5358647792039520" - integrity sha512-LvRVYb7kikuOtIoUeWTkOxQEV1kYvL5B6U3iWEGCzPNRus1MzJweFqORTj+0jkxozkTSYNJozPOddxmqdqsRpw== +"@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz#501ffd9826c0b91da22690720722ac7cb1ca9c78" + integrity sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.2.0" + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.5.4" -"@babel/plugin-syntax-async-generators@^7.0.0", "@babel/plugin-syntax-async-generators@^7.2.0": +"@babel/plugin-syntax-async-generators@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f" integrity sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg== @@ -281,89 +301,89 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-optional-catch-binding@^7.0.0", "@babel/plugin-syntax-optional-catch-binding@^7.2.0": +"@babel/plugin-syntax-optional-catch-binding@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz#a94013d6eda8908dfe6a477e7f9eda85656ecf5c" integrity sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-arrow-functions@^7.0.0", "@babel/plugin-transform-arrow-functions@^7.2.0": +"@babel/plugin-transform-arrow-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" integrity sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-async-to-generator@^7.0.0", "@babel/plugin-transform-async-to-generator@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.2.0.tgz#68b8a438663e88519e65b776f8938f3445b1a2ff" - integrity sha512-CEHzg4g5UraReozI9D4fblBYABs7IM6UerAVG7EJVrTLC5keh00aEuLUT+O40+mJCEzaXkYfTCUKIyeDfMOFFQ== +"@babel/plugin-transform-async-to-generator@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.4.4.tgz#a3f1d01f2f21cadab20b33a82133116f14fb5894" + integrity sha512-YiqW2Li8TXmzgbXw+STsSqPBPFnGviiaSp6CYOq55X8GQ2SGVLrXB6pNid8HkqkZAzOH6knbai3snhP7v0fNwA== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-remap-async-to-generator" "^7.1.0" -"@babel/plugin-transform-block-scoped-functions@^7.0.0", "@babel/plugin-transform-block-scoped-functions@^7.2.0": +"@babel/plugin-transform-block-scoped-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz#5d3cc11e8d5ddd752aa64c9148d0db6cb79fd190" integrity sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.2.0.tgz#f17c49d91eedbcdf5dd50597d16f5f2f770132d4" - integrity sha512-vDTgf19ZEV6mx35yiPJe4fS02mPQUUcBNwWQSZFXSzTSbsJFQvHt7DqyS3LK8oOWALFOsJ+8bbqBgkirZteD5Q== +"@babel/plugin-transform-block-scoping@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.4.4.tgz#c13279fabf6b916661531841a23c4b7dae29646d" + integrity sha512-jkTUyWZcTrwxu5DD4rWz6rDB5Cjdmgz6z7M7RLXOJyCUkFBawssDGcGh8M/0FTSB87avyJI1HsTwUXp9nKA1PA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - lodash "^4.17.10" + lodash "^4.17.11" -"@babel/plugin-transform-classes@^7.0.0", "@babel/plugin-transform-classes@^7.2.0": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.2.2.tgz#6c90542f210ee975aa2aa8c8b5af7fa73a126953" - integrity sha512-gEZvgTy1VtcDOaQty1l10T3jQmJKlNVxLDCs+3rCVPr6nMkODLELxViq5X9l+rfxbie3XrfrMCYYY6eX3aOcOQ== +"@babel/plugin-transform-classes@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.4.4.tgz#0ce4094cdafd709721076d3b9c38ad31ca715eb6" + integrity sha512-/e44eFLImEGIpL9qPxSRat13I5QNRgBLu2hOQJCF7VLy/otSM/sypV1+XaIw5+502RX/+6YaSAPmldk+nhHDPw== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" - "@babel/helper-define-map" "^7.1.0" + "@babel/helper-define-map" "^7.4.4" "@babel/helper-function-name" "^7.1.0" "@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" + "@babel/helper-replace-supers" "^7.4.4" + "@babel/helper-split-export-declaration" "^7.4.4" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.0.0", "@babel/plugin-transform-computed-properties@^7.2.0": +"@babel/plugin-transform-computed-properties@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz#83a7df6a658865b1c8f641d510c6f3af220216da" integrity sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-destructuring@^7.0.0", "@babel/plugin-transform-destructuring@^7.2.0": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.3.2.tgz#f2f5520be055ba1c38c41c0e094d8a461dd78f2d" - integrity sha512-Lrj/u53Ufqxl/sGxyjsJ2XNtNuEjDyjpqdhMNh5aZ+XFOdThL46KBj27Uem4ggoezSYBxKWAil6Hu8HtwqesYw== +"@babel/plugin-transform-destructuring@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.4.4.tgz#9d964717829cc9e4b601fc82a26a71a4d8faf20f" + integrity sha512-/aOx+nW0w8eHiEHm+BTERB2oJn5D127iye/SUQl7NjHy0lf+j7h4MKMMSOwdazGq9OxgiNADncE+SRJkCxjZpQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-dotall-regex@^7.0.0", "@babel/plugin-transform-dotall-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.2.0.tgz#f0aabb93d120a8ac61e925ea0ba440812dbe0e49" - integrity sha512-sKxnyHfizweTgKZf7XsXu/CNupKhzijptfTM+bozonIuyVrLWVUvYjE2bhuSBML8VQeMxq4Mm63Q9qvcvUcciQ== +"@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz#361a148bc951444312c69446d76ed1ea8e4450c3" + integrity sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.1.3" + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.5.4" -"@babel/plugin-transform-duplicate-keys@^7.0.0", "@babel/plugin-transform-duplicate-keys@^7.2.0": +"@babel/plugin-transform-duplicate-keys@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz#d952c4930f312a4dbfff18f0b2914e60c35530b3" integrity sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-exponentiation-operator@^7.0.0", "@babel/plugin-transform-exponentiation-operator@^7.2.0": +"@babel/plugin-transform-exponentiation-operator@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz#a63868289e5b4007f7054d46491af51435766008" integrity sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A== @@ -371,29 +391,36 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-for-of@^7.0.0", "@babel/plugin-transform-for-of@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.2.0.tgz#ab7468befa80f764bb03d3cb5eef8cc998e1cad9" - integrity sha512-Kz7Mt0SsV2tQk6jG5bBv5phVbkd0gd27SgYD4hH1aLMJRchM0dzHaXvrWhVZ+WxAlDoAKZ7Uy3jVTW2mKXQ1WQ== +"@babel/plugin-transform-for-of@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz#0267fc735e24c808ba173866c6c4d1440fc3c556" + integrity sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-function-name@^7.0.0", "@babel/plugin-transform-function-name@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.2.0.tgz#f7930362829ff99a3174c39f0afcc024ef59731a" - integrity sha512-kWgksow9lHdvBC2Z4mxTsvc7YdY7w/V6B2vy9cTIPtLEE9NhwoWivaxdNM/S37elu5bqlLP/qOY906LukO9lkQ== +"@babel/plugin-transform-function-name@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz#e1436116abb0610c2259094848754ac5230922ad" + integrity sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA== dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-literals@^7.0.0", "@babel/plugin-transform-literals@^7.2.0": +"@babel/plugin-transform-literals@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz#690353e81f9267dad4fd8cfd77eafa86aba53ea1" integrity sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-modules-amd@^7.0.0", "@babel/plugin-transform-modules-amd@^7.2.0": +"@babel/plugin-transform-member-expression-literals@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz#fa10aa5c58a2cb6afcf2c9ffa8cb4d8b3d489a2d" + integrity sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-modules-amd@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz#82a9bce45b95441f617a24011dc89d12da7f4ee6" integrity sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw== @@ -401,24 +428,24 @@ "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.2.0.tgz#c4f1933f5991d5145e9cfad1dfd848ea1727f404" - integrity sha512-V6y0uaUQrQPXUrmj+hgnks8va2L0zcZymeU7TtWEgdRLNkceafKXEduv7QzgQAE4lT+suwooG9dC7LFhdRAbVQ== +"@babel/plugin-transform-modules-commonjs@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.4.4.tgz#0bef4713d30f1d78c2e59b3d6db40e60192cac1e" + integrity sha512-4sfBOJt58sEo9a2BQXnZq+Q3ZTSAUXyK3E30o36BOGnJ+tvJ6YSxF0PG6kERvbeISgProodWuI9UVG3/FMY6iw== dependencies: - "@babel/helper-module-transforms" "^7.1.0" + "@babel/helper-module-transforms" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-simple-access" "^7.1.0" -"@babel/plugin-transform-modules-systemjs@^7.0.0", "@babel/plugin-transform-modules-systemjs@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.2.0.tgz#912bfe9e5ff982924c81d0937c92d24994bb9068" - integrity sha512-aYJwpAhoK9a+1+O625WIjvMY11wkB/ok0WClVwmeo3mCjcNRjt+/8gHWrB5i+00mUju0gWsBkQnPpdvQ7PImmQ== +"@babel/plugin-transform-modules-systemjs@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.4.4.tgz#dc83c5665b07d6c2a7b224c00ac63659ea36a405" + integrity sha512-MSiModfILQc3/oqnG7NrP1jHaSPryO6tA2kOMmAQApz5dayPxWiHqmq4sWH2xF5LcQK56LlbKByCd8Aah/OIkQ== dependencies: - "@babel/helper-hoist-variables" "^7.0.0" + "@babel/helper-hoist-variables" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-modules-umd@^7.0.0", "@babel/plugin-transform-modules-umd@^7.2.0": +"@babel/plugin-transform-modules-umd@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz#7678ce75169f0877b8eb2235538c074268dd01ae" integrity sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw== @@ -426,21 +453,21 @@ "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-named-capturing-groups-regex@^7.3.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.3.0.tgz#140b52985b2d6ef0cb092ef3b29502b990f9cd50" - integrity sha512-NxIoNVhk9ZxS+9lSoAQ/LM0V2UEvARLttEHUrRDGKFaAxOYQcrkN/nLRE+BbbicCAvZPl7wMP0X60HsHE5DtQw== +"@babel/plugin-transform-named-capturing-groups-regex@^7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz#9d269fd28a370258199b4294736813a60bbdd106" + integrity sha512-z7+2IsWafTBbjNsOxU/Iv5CvTJlr5w4+HGu1HovKYTtgJ362f7kBcQglkfmlspKKZ3bgrbSGvLfNx++ZJgCWsg== dependencies: - regexp-tree "^0.1.0" + regexp-tree "^0.1.6" -"@babel/plugin-transform-new-target@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz#ae8fbd89517fa7892d20e6564e641e8770c3aa4a" - integrity sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw== +"@babel/plugin-transform-new-target@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz#18d120438b0cc9ee95a47f2c72bc9768fbed60a5" + integrity sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-object-super@^7.0.0", "@babel/plugin-transform-object-super@^7.2.0": +"@babel/plugin-transform-object-super@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz#b35d4c10f56bab5d650047dad0f1d8e8814b6598" integrity sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg== @@ -448,47 +475,61 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-replace-supers" "^7.1.0" -"@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.2.0.tgz#0d5ad15dc805e2ea866df4dd6682bfe76d1408c2" - integrity sha512-kB9+hhUidIgUoBQ0MsxMewhzr8i60nMa2KgeJKQWYrqQpqcBYtnpR+JgkadZVZoaEZ/eKu9mclFaVwhRpLNSzA== +"@babel/plugin-transform-parameters@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz#7556cf03f318bd2719fe4c922d2d808be5571e16" + integrity sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw== dependencies: - "@babel/helper-call-delegate" "^7.1.0" + "@babel/helper-call-delegate" "^7.4.4" "@babel/helper-get-function-arity" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-regenerator@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz#5b41686b4ed40bef874d7ed6a84bdd849c13e0c1" - integrity sha512-sj2qzsEx8KDVv1QuJc/dEfilkg3RRPvPYx/VnKLtItVQRWt1Wqf5eVCOLZm29CiGFfYYsA3VPjfizTCV0S0Dlw== +"@babel/plugin-transform-property-literals@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz#03e33f653f5b25c4eb572c98b9485055b389e905" + integrity sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-regenerator@^7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz#629dc82512c55cee01341fb27bdfcb210354680f" + integrity sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA== dependencies: - regenerator-transform "^0.13.3" + regenerator-transform "^0.14.0" -"@babel/plugin-transform-runtime@7.2.0": +"@babel/plugin-transform-reserved-words@^7.2.0": version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.2.0.tgz#566bc43f7d0aedc880eaddbd29168d0f248966ea" - integrity sha512-jIgkljDdq4RYDnJyQsiWbdvGeei/0MOTtSHKO/rfbd/mXBxNpdlulMx49L0HQ4pug1fXannxoqCI+fYSle9eSw== + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz#4792af87c998a49367597d07fedf02636d2e1634" + integrity sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-runtime@7.4.3": + version "7.4.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.4.3.tgz#4d6691690ecdc9f5cb8c3ab170a1576c1f556371" + integrity sha512-7Q61bU+uEI7bCUFReT1NKn7/X6sDQsZ7wL1sJ9IYMAO7cI+eg6x9re1cEw2fCRMbbTVyoeUKWSV1M6azEfKCfg== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" resolve "^1.8.1" semver "^5.5.1" -"@babel/plugin-transform-shorthand-properties@^7.0.0", "@babel/plugin-transform-shorthand-properties@^7.2.0": +"@babel/plugin-transform-shorthand-properties@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0" integrity sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-spread@^7.0.0", "@babel/plugin-transform-spread@^7.2.0": +"@babel/plugin-transform-spread@^7.2.0": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz#3103a9abe22f742b6d406ecd3cd49b774919b406" integrity sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-sticky-regex@^7.0.0", "@babel/plugin-transform-sticky-regex@^7.2.0": +"@babel/plugin-transform-sticky-regex@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz#a1e454b5995560a9c1e0d537dfc15061fd2687e1" integrity sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw== @@ -496,159 +537,271 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.0.0" -"@babel/plugin-transform-template-literals@^7.0.0", "@babel/plugin-transform-template-literals@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.2.0.tgz#d87ed01b8eaac7a92473f608c97c089de2ba1e5b" - integrity sha512-FkPix00J9A/XWXv4VoKJBMeSkyY9x/TqIh76wzcdfl57RJJcf8CehQ08uwfhCDNtRQYtHQKBTwKZDEyjE13Lwg== +"@babel/plugin-transform-template-literals@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz#9d28fea7bbce637fb7612a0750989d8321d4bcb0" + integrity sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-typeof-symbol@^7.0.0", "@babel/plugin-transform-typeof-symbol@^7.2.0": +"@babel/plugin-transform-typeof-symbol@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz#117d2bcec2fbf64b4b59d1f9819894682d29f2b2" integrity sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-unicode-regex@^7.0.0", "@babel/plugin-transform-unicode-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz#4eb8db16f972f8abb5062c161b8b115546ade08b" - integrity sha512-m48Y0lMhrbXEJnVUaYly29jRXbQ3ksxPrS1Tg8t+MHqzXhtBYAvI51euOBaoAlZLPHsieY9XPVMf80a5x0cPcA== +"@babel/plugin-transform-unicode-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz#ab4634bb4f14d36728bf5978322b35587787970f" + integrity sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.1.3" + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.5.4" -"@babel/preset-env@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.0.0.tgz#f450f200c14e713f98cb14d113bf0c2cfbb89ca9" - integrity sha512-Fnx1wWaWv2w2rl+VHxA9si//Da40941IQ29fKiRejVR7oN1FxSEL8+SyAX/2oKIye2gPvY/GBbJVEKQ/oi43zQ== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-async-generator-functions" "^7.0.0" - "@babel/plugin-proposal-json-strings" "^7.0.0" - "@babel/plugin-proposal-object-rest-spread" "^7.0.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.0.0" - "@babel/plugin-syntax-async-generators" "^7.0.0" - "@babel/plugin-syntax-object-rest-spread" "^7.0.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.0.0" - "@babel/plugin-transform-arrow-functions" "^7.0.0" - "@babel/plugin-transform-async-to-generator" "^7.0.0" - "@babel/plugin-transform-block-scoped-functions" "^7.0.0" - "@babel/plugin-transform-block-scoping" "^7.0.0" - "@babel/plugin-transform-classes" "^7.0.0" - "@babel/plugin-transform-computed-properties" "^7.0.0" - "@babel/plugin-transform-destructuring" "^7.0.0" - "@babel/plugin-transform-dotall-regex" "^7.0.0" - "@babel/plugin-transform-duplicate-keys" "^7.0.0" - "@babel/plugin-transform-exponentiation-operator" "^7.0.0" - "@babel/plugin-transform-for-of" "^7.0.0" - "@babel/plugin-transform-function-name" "^7.0.0" - "@babel/plugin-transform-literals" "^7.0.0" - "@babel/plugin-transform-modules-amd" "^7.0.0" - "@babel/plugin-transform-modules-commonjs" "^7.0.0" - "@babel/plugin-transform-modules-systemjs" "^7.0.0" - "@babel/plugin-transform-modules-umd" "^7.0.0" - "@babel/plugin-transform-new-target" "^7.0.0" - "@babel/plugin-transform-object-super" "^7.0.0" - "@babel/plugin-transform-parameters" "^7.0.0" - "@babel/plugin-transform-regenerator" "^7.0.0" - "@babel/plugin-transform-shorthand-properties" "^7.0.0" - "@babel/plugin-transform-spread" "^7.0.0" - "@babel/plugin-transform-sticky-regex" "^7.0.0" - "@babel/plugin-transform-template-literals" "^7.0.0" - "@babel/plugin-transform-typeof-symbol" "^7.0.0" - "@babel/plugin-transform-unicode-regex" "^7.0.0" - browserslist "^4.1.0" - invariant "^2.2.2" - js-levenshtein "^1.1.3" - semver "^5.3.0" - -"@babel/preset-env@^7.3.1": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.1.tgz#389e8ca6b17ae67aaf9a2111665030be923515db" - integrity sha512-FHKrD6Dxf30e8xgHQO0zJZpUPfVZg+Xwgz5/RdSWCbza9QLNk4Qbp40ctRoqDxml3O8RMzB1DU55SXeDG6PqHQ== +"@babel/preset-env@^7.4.3": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.5.tgz#2fad7f62983d5af563b5f3139242755884998a58" + integrity sha512-f2yNVXM+FsR5V8UwcFeIHzHWgnhXg3NpRmy0ADvALpnhB0SLbCvrCRr4BLOUYbQNLS+Z0Yer46x9dJXpXewI7w== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-async-generator-functions" "^7.2.0" "@babel/plugin-proposal-json-strings" "^7.2.0" - "@babel/plugin-proposal-object-rest-spread" "^7.3.1" + "@babel/plugin-proposal-object-rest-spread" "^7.4.4" "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.2.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" "@babel/plugin-syntax-async-generators" "^7.2.0" "@babel/plugin-syntax-json-strings" "^7.2.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" "@babel/plugin-transform-arrow-functions" "^7.2.0" - "@babel/plugin-transform-async-to-generator" "^7.2.0" + "@babel/plugin-transform-async-to-generator" "^7.4.4" "@babel/plugin-transform-block-scoped-functions" "^7.2.0" - "@babel/plugin-transform-block-scoping" "^7.2.0" - "@babel/plugin-transform-classes" "^7.2.0" + "@babel/plugin-transform-block-scoping" "^7.4.4" + "@babel/plugin-transform-classes" "^7.4.4" "@babel/plugin-transform-computed-properties" "^7.2.0" - "@babel/plugin-transform-destructuring" "^7.2.0" - "@babel/plugin-transform-dotall-regex" "^7.2.0" + "@babel/plugin-transform-destructuring" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" "@babel/plugin-transform-duplicate-keys" "^7.2.0" "@babel/plugin-transform-exponentiation-operator" "^7.2.0" - "@babel/plugin-transform-for-of" "^7.2.0" - "@babel/plugin-transform-function-name" "^7.2.0" + "@babel/plugin-transform-for-of" "^7.4.4" + "@babel/plugin-transform-function-name" "^7.4.4" "@babel/plugin-transform-literals" "^7.2.0" + "@babel/plugin-transform-member-expression-literals" "^7.2.0" "@babel/plugin-transform-modules-amd" "^7.2.0" - "@babel/plugin-transform-modules-commonjs" "^7.2.0" - "@babel/plugin-transform-modules-systemjs" "^7.2.0" + "@babel/plugin-transform-modules-commonjs" "^7.4.4" + "@babel/plugin-transform-modules-systemjs" "^7.4.4" "@babel/plugin-transform-modules-umd" "^7.2.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.3.0" - "@babel/plugin-transform-new-target" "^7.0.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.4.5" + "@babel/plugin-transform-new-target" "^7.4.4" "@babel/plugin-transform-object-super" "^7.2.0" - "@babel/plugin-transform-parameters" "^7.2.0" - "@babel/plugin-transform-regenerator" "^7.0.0" + "@babel/plugin-transform-parameters" "^7.4.4" + "@babel/plugin-transform-property-literals" "^7.2.0" + "@babel/plugin-transform-regenerator" "^7.4.5" + "@babel/plugin-transform-reserved-words" "^7.2.0" "@babel/plugin-transform-shorthand-properties" "^7.2.0" "@babel/plugin-transform-spread" "^7.2.0" "@babel/plugin-transform-sticky-regex" "^7.2.0" - "@babel/plugin-transform-template-literals" "^7.2.0" + "@babel/plugin-transform-template-literals" "^7.4.4" "@babel/plugin-transform-typeof-symbol" "^7.2.0" - "@babel/plugin-transform-unicode-regex" "^7.2.0" - browserslist "^4.3.4" + "@babel/plugin-transform-unicode-regex" "^7.4.4" + "@babel/types" "^7.4.4" + browserslist "^4.6.0" + core-js-compat "^3.1.1" invariant "^2.2.2" js-levenshtein "^1.1.3" - semver "^5.3.0" + semver "^5.5.0" -"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" - integrity sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g== +"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" + integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.2.2" - "@babel/types" "^7.2.2" + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2", "@babel/traverse@^7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8" - integrity sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216" + integrity sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.2.2" + "@babel/generator" "^7.4.4" "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/parser" "^7.2.3" - "@babel/types" "^7.2.2" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.4.5" + "@babel/types" "^7.4.4" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.10" + lodash "^4.17.11" -"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.2": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.2.tgz#424f5be4be633fff33fb83ab8d67e4a8290f5a2f" - integrity sha512-3Y6H8xlUlpbGR+XvawiH0UXehqydTmNmEpozWcXymqwcrwYAl5KMvKtQ+TF6f6E08V6Jur7v/ykdDSF+WDEIXQ== +"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0" + integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ== dependencies: esutils "^2.0.2" - lodash "^4.17.10" + lodash "^4.17.11" to-fast-properties "^2.0.0" +"@cnakazawa/watch@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" + integrity sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA== + dependencies: + exec-sh "^0.3.2" + minimist "^1.2.0" + +"@jest/console@^24.7.1": + version "24.7.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545" + integrity sha512-iNhtIy2M8bXlAOULWVTUxmnelTLFneTNEkHCgPmgd+zNwy9zVddJ6oS5rZ9iwoscNdT5mMwUd0C51v/fSlzItg== + dependencies: + "@jest/source-map" "^24.3.0" + chalk "^2.0.1" + slash "^2.0.0" + +"@jest/core@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.8.0.tgz#fbbdcd42a41d0d39cddbc9f520c8bab0c33eed5b" + integrity sha512-R9rhAJwCBQzaRnrRgAdVfnglUuATXdwTRsYqs6NMdVcAl5euG8LtWDe+fVkN27YfKVBW61IojVsXKaOmSnqd/A== + dependencies: + "@jest/console" "^24.7.1" + "@jest/reporters" "^24.8.0" + "@jest/test-result" "^24.8.0" + "@jest/transform" "^24.8.0" + "@jest/types" "^24.8.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-changed-files "^24.8.0" + jest-config "^24.8.0" + jest-haste-map "^24.8.0" + jest-message-util "^24.8.0" + jest-regex-util "^24.3.0" + jest-resolve-dependencies "^24.8.0" + jest-runner "^24.8.0" + jest-runtime "^24.8.0" + jest-snapshot "^24.8.0" + jest-util "^24.8.0" + jest-validate "^24.8.0" + jest-watcher "^24.8.0" + micromatch "^3.1.10" + p-each-series "^1.0.0" + pirates "^4.0.1" + realpath-native "^1.1.0" + rimraf "^2.5.4" + strip-ansi "^5.0.0" + +"@jest/environment@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.8.0.tgz#0342261383c776bdd652168f68065ef144af0eac" + integrity sha512-vlGt2HLg7qM+vtBrSkjDxk9K0YtRBi7HfRFaDxoRtyi+DyVChzhF20duvpdAnKVBV6W5tym8jm0U9EfXbDk1tw== + dependencies: + "@jest/fake-timers" "^24.8.0" + "@jest/transform" "^24.8.0" + "@jest/types" "^24.8.0" + jest-mock "^24.8.0" + +"@jest/fake-timers@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.8.0.tgz#2e5b80a4f78f284bcb4bd5714b8e10dd36a8d3d1" + integrity sha512-2M4d5MufVXwi6VzZhJ9f5S/wU4ud2ck0kxPof1Iz3zWx6Y+V2eJrES9jEktB6O3o/oEyk+il/uNu9PvASjWXQw== + dependencies: + "@jest/types" "^24.8.0" + jest-message-util "^24.8.0" + jest-mock "^24.8.0" + +"@jest/reporters@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.8.0.tgz#075169cd029bddec54b8f2c0fc489fd0b9e05729" + integrity sha512-eZ9TyUYpyIIXfYCrw0UHUWUvE35vx5I92HGMgS93Pv7du+GHIzl+/vh8Qj9MCWFK/4TqyttVBPakWMOfZRIfxw== + dependencies: + "@jest/environment" "^24.8.0" + "@jest/test-result" "^24.8.0" + "@jest/transform" "^24.8.0" + "@jest/types" "^24.8.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.2" + istanbul-lib-coverage "^2.0.2" + istanbul-lib-instrument "^3.0.1" + istanbul-lib-report "^2.0.4" + istanbul-lib-source-maps "^3.0.1" + istanbul-reports "^2.1.1" + jest-haste-map "^24.8.0" + jest-resolve "^24.8.0" + jest-runtime "^24.8.0" + jest-util "^24.8.0" + jest-worker "^24.6.0" + node-notifier "^5.2.1" + slash "^2.0.0" + source-map "^0.6.0" + string-length "^2.0.0" + +"@jest/source-map@^24.3.0": + version "24.3.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.3.0.tgz#563be3aa4d224caf65ff77edc95cd1ca4da67f28" + integrity sha512-zALZt1t2ou8le/crCeeiRYzvdnTzaIlpOWaet45lNSqNJUnXbppUUFR4ZUAlzgDmKee4Q5P/tKXypI1RiHwgag== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.1.15" + source-map "^0.6.0" + +"@jest/test-result@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.8.0.tgz#7675d0aaf9d2484caa65e048d9b467d160f8e9d3" + integrity sha512-+YdLlxwizlfqkFDh7Mc7ONPQAhA4YylU1s529vVM1rsf67vGZH/2GGm5uO8QzPeVyaVMobCQ7FTxl38QrKRlng== + dependencies: + "@jest/console" "^24.7.1" + "@jest/types" "^24.8.0" + "@types/istanbul-lib-coverage" "^2.0.0" + +"@jest/test-sequencer@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.8.0.tgz#2f993bcf6ef5eb4e65e8233a95a3320248cf994b" + integrity sha512-OzL/2yHyPdCHXEzhoBuq37CE99nkme15eHkAzXRVqthreWZamEMA0WoetwstsQBCXABhczpK03JNbc4L01vvLg== + dependencies: + "@jest/test-result" "^24.8.0" + jest-haste-map "^24.8.0" + jest-runner "^24.8.0" + jest-runtime "^24.8.0" + +"@jest/transform@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.8.0.tgz#628fb99dce4f9d254c6fd9341e3eea262e06fef5" + integrity sha512-xBMfFUP7TortCs0O+Xtez2W7Zu1PLH9bvJgtraN1CDST6LBM/eTOZ9SfwS/lvV8yOfcDpFmwf9bq5cYbXvqsvA== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^24.8.0" + babel-plugin-istanbul "^5.1.0" + chalk "^2.0.1" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.1.15" + jest-haste-map "^24.8.0" + jest-regex-util "^24.3.0" + jest-util "^24.8.0" + micromatch "^3.1.10" + realpath-native "^1.1.0" + slash "^2.0.0" + source-map "^0.6.1" + write-file-atomic "2.4.1" + +"@jest/types@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.8.0.tgz#f31e25948c58f0abd8c845ae26fcea1491dea7ad" + integrity sha512-g17UxVr2YfBtaMUxn9u/4+siG1ptg9IGYAYwvpwn61nBg779RXnjE/m7CxYcIzEt0AbHZZAHSEZNhkE2WxURVg== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^12.0.9" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -662,10 +815,92 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== +"@types/babel__core@^7.1.0": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f" + integrity sha512-cfCCrFmiGY/yq0NuKNxIQvZFy9kY/1immpSpTngOnyIbD4+eJOG5mxphhHDv3CHL9GltO4GcKr54kGBg3RNdbg== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.0.2.tgz#d2112a6b21fad600d7674274293c85dce0cb47fc" + integrity sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307" + integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.7.tgz#2496e9ff56196cc1429c72034e07eab6121b6f3f" + integrity sha512-CeBpmX1J8kWLcDEnI3Cl2Eo6RfbGvzUctA+CjZUhOKDFbLfcr7fc4usEqLNWetrlJd7RhAkyYe2czXop4fICpw== + dependencies: + "@babel/types" "^7.3.0" + +"@types/events@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + +"@types/glob@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" + integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + +"@types/istanbul-lib-report@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#e5471e7fa33c61358dd38426189c037a58433b8c" + integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" + integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + "@types/node@*": - version "10.12.21" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.21.tgz#7e8a0c34cf29f4e17a36e9bd0ea72d45ba03908e" - integrity sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ== + version "12.0.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.8.tgz#551466be11b2adc3f3d47156758f610bd9f6b1d8" + integrity sha512-b8bbUOTwzIY3V5vDTY1fIJ+ePKDUBqt2hC2woVGotdQQhG/2Sh62HOKHrT7ab+VerXAcPyAiTEipPu/FsreUtg== + +"@types/stack-utils@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" + integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== + +"@types/yargs@^12.0.2", "@types/yargs@^12.0.9": + version "12.0.12" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916" + integrity sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw== abab@^1.0.0: version "1.0.4" @@ -682,13 +917,13 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -accepts@~1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" - integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I= +accepts@~1.3.5, accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== dependencies: - mime-types "~2.1.18" - negotiator "0.6.1" + mime-types "~2.1.24" + negotiator "0.6.2" acorn-globals@^1.0.4: version "1.0.9" @@ -698,9 +933,9 @@ acorn-globals@^1.0.4: acorn "^2.1.0" acorn-globals@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.0.tgz#e3b6f8da3c1552a95ae627571f7dd6923bb54103" - integrity sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw== + version "4.3.2" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.2.tgz#4e2c2313a597fd589720395f6354b41cd5ec8006" + integrity sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ== dependencies: acorn "^6.0.1" acorn-walk "^6.0.1" @@ -725,20 +960,20 @@ acorn@^5.5.3: resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== -acorn@^6.0.1, acorn@^6.0.2: - version "6.0.7" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.7.tgz#490180ce18337270232d9488a44be83d9afb7fd3" - integrity sha512-HNJNgE60C9eOTgn974Tlp3dpLZdUr+SoxxDwPaY9J/kDNOLQTkaDgwBUXAF4SSsrAwD9RpdxuHK/EbuF+W9Ahw== +acorn@^6.0.1, acorn@^6.0.7: + version "6.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" + integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== add-matchers@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/add-matchers/-/add-matchers-0.6.2.tgz#faeb2fa9380228c1b0da5f21a655c69304dd2011" integrity sha512-hVO2wodMei9RF00qe+506MoeJ/NEOdCMEkSJ12+fC3hx/5Z4zmhNiP92nJEF6XhmXokeB0hOtuQrjHCx2vmXrQ== -ajv@^6.5.3, ajv@^6.5.5, ajv@^6.6.1: - version "6.8.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.8.1.tgz#0890b93742985ebf8973cd365c5b23920ce3cb20" - integrity sha512-eqxCp82P+JfqL683wwsL73XmFs1eG6qjw+RD3YHx+Jll1r0jNd4dh8QG9NYAeNGA/hnZjeEDgtTskgJULbxpWQ== +ajv@^6.5.5, ajv@^6.9.1: + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== dependencies: fast-deep-equal "^2.0.1" fast-json-stable-stringify "^2.0.0" @@ -760,10 +995,10 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9" - integrity sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w== +ansi-regex@^4.0.0, ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== ansi-styles@^2.2.1: version "2.2.1" @@ -790,13 +1025,6 @@ append-field@^1.0.0: resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= -append-transform@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" - integrity sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw== - dependencies: - default-require-extensions "^2.0.0" - aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -842,6 +1070,14 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= +array-includes@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" + integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + array-union@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -859,11 +1095,6 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -arrify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= - asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -896,12 +1127,10 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== -async@^2.5.0, async@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== - dependencies: - lodash "^4.17.10" +async@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/async/-/async-3.0.1.tgz#dfeb34657d1e63c94c0eee424297bf8a2c9a8182" + integrity sha512-ZswD8vwPtmBZzbn9xyi8XBQWXH3AvOQ43Za1KWYq7JeycrZuUYzx01KvHcVbXltjqH4y0MWrQ33008uLTqXuDw== asynckit@^0.4.0: version "0.4.0" @@ -958,13 +1187,16 @@ babel-generator@6.26.1: source-map "^0.5.7" trim-right "^1.0.1" -babel-jest@^24.1.0: - version "24.1.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.1.0.tgz#441e23ef75ded3bd547e300ac3194cef87b55190" - integrity sha512-MLcagnVrO9ybQGLEfZUqnOzv36iQzU7Bj4elm39vCukumLVSfoX+tRy3/jW7lUKc7XdpRmB/jech6L/UCsSZjw== +babel-jest@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.8.0.tgz#5c15ff2b28e20b0f45df43fe6b7f2aae93dba589" + integrity sha512-+5/kaZt4I9efoXzPlZASyK/lN9qdRKmmUav9smVc0ruPQD7IsfucQ87gpOE8mn2jbDuS6M/YOW6n3v9ZoIfgnw== dependencies: + "@jest/transform" "^24.8.0" + "@jest/types" "^24.8.0" + "@types/babel__core" "^7.1.0" babel-plugin-istanbul "^5.1.0" - babel-preset-jest "^24.1.0" + babel-preset-jest "^24.6.0" chalk "^2.4.2" slash "^2.0.0" @@ -976,26 +1208,28 @@ babel-messages@^6.23.0, babel-messages@^6.8.0: babel-runtime "^6.22.0" babel-plugin-istanbul@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.0.tgz#6892f529eff65a3e2d33d87dc5888ffa2ecd4a30" - integrity sha512-CLoXPRSUWiR8yao8bShqZUIC6qLfZVVY3X1wj+QPNXu0wfmrRRfarh1LYy+dYMVI+bDj0ghy3tuqFFRFZmL1Nw== + version "5.1.4" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.4.tgz#841d16b9a58eeb407a0ddce622ba02fe87a752ba" + integrity sha512-dySz4VJMH+dpndj0wjJ8JPs/7i1TdSPb1nRrn56/92pKOF9VKC1FMFJmMXjzlGGusnCAqujP6PBCiKq0sVA+YQ== dependencies: find-up "^3.0.0" - istanbul-lib-instrument "^3.0.0" - test-exclude "^5.0.0" + istanbul-lib-instrument "^3.3.0" + test-exclude "^5.2.3" -babel-plugin-jest-hoist@^24.1.0: - version "24.1.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.1.0.tgz#dfecc491fb15e2668abbd690a697a8fd1411a7f8" - integrity sha512-gljYrZz8w1b6fJzKcsfKsipSru2DU2DmQ39aB6nV3xQ0DDv3zpIzKGortA5gknrhNnPN8DweaEgrnZdmbGmhnw== +babel-plugin-jest-hoist@^24.6.0: + version "24.6.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.6.0.tgz#f7f7f7ad150ee96d7a5e8e2c5da8319579e78019" + integrity sha512-3pKNH6hMt9SbOv0F3WVmy5CWQ4uogS3k0GY5XLyQHJ9EGpAT9XWkFd2ZiXXtkwFHdAHa5j7w7kfxSP5lAIwu7w== + dependencies: + "@types/babel__traverse" "^7.0.6" -babel-preset-jest@^24.1.0: - version "24.1.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.1.0.tgz#83bc564fdcd4903641af65ec63f2f5de6b04132e" - integrity sha512-FfNLDxFWsNX9lUmtwY7NheGlANnagvxq8LZdl5PKnVG3umP+S/g0XbVBfwtA4Ai3Ri/IMkWabBz3Tyk9wdspcw== +babel-preset-jest@^24.6.0: + version "24.6.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.6.0.tgz#66f06136eefce87797539c0d63f1769cc3915984" + integrity sha512-pdZqLEdmy1ZK5kyRUfvBb2IfTPb2BUvIJczlPspS8fWmBQslNNDBqVfh7BW5leOVJMDZKzjD8XEyABTk6gQ5yw== dependencies: "@babel/plugin-syntax-object-rest-spread" "^7.0.0" - babel-plugin-jest-hoist "^24.1.0" + babel-plugin-jest-hoist "^24.6.0" babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.0: version "6.26.0" @@ -1065,21 +1299,21 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -body-parser@1.18.3: - version "1.18.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" - integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ= +body-parser@1.19.0, body-parser@^1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== dependencies: - bytes "3.0.0" + bytes "3.1.0" content-type "~1.0.4" debug "2.6.9" depd "~1.1.2" - http-errors "~1.6.3" - iconv-lite "0.4.23" + http-errors "1.7.2" + iconv-lite "0.4.24" on-finished "~2.3.0" - qs "6.5.2" - raw-body "2.3.3" - type-is "~1.6.16" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" boolbase@~1.0.0: version "1.0.0" @@ -1122,14 +1356,14 @@ browser-resolve@^1.11.3: dependencies: resolve "1.1.7" -browserslist@^4.1.0, browserslist@^4.3.4: - version "4.4.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.1.tgz#42e828954b6b29a7a53e352277be429478a69062" - integrity sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A== +browserslist@^4.6.0, browserslist@^4.6.2: + version "4.6.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.3.tgz#0530cbc6ab0c1f3fc8c819c72377ba55cf647f05" + integrity sha512-CNBqTCq22RKM8wKJNowcqihHJ4SkI8CGeK7KOR9tPboXUuS5Zk5lQgzzTbs4oxD8x+6HUshZUa2OyNI9lR93bQ== dependencies: - caniuse-lite "^1.0.30000929" - electron-to-chromium "^1.3.103" - node-releases "^1.1.3" + caniuse-lite "^1.0.30000975" + electron-to-chromium "^1.3.164" + node-releases "^1.1.23" bser@^2.0.0: version "2.0.0" @@ -1156,6 +1390,11 @@ bytes@3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -1177,9 +1416,9 @@ call-me-maybe@^1.0.1: integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= callsites@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.0.0.tgz#fb7eb569b72ad7a45812f93fd9430a3e410b3dd3" - integrity sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw== + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camel-case@3.0.x: version "3.0.0" @@ -1190,21 +1429,21 @@ camel-case@3.0.x: upper-case "^1.1.1" camelcase@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" - integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30000929: - version "1.0.30000935" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000935.tgz#d1b59df00b46f4921bb84a8a34c1d172b346df59" - integrity sha512-1Y2uJ5y56qDt3jsDTdBHL1OqiImzjoQcBG6Yl3Qizq8mcc2SgCFpi+ZwLLqkztYnk9l87IYqRlNBnPSOTbFkXQ== +caniuse-lite@^1.0.30000975: + version "1.0.30000975" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000975.tgz#d4e7131391dddcf2838999d3ce75065f65f1cdfc" + integrity sha512-ZsXA9YWQX6ATu5MNg+Vx/cMQ+hM6vBBSqDeJs8ruk9z0ky4yIHML15MoxcFt088ST2uyjgqyUGRJButkptWf0w== -capture-exit@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f" - integrity sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28= +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== dependencies: - rsvp "^3.3.3" + rsvp "^4.8.4" caseless@~0.12.0: version "0.12.0" @@ -1293,11 +1532,6 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -circular-json@^0.3.1: - version "0.3.3" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" - integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== - class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -1322,16 +1556,6 @@ cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" -cli-table3@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" - integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== - dependencies: - object-assign "^4.1.0" - string-width "^2.1.1" - optionalDependencies: - colors "^1.1.2" - cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" @@ -1386,55 +1610,55 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -colors@1.3.3, colors@^1.1.2: +colors@1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg== combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" - integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" -commander@2.17.x, commander@~2.17.1: +commander@2.17.x: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@^2.19.0: +commander@^2.20.0, commander@~2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" + integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== + +commander@~2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== -compare-versions@^3.2.1: - version "3.4.0" - resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.4.0.tgz#e0747df5c9cb7f054d6d3dc3e1dbc444f9e92b26" - integrity sha512-tK69D7oNXXqUW3ZNo/z7NXTEz22TCF0pTE+YF9cxvaAM9XnkLo1fV621xCLrRR6aevJlKxExkss0vWqUCUpqdg== - component-emitter@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" - integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== -compressible@~2.0.14: - version "2.0.15" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.15.tgz#857a9ab0a7e5a07d8d837ed43fe2defff64fe212" - integrity sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw== +compressible@~2.0.16: + version "2.0.17" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.17.tgz#6e8c108a16ad58384a977f3a482ca20bff2f38c1" + integrity sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw== dependencies: - mime-db ">= 1.36.0 < 2" + mime-db ">= 1.40.0 < 2" -compression@1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db" - integrity sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg== +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== dependencies: accepts "~1.3.5" bytes "3.0.0" - compressible "~2.0.14" + compressible "~2.0.16" debug "2.6.9" - on-headers "~1.0.1" + on-headers "~1.0.2" safe-buffer "5.1.2" vary "~1.1.2" @@ -1463,10 +1687,12 @@ contains-path@^0.1.0: resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" content-type@~1.0.4: version "1.0.4" @@ -1485,37 +1711,51 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" - integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +core-js-compat@^3.1.1: + version "3.1.4" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.1.4.tgz#e4d0c40fbd01e65b1d457980fe4112d4358a7408" + integrity sha512-Z5zbO9f1d0YrJdoaQhphVAnKPimX92D6z8lCGphH89MNRxlL1prI9ExJPqVwP0/kgkQCv8c4GJGT8X16yUncOg== + dependencies: + browserslist "^4.6.2" + core-js-pure "3.1.4" + semver "^6.1.1" + +core-js-pure@3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.1.4.tgz#5fa17dc77002a169a3566cc48dc774d2e13e3769" + integrity sha512-uJ4Z7iPNwiu1foygbcZYJsJs1jiXrTTCvxfLDXNhI/I+NHbSIEyr548y4fcsCEyWY0XgfAG/qqaunJ1SThHenA== + core-js@^2.4.0: - version "2.6.3" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.3.tgz#4b70938bdffdaf64931e66e2db158f0892289c49" - integrity sha512-l00tmFFZOBHtYhN4Cz7k32VM7vTn3rE2ANjQDxdEN6zmXZ/xq1jQuutnmHvMG1ZJ7xd72+TA5YpUK8wz3rWsfQ== + version "2.6.9" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" + integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -coveralls@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.0.2.tgz#f5a0bcd90ca4e64e088b710fa8dda640aea4884f" - integrity sha512-Tv0LKe/MkBOilH2v7WBiTBdudg2ChfGbdXafc/s330djpF3zKOmuehTeRwjXWc7pzfj9FrDUTA7tEx6Div8NFw== +coveralls@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.0.4.tgz#f50233c9c62fd0973f710fce85fd19dba24cff4b" + integrity sha512-eyqUWA/7RT0JagiL0tThVhjbIjoiEUyWCjtUJoOPcWoeofP5WK/jb2OJYoBFrR6DvplR+AxOyuBqk4JHkk5ykA== dependencies: growl "~> 1.10.0" js-yaml "^3.11.0" lcov-parse "^0.0.10" log-driver "^1.2.7" minimist "^1.2.0" - request "^2.85.0" + request "^2.86.0" cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" @@ -1539,9 +1779,9 @@ css-select@~1.2.0: nth-check "~1.0.1" css-what@2.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.2.tgz#c0876d9d0480927d7d4920dcd72af3595649554d" - integrity sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ== + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0", "cssom@>= 0.3.2 < 0.4.0": version "0.3.6" @@ -1556,9 +1796,9 @@ cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0", "cssom@>= 0.3.2 < 0.4.0": cssom "0.3.x" cssstyle@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.1.1.tgz#18b038a9c44d65f7a8e428a653b9f6fe42faf5fb" - integrity sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog== + version "1.2.2" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.2.2.tgz#427ea4d585b18624f6fdbf9de7a2a1a3ba713077" + integrity sha512-43wY3kl1CVQSvL7wUY1qXkxVGkStjpkDmVjiIKX8R97uhajy8Bybay78uOtqvh7Q5GK75dNPfW0geWjE6qQQow== dependencies: cssom "0.3.x" @@ -1578,13 +1818,20 @@ data-urls@^1.0.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" +debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" @@ -1612,13 +1859,6 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= -default-require-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7" - integrity sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc= - dependencies: - strip-bom "^3.0.0" - define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -1702,12 +1942,12 @@ dicer@0.2.5: readable-stream "1.1.x" streamsearch "0.1.2" -diff-sequences@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.0.0.tgz#cdf8e27ed20d8b8d3caccb4e0c0d8fe31a173013" - integrity sha512-46OkIuVGBBnrC0soO/4LHu5LHGHx0uhP65OVz8XOrAJpqiCB2aVIuESvjI1F9oqebuvY8lekS1pt6TN7vt7qsw== +diff-sequences@^24.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.3.0.tgz#0f20e8a1df1abddaf4d9c226680952e64118b975" + integrity sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw== -dir-glob@^2.2.1: +dir-glob@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== @@ -1722,31 +1962,26 @@ doctrine@1.5.0: esutils "^2.0.2" isarray "^1.0.0" -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== dependencies: esutils "^2.0.2" dom-serializer@0, dom-serializer@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" - integrity sha1-BzxpdUbOB4DOI75KKOKT5AvDDII= + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== dependencies: - domelementtype "~1.1.1" - entities "~1.1.1" + domelementtype "^1.3.0" + entities "^1.1.1" -domelementtype@1, domelementtype@^1.3.0: +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== -domelementtype@~1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" - integrity sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs= - domexception@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" @@ -1797,10 +2032,15 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.103: - version "1.3.113" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9" - integrity sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g== +electron-to-chromium@^1.3.164: + version "1.3.166" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.166.tgz#99d267514f4b92339788172400bc527545deb75b" + integrity sha512-7XwtJz81H/PBnkmQ/07oVPOGTkBZs6ibZN8OqXNUrxjRPzR0Xj+MFcMmRZEXGilEg1Pm+97V8BZVI63qnBX1hQ== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== emojis-list@^2.0.0: version "2.1.0" @@ -1841,7 +2081,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.12.0, es-abstract@^1.5.1: +es-abstract@^1.12.0, es-abstract@^1.5.1, es-abstract@^1.7.0: version "1.13.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== @@ -1881,9 +2121,9 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= escodegen@^1.6.1, escodegen@^1.9.1: - version "1.11.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589" - integrity sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw== + version "1.11.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510" + integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw== dependencies: esprima "^3.1.3" estraverse "^4.2.0" @@ -1931,7 +2171,7 @@ esdoc-lint-plugin@^1.0.0: resolved "https://registry.yarnpkg.com/esdoc-lint-plugin/-/esdoc-lint-plugin-1.0.2.tgz#4962930c6dc5b25d80cf6eff1b0f3c24609077f7" integrity sha512-24AYqD2WbZI9We02I7/6dzAa7yUliRTFUaJCZAcYJMQicJT5gUrNFVaI8XmWEN/mhF3szIn1uZBNWeLul4CmNw== -esdoc-node@1.0.4: +esdoc-node@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/esdoc-node/-/esdoc-node-1.0.4.tgz#97fec78a9bbed26f15349c03c9b9e2f8a6baafe5" integrity sha512-th9rhOdO3oFiv9YJWv8+RT2fzILxhvlvT9w0bZJVU9y6glXyA1zAiGrFAAI96HC8ArGOnlXDz6jbTBBAWa3UXg== @@ -1949,7 +2189,7 @@ esdoc-publish-html-plugin@^1.0.0: marked "0.3.19" taffydb "2.7.2" -esdoc-standard-plugin@1.0.0: +esdoc-standard-plugin@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/esdoc-standard-plugin/-/esdoc-standard-plugin-1.0.0.tgz#661201cac7ef868924902446fdac1527253c5d4d" integrity sha1-ZhIBysfvhokkkCRG/awVJyU8XU0= @@ -1981,7 +2221,7 @@ esdoc-unexported-identifier-plugin@^1.0.0: resolved "https://registry.yarnpkg.com/esdoc-unexported-identifier-plugin/-/esdoc-unexported-identifier-plugin-1.0.0.tgz#1f9874c6a7c2bebf9ad397c3ceb75c9c69dabab1" integrity sha1-H5h0xqfCvr+a05fDzrdcnGnaurE= -esdoc@1.1.0: +esdoc@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/esdoc/-/esdoc-1.1.0.tgz#07d40ebf791764cd537929c29111e20a857624f3" integrity sha512-vsUcp52XJkOWg9m1vDYplGZN2iDzvmjDL5M/Mp8qkoDG3p2s0yIQCIjKR5wfPBaM3eV14a6zhQNYiNTCVzPnxA== @@ -1998,7 +2238,7 @@ esdoc@1.1.0: minimist "1.2.0" taffydb "2.7.3" -eslint-config-airbnb-base@13.1.0: +eslint-config-airbnb-base@^13.1.0: version "13.1.0" resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-13.1.0.tgz#b5a1b480b80dfad16433d6c4ad84e6605052c05c" integrity sha512-XWwQtf3U3zIoKO1BbHh6aUhJZQweOwSt4c2JrPDg9FP3Ltv3+YfEv7jIDB8275tVnO/qOHbfuYg3kzw6Je7uWw== @@ -2015,15 +2255,15 @@ eslint-import-resolver-node@^0.3.2: debug "^2.6.9" resolve "^1.5.0" -eslint-module-utils@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz#546178dab5e046c8b562bbb50705e2456d7bda49" - integrity sha512-lmDJgeOOjk8hObTysjqH7wyMi+nsHwwvfBykwfhjR1LNdd7C2uFJBvx4OpWYpXOw4df1yE1cDEVd1yLHitk34w== +eslint-module-utils@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz#8b93499e9b00eab80ccb6614e69f03678e84e09a" + integrity sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw== dependencies: debug "^2.6.8" pkg-dir "^2.0.0" -eslint-plugin-es@^1.3.1: +eslint-plugin-es@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz#475f65bb20c993fc10e8c8fe77d1d60068072da6" integrity sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw== @@ -2031,43 +2271,44 @@ eslint-plugin-es@^1.3.1: eslint-utils "^1.3.0" regexpp "^2.0.1" -eslint-plugin-import@2.16.0: - version "2.16.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz#97ac3e75d0791c4fac0e15ef388510217be7f66f" - integrity sha512-z6oqWlf1x5GkHIFgrSvtmudnqM6Q60KM4KvpWi5ubonMjycLjndvd5+8VAZIsTlHC03djdgJuyKG6XO577px6A== +eslint-plugin-import@^2.17.3: + version "2.17.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.17.3.tgz#00548b4434c18faebaba04b24ae6198f280de189" + integrity sha512-qeVf/UwXFJbeyLbxuY8RgqDyEKCkqV7YC+E5S5uOjAp4tOc8zj01JP3ucoBM8JcEqd1qRasJSg6LLlisirfy0Q== dependencies: + array-includes "^3.0.3" contains-path "^0.1.0" debug "^2.6.9" doctrine "1.5.0" eslint-import-resolver-node "^0.3.2" - eslint-module-utils "^2.3.0" + eslint-module-utils "^2.4.0" has "^1.0.3" lodash "^4.17.11" minimatch "^3.0.4" read-pkg-up "^2.0.0" - resolve "^1.9.0" + resolve "^1.11.0" -eslint-plugin-node@8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-8.0.1.tgz#55ae3560022863d141fa7a11799532340a685964" - integrity sha512-ZjOjbjEi6jd82rIpFSgagv4CHWzG9xsQAVp1ZPlhRnnYxcTgENUVBvhYmkQ7GvT1QFijUSo69RaiOJKhMu6i8w== +eslint-plugin-node@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.1.0.tgz#f2fd88509a31ec69db6e9606d76dabc5adc1b91a" + integrity sha512-ZwQYGm6EoV2cfLpE1wxJWsfnKUIXfM/KM09/TlorkukgCAwmkgajEJnPCmyzoFPQQkmvo5DrW/nyKutNIw36Mw== dependencies: - eslint-plugin-es "^1.3.1" + eslint-plugin-es "^1.4.0" eslint-utils "^1.3.1" - ignore "^5.0.2" + ignore "^5.1.1" minimatch "^3.0.4" - resolve "^1.8.1" - semver "^5.5.0" + resolve "^1.10.1" + semver "^6.1.0" eslint-restricted-globals@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7" integrity sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc= -eslint-scope@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" - integrity sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA== +eslint-scope@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" + integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" @@ -2082,35 +2323,35 @@ eslint-visitor-keys@^1.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ== -eslint@5.13.0: - version "5.13.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.13.0.tgz#ce71cc529c450eed9504530939aa97527861ede9" - integrity sha512-nqD5WQMisciZC5EHZowejLKQjWGuFS5c70fxqSKlnDME+oz9zmE8KTlX+lHSg+/5wsC/kf9Q9eMkC8qS3oM2fg== +eslint@^5.16.0: + version "5.16.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea" + integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg== dependencies: "@babel/code-frame" "^7.0.0" - ajv "^6.5.3" + ajv "^6.9.1" chalk "^2.1.0" cross-spawn "^6.0.5" debug "^4.0.1" - doctrine "^2.1.0" - eslint-scope "^4.0.0" + doctrine "^3.0.0" + eslint-scope "^4.0.3" eslint-utils "^1.3.1" eslint-visitor-keys "^1.0.0" - espree "^5.0.0" + espree "^5.0.1" esquery "^1.0.1" esutils "^2.0.2" - file-entry-cache "^2.0.0" + file-entry-cache "^5.0.1" functional-red-black-tree "^1.0.1" glob "^7.1.2" globals "^11.7.0" ignore "^4.0.6" import-fresh "^3.0.0" imurmurhash "^0.1.4" - inquirer "^6.1.0" - js-yaml "^3.12.0" + inquirer "^6.2.2" + js-yaml "^3.13.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.3.0" - lodash "^4.17.5" + lodash "^4.17.11" minimatch "^3.0.4" mkdirp "^0.5.1" natural-compare "^1.4.0" @@ -2121,15 +2362,15 @@ eslint@5.13.0: semver "^5.5.1" strip-ansi "^4.0.0" strip-json-comments "^2.0.1" - table "^5.0.2" + table "^5.2.3" text-table "^0.2.0" -espree@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.0.tgz#fc7f984b62b36a0f543b13fb9cd7b9f4a7f5b65c" - integrity sha512-1MpUfwsdS9MMoN7ZXqAr9e9UKdVHDcvrJpyx7mm1WuQlx/ygErEQBzgi5Nh5qBHIoYweprhtMkTCb9GhcAIcsA== +espree@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a" + integrity sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A== dependencies: - acorn "^6.0.2" + acorn "^6.0.7" acorn-jsx "^5.0.0" eslint-visitor-keys "^1.0.0" @@ -2172,12 +2413,10 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -exec-sh@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" - integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw== - dependencies: - merge "^1.2.0" +exec-sh@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" + integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== execa@^1.0.0: version "1.0.0" @@ -2210,50 +2449,51 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -expect@^24.1.0: - version "24.1.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-24.1.0.tgz#88e73301c4c785cde5f16da130ab407bdaf8c0f2" - integrity sha512-lVcAPhaYkQcIyMS+F8RVwzbm1jro20IG8OkvxQ6f1JfqhVZyyudCwYogQ7wnktlf14iF3ii7ArIUO/mqvrW9Gw== +expect@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-24.8.0.tgz#471f8ec256b7b6129ca2524b2a62f030df38718d" + integrity sha512-/zYvP8iMDrzaaxHVa724eJBCKqSHmO0FA7EDkBiRHxg6OipmMn1fN+C8T9L9K8yr7UONkOifu6+LLH+z76CnaA== dependencies: + "@jest/types" "^24.8.0" ansi-styles "^3.2.0" - jest-get-type "^24.0.0" - jest-matcher-utils "^24.0.0" - jest-message-util "^24.0.0" - jest-regex-util "^24.0.0" + jest-get-type "^24.8.0" + jest-matcher-utils "^24.8.0" + jest-message-util "^24.8.0" + jest-regex-util "^24.3.0" -express@4.16.4: - version "4.16.4" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" - integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== dependencies: - accepts "~1.3.5" + accepts "~1.3.7" array-flatten "1.1.1" - body-parser "1.18.3" - content-disposition "0.5.2" + body-parser "1.19.0" + content-disposition "0.5.3" content-type "~1.0.4" - cookie "0.3.1" + cookie "0.4.0" cookie-signature "1.0.6" debug "2.6.9" depd "~1.1.2" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.1.1" + finalhandler "~1.1.2" fresh "0.5.2" merge-descriptors "1.0.1" methods "~1.1.2" on-finished "~2.3.0" - parseurl "~1.3.2" + parseurl "~1.3.3" path-to-regexp "0.1.7" - proxy-addr "~2.0.4" - qs "6.5.2" - range-parser "~1.2.0" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" safe-buffer "5.1.2" - send "0.16.2" - serve-static "1.13.2" - setprototypeof "1.1.0" - statuses "~1.4.0" - type-is "~1.6.16" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" utils-merge "1.0.1" vary "~1.1.2" @@ -2272,7 +2512,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@3.0.2, extend@~3.0.2: +extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -2316,9 +2556,9 @@ fast-deep-equal@^2.0.1: integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= fast-glob@^2.2.6: - version "2.2.6" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.6.tgz#a5d5b697ec8deda468d85a74035290a025a95295" - integrity sha512-0BvMaZc1k9F+MeWWMe8pL6YltFzZYcJsYU7D4JyDA6PAczaXvxqQQ/z+mDF7/4Mw01DeUc+i3CTKajnkANkV4w== + version "2.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" + integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== dependencies: "@mrmlnc/readdir-enhanced" "^2.2.1" "@nodelib/fs.stat" "^1.1.2" @@ -2356,21 +2596,12 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" -file-entry-cache@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" - integrity sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E= - dependencies: - flat-cache "^1.2.1" - object-assign "^4.0.1" - -fileset@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" - integrity sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA= +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== dependencies: - glob "^7.0.3" - minimatch "^3.0.3" + flat-cache "^2.0.1" fill-range@^4.0.0: version "4.0.0" @@ -2382,17 +2613,17 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" -finalhandler@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" - integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg== +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== dependencies: debug "2.6.9" encodeurl "~1.0.2" escape-html "~1.0.3" on-finished "~2.3.0" - parseurl "~1.3.2" - statuses "~1.4.0" + parseurl "~1.3.3" + statuses "~1.5.0" unpipe "~1.0.0" find-up@^2.0.0, find-up@^2.1.0: @@ -2409,15 +2640,19 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -flat-cache@^1.2.1: - version "1.3.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.4.tgz#2c2ef77525cc2929007dfffa1dd314aa9c9dee6f" - integrity sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg== +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== dependencies: - circular-json "^0.3.1" - graceful-fs "^4.1.2" - rimraf "~2.6.2" - write "^0.2.1" + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916" + integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg== for-in@^1.0.2: version "1.0.2" @@ -2473,19 +2708,19 @@ fs-extra@5.0.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" - integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== +fs-extra@8.0.1, fs-extra@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.0.1.tgz#90294081f978b1f182f347a440a209154344285b" + integrity sha512-W+XLrggcDzlle47X/XnS7FXrXu9sDo+Ze9zpndeBxdgv88FHLm1HtmkhEwavruS6koanBjp098rUpHs65EmG7A== dependencies: graceful-fs "^4.1.2" jsonfile "^4.0.0" universalify "^0.1.0" fs-minipass@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" - integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ== + version "1.2.6" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" + integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ== dependencies: minipass "^2.2.1" @@ -2494,13 +2729,13 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^1.2.3: - version "1.2.7" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4" - integrity sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw== +fsevents@^1.2.7: + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== dependencies: - nan "^2.9.2" - node-pre-gyp "^0.10.0" + nan "^2.12.1" + node-pre-gyp "^0.12.0" function-bind@^1.1.1: version "1.1.1" @@ -2531,15 +2766,20 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= -get-stdin@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" - integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== +get-stdin@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6" + integrity sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ== get-stream@^4.0.0: version "4.1.0" @@ -2573,7 +2813,7 @@ glob-to-regexp@^0.3.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= -glob@7.1.3, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: +glob@7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== @@ -2585,23 +2825,36 @@ glob@7.1.3, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: + version "7.1.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" + integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + globals@^11.1.0, globals@^11.7.0: - version "11.10.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.10.0.tgz#1e09776dffda5e01816b3bb4077c8b59c24eaa50" - integrity sha512-0GZF1RiPKU97IHUO5TORo9w1PwrH/NBPl+fS7oMLdaTRiYmYbwK4NWoZWrAdd0/abG9R2BU+OiwyQpTpE6pdfQ== + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== -globby@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-9.0.0.tgz#3800df736dc711266df39b4ce33fe0d481f94c23" - integrity sha512-q0qiO/p1w/yJ0hk8V9x1UXlgsXUxlGd0AHUOXZVXBO6aznDtpx7M8D1kBrCAItoPm+4l8r6ATXV1JpjY2SBQOw== +globby@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d" + integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg== dependencies: + "@types/glob" "^7.1.1" array-union "^1.0.2" - dir-glob "^2.2.1" + dir-glob "^2.2.2" fast-glob "^2.2.6" glob "^7.1.3" ignore "^4.0.3" @@ -2623,12 +2876,12 @@ growly@^1.3.0: resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= -handlebars@^4.0.11: - version "4.0.12" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5" - integrity sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA== +handlebars@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" + integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== dependencies: - async "^2.5.0" + neo-async "^2.6.0" optimist "^0.6.1" source-map "^0.6.1" optionalDependencies: @@ -2749,16 +3002,16 @@ html-minifier@^3.5.8: uglify-js "3.4.x" htmlparser2@^3.9.1: - version "3.10.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464" - integrity sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ== + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== dependencies: - domelementtype "^1.3.0" + domelementtype "^1.3.1" domhandler "^2.3.0" domutils "^1.5.1" entities "^1.1.1" inherits "^2.0.1" - readable-stream "^3.0.6" + readable-stream "^3.1.1" htmlparser2@~3.8.1: version "3.8.3" @@ -2771,15 +3024,16 @@ htmlparser2@~3.8.1: entities "1.0" readable-stream "1.1" -http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= +http-errors@1.7.2, http-errors@~1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== dependencies: depd "~1.1.2" inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" http-signature@~1.2.0: version "1.2.0" @@ -2798,13 +3052,6 @@ ice-cap@0.0.4: cheerio "0.20.0" color-logger "0.0.3" -iconv-lite@0.4.23: - version "0.4.23" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" - integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -2824,10 +3071,10 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.0.2: - version "5.0.5" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.0.5.tgz#c663c548d6ce186fb33616a8ccb5d46e56bdbbf9" - integrity sha512-kOC8IUb8HSDMVcYrDVezCxpJkzSQWTAzf3olpKM6o9rM5zpojx23O0Fl8Wr4+qJ6ZbPEHqf1fdwev/DS7v7pmA== +ignore@^5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.2.tgz#e28e584d43ad7e92f96995019cc43b9e1ac49558" + integrity sha512-vdqWBp7MyzdmHkkRWV5nY+PfGRbYbahfuvsBCh277tq+w9zyNi7h5CYJCK0kmzti9kU+O/cB7sE8HvKv6aXAKQ== import-fresh@^3.0.0: version "3.0.0" @@ -2858,7 +3105,12 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= @@ -2868,10 +3120,10 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -inquirer@^6.1.0: - version "6.2.2" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.2.tgz#46941176f65c9eb20804627149b743a218f25406" - integrity sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA== +inquirer@^6.2.2: + version "6.4.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.4.0.tgz#6f9284047c4e48b76b169e46b3ae3b2171ce30a2" + integrity sha512-O3qJQ+fU/AI1K2y5/RjqefMEQTdJQf6sPTvyRA1bx6D634ADxcu97u6YOUciIeU2OWIuvpUsQs6Wx3Fdi3eFaQ== dependencies: ansi-escapes "^3.2.0" chalk "^2.4.2" @@ -2884,7 +3136,7 @@ inquirer@^6.1.0: run-async "^2.2.0" rxjs "^6.4.0" string-width "^2.1.0" - strip-ansi "^5.0.0" + strip-ansi "^5.1.0" through "^2.3.6" invariant@^2.2.2, invariant@^2.2.4: @@ -2899,15 +3151,10 @@ invert-kv@^2.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== -ip-regex@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" - integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= - -ipaddr.js@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" - integrity sha1-6qM9bd16zo9/b+DJygRA5wZzix4= +ipaddr.js@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" + integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== is-accessor-descriptor@^0.1.6: version "0.1.6" @@ -3019,9 +3266,9 @@ is-fullwidth-code-point@^2.0.0: integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= is-generator-fn@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.0.0.tgz#038c31b774709641bda678b1f06a4e3227c10b3e" - integrity sha512-elzyIdM7iKoFHzcrndIqjYomImhxrFRnGP3galODoII4TB9gI7mZ+FnlLQmmjf27SxHS2gKEeyhX5/+YRS6H9g== + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== is-glob@^3.1.0: version "3.1.0" @@ -3031,9 +3278,9 @@ is-glob@^3.1.0: is-extglob "^2.1.0" is-glob@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" - integrity sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A= + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== dependencies: is-extglob "^2.1.1" @@ -3122,463 +3369,416 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -istanbul-api@^2.0.8: - version "2.1.0" - resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-2.1.0.tgz#37ab0c2c3e83065462f5254b94749d6157846c4e" - integrity sha512-+Ygg4t1StoiNlBGc6x0f8q/Bv26FbZqP/+jegzfNpU7Q8o+4ZRoJxJPhBkgE/UonpAjtxnE4zCZIyJX+MwLRMQ== - dependencies: - async "^2.6.1" - compare-versions "^3.2.1" - fileset "^2.0.3" - istanbul-lib-coverage "^2.0.3" - istanbul-lib-hook "^2.0.3" - istanbul-lib-instrument "^3.1.0" - istanbul-lib-report "^2.0.4" - istanbul-lib-source-maps "^3.0.2" - istanbul-reports "^2.1.0" - js-yaml "^3.12.0" - make-dir "^1.3.0" - minimatch "^3.0.4" - once "^1.4.0" - -istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#0b891e5ad42312c2b9488554f603795f9a2211ba" - integrity sha512-dKWuzRGCs4G+67VfW9pBFFz2Jpi4vSp/k7zBcJ888ofV5Mi1g5CUML5GvMvV6u9Cjybftu+E8Cgp+k0dI1E5lw== - -istanbul-lib-hook@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-2.0.3.tgz#e0e581e461c611be5d0e5ef31c5f0109759916fb" - integrity sha512-CLmEqwEhuCYtGcpNVJjLV1DQyVnIqavMLFHV/DP+np/g3qvdxu3gsPqYoJMXm15sN84xOlckFB3VNvRbf5yEgA== - dependencies: - append-transform "^1.0.0" +istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== -istanbul-lib-instrument@^3.0.0, istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.1.0.tgz#a2b5484a7d445f1f311e93190813fa56dfb62971" - integrity sha512-ooVllVGT38HIk8MxDj/OIHXSYvH+1tq/Vb38s8ixt9GoJadXska4WkGY+0wkmtYCZNYtaARniH/DixUGGLZ0uA== +istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" + integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== dependencies: - "@babel/generator" "^7.0.0" - "@babel/parser" "^7.0.0" - "@babel/template" "^7.0.0" - "@babel/traverse" "^7.0.0" - "@babel/types" "^7.0.0" - istanbul-lib-coverage "^2.0.3" - semver "^5.5.0" + "@babel/generator" "^7.4.0" + "@babel/parser" "^7.4.3" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.3" + "@babel/types" "^7.4.0" + istanbul-lib-coverage "^2.0.5" + semver "^6.0.0" istanbul-lib-report@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.4.tgz#bfd324ee0c04f59119cb4f07dab157d09f24d7e4" - integrity sha512-sOiLZLAWpA0+3b5w5/dq0cjm2rrNdAfHWaGhmn7XEFW6X++IV9Ohn+pnELAl9K3rfpaeBfbmH9JU5sejacdLeA== + version "2.0.8" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" + integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== dependencies: - istanbul-lib-coverage "^2.0.3" - make-dir "^1.3.0" - supports-color "^6.0.0" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + supports-color "^6.1.0" -istanbul-lib-source-maps@^3.0.1, istanbul-lib-source-maps@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.2.tgz#f1e817229a9146e8424a28e5d69ba220fda34156" - integrity sha512-JX4v0CiKTGp9fZPmoxpu9YEkPbEqCqBbO3403VabKjH+NRXo72HafD5UgnjTEqHL2SAjaZK1XDuDOkn6I5QVfQ== +istanbul-lib-source-maps@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== dependencies: debug "^4.1.1" - istanbul-lib-coverage "^2.0.3" - make-dir "^1.3.0" - rimraf "^2.6.2" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + rimraf "^2.6.3" source-map "^0.6.1" -istanbul-reports@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.1.0.tgz#87b8b55cd1901ba1748964c98ddd8900ce306d59" - integrity sha512-azQdSX+dtTtkQEfqq20ICxWi6eOHXyHIgMFw1VOOVi8iIPWeCWRgCyFh/CsBKIhcgskMI8ExXmU7rjXTRCIJ+A== +istanbul-reports@^2.1.1: + version "2.2.6" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.6.tgz#7b4f2660d82b29303a8fe6091f8ca4bf058da1af" + integrity sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA== dependencies: - handlebars "^4.0.11" + handlebars "^4.1.2" -jasmine-expect@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/jasmine-expect/-/jasmine-expect-4.0.1.tgz#48934ec5a3ba6b13dc0396eab8d4ddc336a0f7b3" - integrity sha512-tn+sdVx04MRHpW6ltIkEKANRO29C7AUdmkeupFrwbAawd8ICA/IZT9YsdIljGuxU+wLYoOJsaics6swnw2lO2g== +jasmine-expect@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/jasmine-expect/-/jasmine-expect-4.0.2.tgz#a35bc1dd9e03504cfd73a5b0282ce73d103fc5c5" + integrity sha512-VpHLwpBE1chVIhiJ7kJHBbdsm2GVvBli5bem4pGqkbiuvIW2mxghPNhYrKyoHBQKVmlq+xLUTGlrWMC/Ovn+2g== dependencies: add-matchers "0.6.2" -jest-changed-files@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.0.0.tgz#c02c09a8cc9ca93f513166bc773741bd39898ff7" - integrity sha512-nnuU510R9U+UX0WNb5XFEcsrMqriSiRLeO9KWDFgPrpToaQm60prfQYpxsXigdClpvNot5bekDY440x9dNGnsQ== +jest-changed-files@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.8.0.tgz#7e7eb21cf687587a85e50f3d249d1327e15b157b" + integrity sha512-qgANC1Yrivsq+UrLXsvJefBKVoCsKB0Hv+mBb6NMjjZ90wwxCDmU3hsCXBya30cH+LnPYjwgcU65i6yJ5Nfuug== dependencies: + "@jest/types" "^24.8.0" execa "^1.0.0" throat "^4.0.0" -jest-cli@24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.0.0.tgz#691fd4f7bce2574c1865db6844a43b56e60ce2a4" - integrity sha512-mElnFipLaGxo1SiQ1CLvuaz3eX07MJc4HcyKrApSJf8xSdY1/EwaHurKwu1g2cDiwIgY8uHj7UcF5OYbtiBOWg== +jest-cli@^24.7.1, jest-cli@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.8.0.tgz#b075ac914492ed114fa338ade7362a301693e989" + integrity sha512-+p6J00jSMPQ116ZLlHJJvdf8wbjNbZdeSX9ptfHX06/MSNaXmKihQzx5vQcw0q2G6JsdVkUIdWbOWtSnaYs3yA== dependencies: - ansi-escapes "^3.0.0" + "@jest/core" "^24.8.0" + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" chalk "^2.0.1" exit "^0.1.2" - glob "^7.1.2" - graceful-fs "^4.1.15" import-local "^2.0.0" is-ci "^2.0.0" - istanbul-api "^2.0.8" - istanbul-lib-coverage "^2.0.2" - istanbul-lib-instrument "^3.0.1" - istanbul-lib-source-maps "^3.0.1" - jest-changed-files "^24.0.0" - jest-config "^24.0.0" - jest-environment-jsdom "^24.0.0" - jest-get-type "^24.0.0" - jest-haste-map "^24.0.0" - jest-message-util "^24.0.0" - jest-regex-util "^24.0.0" - jest-resolve-dependencies "^24.0.0" - jest-runner "^24.0.0" - jest-runtime "^24.0.0" - jest-snapshot "^24.0.0" - jest-util "^24.0.0" - jest-validate "^24.0.0" - jest-watcher "^24.0.0" - jest-worker "^24.0.0" - micromatch "^3.1.10" - node-notifier "^5.2.1" - p-each-series "^1.0.0" - pirates "^4.0.0" + jest-config "^24.8.0" + jest-util "^24.8.0" + jest-validate "^24.8.0" prompts "^2.0.1" - realpath-native "^1.0.0" - rimraf "^2.5.4" - slash "^2.0.0" - string-length "^2.0.0" - strip-ansi "^5.0.0" - which "^1.2.12" + realpath-native "^1.1.0" yargs "^12.0.2" -jest-cli@^24.0.0: - version "24.1.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.1.0.tgz#f7cc98995f36e7210cce3cbb12974cbf60940843" - integrity sha512-U/iyWPwOI0T1CIxVLtk/2uviOTJ/OiSWJSe8qt6X1VkbbgP+nrtLJlmT9lPBe4lK78VNFJtrJ7pttcNv/s7yCw== - dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.1" - exit "^0.1.2" - glob "^7.1.2" - graceful-fs "^4.1.15" - import-local "^2.0.0" - is-ci "^2.0.0" - istanbul-api "^2.0.8" - istanbul-lib-coverage "^2.0.2" - istanbul-lib-instrument "^3.0.1" - istanbul-lib-source-maps "^3.0.1" - jest-changed-files "^24.0.0" - jest-config "^24.1.0" - jest-environment-jsdom "^24.0.0" - jest-get-type "^24.0.0" - jest-haste-map "^24.0.0" - jest-message-util "^24.0.0" - jest-regex-util "^24.0.0" - jest-resolve-dependencies "^24.1.0" - jest-runner "^24.1.0" - jest-runtime "^24.1.0" - jest-snapshot "^24.1.0" - jest-util "^24.0.0" - jest-validate "^24.0.0" - jest-watcher "^24.0.0" - jest-worker "^24.0.0" - micromatch "^3.1.10" - node-notifier "^5.2.1" - p-each-series "^1.0.0" - pirates "^4.0.0" - prompts "^2.0.1" - realpath-native "^1.0.0" - rimraf "^2.5.4" - slash "^2.0.0" - string-length "^2.0.0" - strip-ansi "^5.0.0" - which "^1.2.12" - yargs "^12.0.2" - -jest-config@^24.0.0, jest-config@^24.1.0: - version "24.1.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.1.0.tgz#6ea6881cfdd299bc86cc144ee36d937c97c3850c" - integrity sha512-FbbRzRqtFC6eGjG5VwsbW4E5dW3zqJKLWYiZWhB0/4E5fgsMw8GODLbGSrY5t17kKOtCWb/Z7nsIThRoDpuVyg== +jest-config@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.8.0.tgz#77db3d265a6f726294687cbbccc36f8a76ee0f4f" + integrity sha512-Czl3Nn2uEzVGsOeaewGWoDPD8GStxCpAe0zOYs2x2l0fZAgPbCr3uwUkgNKV3LwE13VXythM946cd5rdGkkBZw== dependencies: "@babel/core" "^7.1.0" - babel-jest "^24.1.0" + "@jest/test-sequencer" "^24.8.0" + "@jest/types" "^24.8.0" + babel-jest "^24.8.0" chalk "^2.0.1" glob "^7.1.1" - jest-environment-jsdom "^24.0.0" - jest-environment-node "^24.0.0" - jest-get-type "^24.0.0" - jest-jasmine2 "^24.1.0" - jest-regex-util "^24.0.0" - jest-resolve "^24.1.0" - jest-util "^24.0.0" - jest-validate "^24.0.0" + jest-environment-jsdom "^24.8.0" + jest-environment-node "^24.8.0" + jest-get-type "^24.8.0" + jest-jasmine2 "^24.8.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.8.0" + jest-util "^24.8.0" + jest-validate "^24.8.0" micromatch "^3.1.10" - pretty-format "^24.0.0" - realpath-native "^1.0.2" + pretty-format "^24.8.0" + realpath-native "^1.1.0" -jest-diff@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.0.0.tgz#a3e5f573dbac482f7d9513ac9cfa21644d3d6b34" - integrity sha512-XY5wMpRaTsuMoU+1/B2zQSKQ9RdE9gsLkGydx3nvApeyPijLA8GtEvIcPwISRCer+VDf9W1mStTYYq6fPt8ryA== +jest-diff@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.8.0.tgz#146435e7d1e3ffdf293d53ff97e193f1d1546172" + integrity sha512-wxetCEl49zUpJ/bvUmIFjd/o52J+yWcoc5ZyPq4/W1LUKGEhRYDIbP1KcF6t+PvqNrGAFk4/JhtxDq/Nnzs66g== dependencies: chalk "^2.0.1" - diff-sequences "^24.0.0" - jest-get-type "^24.0.0" - pretty-format "^24.0.0" + diff-sequences "^24.3.0" + jest-get-type "^24.8.0" + pretty-format "^24.8.0" -jest-docblock@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.0.0.tgz#54d77a188743e37f62181a91a01eb9222289f94e" - integrity sha512-KfAKZ4SN7CFOZpWg4i7g7MSlY0M+mq7K0aMqENaG2vHuhC9fc3vkpU/iNN9sOus7v3h3Y48uEjqz3+Gdn2iptA== +jest-docblock@^24.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.3.0.tgz#b9c32dac70f72e4464520d2ba4aec02ab14db5dd" + integrity sha512-nlANmF9Yq1dufhFlKG9rasfQlrY7wINJbo3q01tu56Jv5eBU5jirylhF2O5ZBnLxzOVBGRDz/9NAwNyBtG4Nyg== dependencies: detect-newline "^2.1.0" -jest-each@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.0.0.tgz#10987a06b21c7ffbfb7706c89d24c52ed864be55" - integrity sha512-gFcbY4Cu55yxExXMkjrnLXov3bWO3dbPAW7HXb31h/DNWdNc/6X8MtxGff8nh3/MjkF9DpVqnj0KsPKuPK0cpA== +jest-each@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.8.0.tgz#a05fd2bf94ddc0b1da66c6d13ec2457f35e52775" + integrity sha512-NrwK9gaL5+XgrgoCsd9svsoWdVkK4gnvyhcpzd6m487tXHqIdYeykgq3MKI1u4I+5Zf0tofr70at9dWJDeb+BA== dependencies: + "@jest/types" "^24.8.0" chalk "^2.0.1" - jest-get-type "^24.0.0" - jest-util "^24.0.0" - pretty-format "^24.0.0" - -jest-environment-jsdom@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.0.0.tgz#5affa0654d6e44cd798003daa1a8701dbd6e4d11" - integrity sha512-1YNp7xtxajTRaxbylDc2pWvFnfDTH5BJJGyVzyGAKNt/lEULohwEV9zFqTgG4bXRcq7xzdd+sGFws+LxThXXOw== - dependencies: - jest-mock "^24.0.0" - jest-util "^24.0.0" + jest-get-type "^24.8.0" + jest-util "^24.8.0" + pretty-format "^24.8.0" + +jest-environment-jsdom@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.8.0.tgz#300f6949a146cabe1c9357ad9e9ecf9f43f38857" + integrity sha512-qbvgLmR7PpwjoFjM/sbuqHJt/NCkviuq9vus9NBn/76hhSidO+Z6Bn9tU8friecegbJL8gzZQEMZBQlFWDCwAQ== + dependencies: + "@jest/environment" "^24.8.0" + "@jest/fake-timers" "^24.8.0" + "@jest/types" "^24.8.0" + jest-mock "^24.8.0" + jest-util "^24.8.0" jsdom "^11.5.1" -jest-environment-node@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.0.0.tgz#330948980656ed8773ce2e04eb597ed91e3c7190" - integrity sha512-62fOFcaEdU0VLaq8JL90TqwI7hLn0cOKOl8vY2n477vRkCJRojiRRtJVRzzCcgFvs6gqU97DNqX5R0BrBP6Rxg== +jest-environment-node@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.8.0.tgz#d3f726ba8bc53087a60e7a84ca08883a4c892231" + integrity sha512-vIGUEScd1cdDgR6sqn2M08sJTRLQp6Dk/eIkCeO4PFHxZMOgy+uYLPMC4ix3PEfM5Au/x3uQ/5Tl0DpXXZsJ/Q== dependencies: - jest-mock "^24.0.0" - jest-util "^24.0.0" + "@jest/environment" "^24.8.0" + "@jest/fake-timers" "^24.8.0" + "@jest/types" "^24.8.0" + jest-mock "^24.8.0" + jest-util "^24.8.0" -jest-ex@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/jest-ex/-/jest-ex-6.0.0.tgz#e7de9c8e07e3a4854e090fd7b4fbe0e149ad2cd6" - integrity sha512-ItdS7cU3LLTxNvI2dnbbJE/L+Yxp/bwDoMyt042kj6iGsOiE4JrnfQwM9IQang35Y7YbJ/sITbVr84k/xHWaKA== +jest-ex@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jest-ex/-/jest-ex-6.1.0.tgz#966f7afb19969593fc7ea92d7df2870ea8e3814e" + integrity sha512-5YMxOXRrRUge1OmpzsCMAVnfumqF1Kq+lBA/yQNIEHYDsUFasI6iYdWgfG2YZbtsYBwTIYbGswUePEFztYSaog== dependencies: - "@babel/core" "7.2.2" - "@babel/plugin-transform-runtime" "7.2.0" - "@babel/preset-env" "^7.3.1" + "@babel/core" "7.4.3" + "@babel/plugin-transform-runtime" "7.4.3" + "@babel/preset-env" "^7.4.3" glob "7.1.3" html-loader "0.5.5" - jest-cli "^24.0.0" - yargs "12.0.5" + jest-cli "^24.7.1" + yargs "13.2.2" -jest-get-type@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.0.0.tgz#36e72930b78e33da59a4f63d44d332188278940b" - integrity sha512-z6/Eyf6s9ZDGz7eOvl+fzpuJmN9i0KyTt1no37/dHu8galssxz5ZEgnc1KaV8R31q1khxyhB4ui/X5ZjjPk77w== +jest-get-type@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.8.0.tgz#a7440de30b651f5a70ea3ed7ff073a32dfe646fc" + integrity sha512-RR4fo8jEmMD9zSz2nLbs2j0zvPpk/KCEz3a62jJWbd2ayNo0cb+KFRxPHVhE4ZmgGJEQp0fosmNz84IfqM8cMQ== -jest-haste-map@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.0.0.tgz#e9ef51b2c9257384b4d6beb83bd48c65b37b5e6e" - integrity sha512-CcViJyUo41IQqttLxXVdI41YErkzBKbE6cS6dRAploCeutePYfUimWd3C9rQEWhX0YBOQzvNsC0O9nYxK2nnxQ== +jest-haste-map@^24.8.0: + version "24.8.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.8.1.tgz#f39cc1d2b1d907e014165b4bd5a957afcb992982" + integrity sha512-SwaxMGVdAZk3ernAx2Uv2sorA7jm3Kx+lR0grp6rMmnY06Kn/urtKx1LPN2mGTea4fCT38impYT28FfcLUhX0g== dependencies: + "@jest/types" "^24.8.0" + anymatch "^2.0.0" fb-watchman "^2.0.0" graceful-fs "^4.1.15" invariant "^2.2.4" - jest-serializer "^24.0.0" - jest-util "^24.0.0" - jest-worker "^24.0.0" + jest-serializer "^24.4.0" + jest-util "^24.8.0" + jest-worker "^24.6.0" micromatch "^3.1.10" - sane "^3.0.0" + sane "^4.0.3" + walker "^1.0.7" + optionalDependencies: + fsevents "^1.2.7" -jest-jasmine2@^24.1.0: - version "24.1.0" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.1.0.tgz#8377324b967037c440f0a549ee0bbd9912055db6" - integrity sha512-H+o76SdSNyCh9fM5K8upK45YTo/DiFx5w2YAzblQebSQmukDcoVBVeXynyr7DDnxh+0NTHYRCLwJVf3tC518wg== +jest-jasmine2@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.8.0.tgz#a9c7e14c83dd77d8b15e820549ce8987cc8cd898" + integrity sha512-cEky88npEE5LKd5jPpTdDCLvKkdyklnaRycBXL6GNmpxe41F0WN44+i7lpQKa/hcbXaQ+rc9RMaM4dsebrYong== dependencies: "@babel/traverse" "^7.1.0" + "@jest/environment" "^24.8.0" + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" chalk "^2.0.1" co "^4.6.0" - expect "^24.1.0" + expect "^24.8.0" is-generator-fn "^2.0.0" - jest-each "^24.0.0" - jest-matcher-utils "^24.0.0" - jest-message-util "^24.0.0" - jest-snapshot "^24.1.0" - jest-util "^24.0.0" - pretty-format "^24.0.0" + jest-each "^24.8.0" + jest-matcher-utils "^24.8.0" + jest-message-util "^24.8.0" + jest-runtime "^24.8.0" + jest-snapshot "^24.8.0" + jest-util "^24.8.0" + pretty-format "^24.8.0" throat "^4.0.0" -jest-leak-detector@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.0.0.tgz#78280119fd05ee98317daee62cddb3aa537a31c6" - integrity sha512-ZYHJYFeibxfsDSKowjDP332pStuiFT2xfc5R67Rjm/l+HFJWJgNIOCOlQGeXLCtyUn3A23+VVDdiCcnB6dTTrg== +jest-leak-detector@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.8.0.tgz#c0086384e1f650c2d8348095df769f29b48e6980" + integrity sha512-cG0yRSK8A831LN8lIHxI3AblB40uhv0z+SsQdW3GoMMVcK+sJwrIIyax5tu3eHHNJ8Fu6IMDpnLda2jhn2pD/g== dependencies: - pretty-format "^24.0.0" + pretty-format "^24.8.0" -jest-matcher-utils@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.0.0.tgz#fc9c41cfc49b2c3ec14e576f53d519c37729d579" - integrity sha512-LQTDmO+aWRz1Tf9HJg+HlPHhDh1E1c65kVwRFo5mwCVp5aQDzlkz4+vCvXhOKFjitV2f0kMdHxnODrXVoi+rlA== +jest-matcher-utils@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.8.0.tgz#2bce42204c9af12bde46f83dc839efe8be832495" + integrity sha512-lex1yASY51FvUuHgm0GOVj7DCYEouWSlIYmCW7APSqB9v8mXmKSn5+sWVF0MhuASG0bnYY106/49JU1FZNl5hw== dependencies: chalk "^2.0.1" - jest-diff "^24.0.0" - jest-get-type "^24.0.0" - pretty-format "^24.0.0" + jest-diff "^24.8.0" + jest-get-type "^24.8.0" + pretty-format "^24.8.0" -jest-message-util@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.0.0.tgz#a07a141433b2c992dbaec68d4cbfe470ba289619" - integrity sha512-J9ROJIwz/IeC+eV1XSwnRK4oAwPuhmxEyYx1+K5UI+pIYwFZDSrfZaiWTdq0d2xYFw4Xiu+0KQWsdsQpgJMf3Q== +jest-message-util@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.8.0.tgz#0d6891e72a4beacc0292b638685df42e28d6218b" + integrity sha512-p2k71rf/b6ns8btdB0uVdljWo9h0ovpnEe05ZKWceQGfXYr4KkzgKo3PBi8wdnd9OtNh46VpNIJynUn/3MKm1g== dependencies: "@babel/code-frame" "^7.0.0" + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" + "@types/stack-utils" "^1.0.1" chalk "^2.0.1" micromatch "^3.1.10" slash "^2.0.0" stack-utils "^1.0.1" -jest-mock@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.0.0.tgz#9a4b53e01d66a0e780f7d857462d063e024c617d" - integrity sha512-sQp0Hu5fcf5NZEh1U9eIW2qD0BwJZjb63Yqd98PQJFvf/zzUTBoUAwv/Dc/HFeNHIw1f3hl/48vNn+j3STaI7A== +jest-mock@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.8.0.tgz#2f9d14d37699e863f1febf4e4d5a33b7fdbbde56" + integrity sha512-6kWugwjGjJw+ZkK4mDa0Df3sDlUTsV47MSrT0nGQ0RBWJbpODDQ8MHDVtGtUYBne3IwZUhtB7elxHspU79WH3A== + dependencies: + "@jest/types" "^24.8.0" + +jest-pnp-resolver@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" + integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== -jest-regex-util@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.0.0.tgz#4feee8ec4a358f5bee0a654e94eb26163cb9089a" - integrity sha512-Jv/uOTCuC+PY7WpJl2mpoI+WbY2ut73qwwO9ByJJNwOCwr1qWhEW2Lyi2S9ZewUdJqeVpEBisdEVZSI+Zxo58Q== +jest-regex-util@^24.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.3.0.tgz#d5a65f60be1ae3e310d5214a0307581995227b36" + integrity sha512-tXQR1NEOyGlfylyEjg1ImtScwMq8Oh3iJbGTjN7p0J23EuVX1MA8rwU69K4sLbCmwzgCUbVkm0FkSF9TdzOhtg== -jest-resolve-dependencies@^24.0.0, jest-resolve-dependencies@^24.1.0: - version "24.1.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.1.0.tgz#78f738a2ec59ff4d00751d9da56f176e3f589f6c" - integrity sha512-2VwPsjd3kRPu7qe2cpytAgowCObk5AKeizfXuuiwgm1a9sijJDZe8Kh1sFj6FKvSaNEfCPlBVkZEJa2482m/Uw== +jest-resolve-dependencies@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.8.0.tgz#19eec3241f2045d3f990dba331d0d7526acff8e0" + integrity sha512-hyK1qfIf/krV+fSNyhyJeq3elVMhK9Eijlwy+j5jqmZ9QsxwKBiP6qukQxaHtK8k6zql/KYWwCTQ+fDGTIJauw== dependencies: - jest-regex-util "^24.0.0" - jest-snapshot "^24.1.0" + "@jest/types" "^24.8.0" + jest-regex-util "^24.3.0" + jest-snapshot "^24.8.0" -jest-resolve@^24.1.0: - version "24.1.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.1.0.tgz#42ff0169b0ea47bfdbd0c52a0067ca7d022c7688" - integrity sha512-TPiAIVp3TG6zAxH28u/6eogbwrvZjBMWroSLBDkwkHKrqxB/RIdwkWDye4uqPlZIXWIaHtifY3L0/eO5Z0f2wg== +jest-resolve@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.8.0.tgz#84b8e5408c1f6a11539793e2b5feb1b6e722439f" + integrity sha512-+hjSzi1PoRvnuOICoYd5V/KpIQmkAsfjFO71458hQ2Whi/yf1GDeBOFj8Gxw4LrApHsVJvn5fmjcPdmoUHaVKw== dependencies: + "@jest/types" "^24.8.0" browser-resolve "^1.11.3" chalk "^2.0.1" - realpath-native "^1.0.0" - -jest-runner@^24.0.0, jest-runner@^24.1.0: - version "24.1.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.1.0.tgz#3686a2bb89ce62800da23d7fdc3da2c32792943b" - integrity sha512-CDGOkT3AIFl16BLL/OdbtYgYvbAprwJ+ExKuLZmGSCSldwsuU2dEGauqkpvd9nphVdAnJUcP12e/EIlnTX0QXg== - dependencies: + jest-pnp-resolver "^1.2.1" + realpath-native "^1.1.0" + +jest-runner@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.8.0.tgz#4f9ae07b767db27b740d7deffad0cf67ccb4c5bb" + integrity sha512-utFqC5BaA3JmznbissSs95X1ZF+d+4WuOWwpM9+Ak356YtMhHE/GXUondZdcyAAOTBEsRGAgH/0TwLzfI9h7ow== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.8.0" + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" chalk "^2.4.2" exit "^0.1.2" graceful-fs "^4.1.15" - jest-config "^24.1.0" - jest-docblock "^24.0.0" - jest-haste-map "^24.0.0" - jest-jasmine2 "^24.1.0" - jest-leak-detector "^24.0.0" - jest-message-util "^24.0.0" - jest-runtime "^24.1.0" - jest-util "^24.0.0" - jest-worker "^24.0.0" + jest-config "^24.8.0" + jest-docblock "^24.3.0" + jest-haste-map "^24.8.0" + jest-jasmine2 "^24.8.0" + jest-leak-detector "^24.8.0" + jest-message-util "^24.8.0" + jest-resolve "^24.8.0" + jest-runtime "^24.8.0" + jest-util "^24.8.0" + jest-worker "^24.6.0" source-map-support "^0.5.6" throat "^4.0.0" -jest-runtime@^24.0.0, jest-runtime@^24.1.0: - version "24.1.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.1.0.tgz#7c157a2e776609e8cf552f956a5a19ec9c985214" - integrity sha512-59/BY6OCuTXxGeDhEMU7+N33dpMQyXq7MLK07cNSIY/QYt2QZgJ7Tjx+rykBI0skAoigFl0A5tmT8UdwX92YuQ== - dependencies: - "@babel/core" "^7.1.0" - babel-plugin-istanbul "^5.1.0" +jest-runtime@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.8.0.tgz#05f94d5b05c21f6dc54e427cd2e4980923350620" + integrity sha512-Mq0aIXhvO/3bX44ccT+czU1/57IgOMyy80oM0XR/nyD5zgBcesF84BPabZi39pJVA6UXw+fY2Q1N+4BiVUBWOA== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.8.0" + "@jest/source-map" "^24.3.0" + "@jest/transform" "^24.8.0" + "@jest/types" "^24.8.0" + "@types/yargs" "^12.0.2" chalk "^2.0.1" - convert-source-map "^1.4.0" exit "^0.1.2" - fast-json-stable-stringify "^2.0.0" glob "^7.1.3" graceful-fs "^4.1.15" - jest-config "^24.1.0" - jest-haste-map "^24.0.0" - jest-message-util "^24.0.0" - jest-regex-util "^24.0.0" - jest-resolve "^24.1.0" - jest-snapshot "^24.1.0" - jest-util "^24.0.0" - jest-validate "^24.0.0" - micromatch "^3.1.10" - realpath-native "^1.0.0" + jest-config "^24.8.0" + jest-haste-map "^24.8.0" + jest-message-util "^24.8.0" + jest-mock "^24.8.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.8.0" + jest-snapshot "^24.8.0" + jest-util "^24.8.0" + jest-validate "^24.8.0" + realpath-native "^1.1.0" slash "^2.0.0" strip-bom "^3.0.0" - write-file-atomic "2.4.1" yargs "^12.0.2" -jest-serializer@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.0.0.tgz#522c44a332cdd194d8c0531eb06a1ee5afb4256b" - integrity sha512-9FKxQyrFgHtx3ozU+1a8v938ILBE7S8Ko3uiAVjT8Yfi2o91j/fj81jacCQZ/Ihjiff/VsUCXVgQ+iF1XdImOw== +jest-serializer@^24.4.0: + version "24.4.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.4.0.tgz#f70c5918c8ea9235ccb1276d232e459080588db3" + integrity sha512-k//0DtglVstc1fv+GY/VHDIjrtNjdYvYjMlbLUed4kxrE92sIUewOi5Hj3vrpB8CXfkJntRPDRjCrCvUhBdL8Q== -jest-snapshot@^24.0.0, jest-snapshot@^24.1.0: - version "24.1.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.1.0.tgz#85e22f810357aa5994ab61f236617dc2205f2f5b" - integrity sha512-th6TDfFqEmXvuViacU1ikD7xFb7lQsPn2rJl7OEmnfIVpnrx3QNY2t3PE88meeg0u/mQ0nkyvmC05PBqO4USFA== +jest-snapshot@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.8.0.tgz#3bec6a59da2ff7bc7d097a853fb67f9d415cb7c6" + integrity sha512-5ehtWoc8oU9/cAPe6fez6QofVJLBKyqkY2+TlKTOf0VllBB/mqUNdARdcjlZrs9F1Cv+/HKoCS/BknT0+tmfPg== dependencies: "@babel/types" "^7.0.0" + "@jest/types" "^24.8.0" chalk "^2.0.1" - jest-diff "^24.0.0" - jest-matcher-utils "^24.0.0" - jest-message-util "^24.0.0" - jest-resolve "^24.1.0" + expect "^24.8.0" + jest-diff "^24.8.0" + jest-matcher-utils "^24.8.0" + jest-message-util "^24.8.0" + jest-resolve "^24.8.0" mkdirp "^0.5.1" natural-compare "^1.4.0" - pretty-format "^24.0.0" + pretty-format "^24.8.0" semver "^5.5.0" -jest-util@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.0.0.tgz#fd38fcafd6dedbd0af2944d7a227c0d91b68f7d6" - integrity sha512-QxsALc4wguYS7cfjdQSOr5HTkmjzkHgmZvIDkcmPfl1ib8PNV8QUWLwbKefCudWS0PRKioV+VbQ0oCUPC691fQ== +jest-util@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.8.0.tgz#41f0e945da11df44cc76d64ffb915d0716f46cd1" + integrity sha512-DYZeE+XyAnbNt0BG1OQqKy/4GVLPtzwGx5tsnDrFcax36rVE3lTA5fbvgmbVPUZf9w77AJ8otqR4VBbfFJkUZA== dependencies: + "@jest/console" "^24.7.1" + "@jest/fake-timers" "^24.8.0" + "@jest/source-map" "^24.3.0" + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" callsites "^3.0.0" chalk "^2.0.1" graceful-fs "^4.1.15" is-ci "^2.0.0" - jest-message-util "^24.0.0" mkdirp "^0.5.1" slash "^2.0.0" source-map "^0.6.0" -jest-validate@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.0.0.tgz#aa8571a46983a6538328fef20406b4a496b6c020" - integrity sha512-vMrKrTOP4BBFIeOWsjpsDgVXATxCspC9S1gqvbJ3Tnn/b9ACsJmteYeVx9830UMV28Cob1RX55x96Qq3Tfad4g== +jest-validate@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.8.0.tgz#624c41533e6dfe356ffadc6e2423a35c2d3b4849" + integrity sha512-+/N7VOEMW1Vzsrk3UWBDYTExTPwf68tavEPKDnJzrC6UlHtUDU/fuEdXqFoHzv9XnQ+zW6X3qMZhJ3YexfeLDA== dependencies: + "@jest/types" "^24.8.0" camelcase "^5.0.0" chalk "^2.0.1" - jest-get-type "^24.0.0" + jest-get-type "^24.8.0" leven "^2.1.0" - pretty-format "^24.0.0" + pretty-format "^24.8.0" -jest-watcher@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.0.0.tgz#20d44244d10b0b7312410aefd256c1c1eef68890" - integrity sha512-GxkW2QrZ4YxmW1GUWER05McjVDunBlKMFfExu+VsGmXJmpej1saTEKvONdx5RJBlVdpPI5x6E3+EDQSIGgl53g== +jest-watcher@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.8.0.tgz#58d49915ceddd2de85e238f6213cef1c93715de4" + integrity sha512-SBjwHt5NedQoVu54M5GEx7cl7IGEFFznvd/HNT8ier7cCAx/Qgu9ZMlaTQkvK22G1YOpcWBLQPFSImmxdn3DAw== dependencies: + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" + "@types/yargs" "^12.0.9" ansi-escapes "^3.0.0" chalk "^2.0.1" - jest-util "^24.0.0" + jest-util "^24.8.0" string-length "^2.0.0" -jest-worker@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.0.0.tgz#3d3483b077bf04f412f47654a27bba7e947f8b6d" - integrity sha512-s64/OThpfQvoCeHG963MiEZOAAxu8kHsaL/rCMF7lpdzo7vgF0CtPml9hfguOMgykgH/eOm4jFP4ibfHLruytg== +jest-worker@^24.6.0: + version "24.6.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.6.0.tgz#7f81ceae34b7cde0c9827a6980c35b7cdc0161b3" + integrity sha512-jDwgW5W9qGNvpI1tNnvajh0a5IE/PuGLFmHk6aR/BZFz8tSgGw17GsDPXAJ6p91IvYDjOw8GpFbvvZGAK+DPQQ== dependencies: merge-stream "^1.0.1" supports-color "^6.1.0" -jimple@1.5.0: +jimple@1.5.0, jimple@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/jimple/-/jimple-1.5.0.tgz#a964bf4862bb2b488ce3830266a4dd8e8ef1213b" integrity sha512-lV8T+LMLvq4qYomJvXtIoL+rwT3tQ2fjq6Bv+vgO0zEtfIw6oXlOmSbHusq5TgR5IKP2avouqWVpGFjbsWlR2Q== @@ -3598,10 +3798,10 @@ js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@^3.11.0, js-yaml@^3.12.0: - version "3.12.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600" - integrity sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA== +js-yaml@^3.11.0, js-yaml@^3.13.0: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -3704,7 +3904,7 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json2xml@^0.1.2: +json2xml@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/json2xml/-/json2xml-0.1.3.tgz#9ae7c220bedd7c66a668e26f7ac182f6704eca21" integrity sha1-mufCIL7dfGamaOJvesGC9nBOyiE= @@ -3778,10 +3978,10 @@ klaw@^1.0.0: optionalDependencies: graceful-fs "^4.1.9" -kleur@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.1.tgz#4f5b313f5fa315432a400f19a24db78d451ede62" - integrity sha512-P3kRv+B+Ra070ng2VKQqW4qW7gd/v3iD8sy/zOdcYRsfiD+QBokQNOps/AfP6Hr48cBhIIBFWckB9aO+IZhrWg== +kleur@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== lcid@^2.0.0: version "2.0.0" @@ -3795,21 +3995,21 @@ lcov-parse@^0.0.10: resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-0.0.10.tgz#1b0b8ff9ac9c7889250582b70b71315d9da6d9a3" integrity sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM= -leasot@7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/leasot/-/leasot-7.2.0.tgz#88bb54da2db2af58950a2082c5c610449963ce98" - integrity sha512-JUyKDXnn6ESFs/n3s5MWhssO5P1zaFW30NTtJPxGzAKzW02Wi9G7JiVU5weSKIXvJQ1MGm8iRy9JzFVd5VrEwA== +leasot@^7.4.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/leasot/-/leasot-7.4.0.tgz#5df74267af8ca67833b435e92576e4c5a1a39aaf" + integrity sha512-l29dwWEBiaXwDTvUyKAt5YeZXJrETyptxsTYsjaY5XaU1Q6lMM/XsF28xnWgGRAfXYJsx3kTlctEflsyI1Dfng== dependencies: - async "^2.6.1" + async "^3.0.1" chalk "^2.4.2" - commander "^2.19.0" + commander "^2.20.0" eol "^0.9.1" - get-stdin "^6.0.0" - globby "^9.0.0" - json2xml "^0.1.2" + get-stdin "^7.0.0" + globby "^9.2.0" + json2xml "^0.1.3" lodash "^4.17.11" - log-symbols "^2.2.0" - strip-ansi "^5.0.0" + log-symbols "^3.0.0" + strip-ansi "^5.2.0" text-table "^0.2.0" left-pad@^1.3.0: @@ -3940,7 +4140,7 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash@^4.1.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0: +lodash@^4.1.0, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.2.0: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== @@ -3950,12 +4150,12 @@ log-driver@^1.2.7: resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8" integrity sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg== -log-symbols@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" - integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== +log-symbols@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" + integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== dependencies: - chalk "^2.0.1" + chalk "^2.4.2" loose-envify@^1.0.0: version "1.4.0" @@ -3969,12 +4169,13 @@ lower-case@^1.1.1: resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= -make-dir@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" - integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== +make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== dependencies: - pify "^3.0.0" + pify "^4.0.1" + semver "^5.6.0" makeerror@1.0.x: version "1.0.11" @@ -4013,12 +4214,12 @@ media-typer@0.3.0: integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= mem@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.1.0.tgz#aeb9be2d21f47e78af29e4ac5978e8afa2ca5b8a" - integrity sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg== + version "4.3.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" + integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== dependencies: map-age-cleaner "^0.1.1" - mimic-fn "^1.0.0" + mimic-fn "^2.0.0" p-is-promise "^2.0.0" merge-descriptors@1.0.1: @@ -4038,11 +4239,6 @@ merge2@^1.2.3: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5" integrity sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA== -merge@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" - integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== - methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -4067,39 +4263,39 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -"mime-db@>= 1.36.0 < 2": - version "1.38.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" - integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== +mime-db@1.40.0, "mime-db@>= 1.40.0 < 2": + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== -mime-db@~1.37.0: - version "1.37.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" - integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== - -mime-types@^2.1.12, mime-types@~2.1.18, mime-types@~2.1.19: - version "2.1.21" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" - integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== dependencies: - mime-db "~1.37.0" + mime-db "1.40.0" -mime@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" - integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.0.tgz#e051fd881358585f3279df333fe694da0bcffdd6" - integrity sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w== +mime@^2.4.4: + version "2.4.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" + integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== -minimatch@^3.0.3, minimatch@^3.0.4: +mimic-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -4121,7 +4317,7 @@ minimist@~0.0.1: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= -minipass@^2.2.1, minipass@^2.3.4: +minipass@^2.2.1, minipass@^2.3.5: version "2.3.5" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== @@ -4129,7 +4325,7 @@ minipass@^2.2.1, minipass@^2.3.4: safe-buffer "^5.1.2" yallist "^3.0.0" -minizlib@^1.1.1: +minizlib@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== @@ -4156,12 +4352,17 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@^2.1.1: +ms@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -multer@1.4.1: +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +multer@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.1.tgz#24b12a416a22fec2ade810539184bf138720159e" integrity sha512-zzOLNRxzszwd+61JFuAo0fxdQfvku12aNJgnla0AQ+hHxFmfc/B7jBVuPr5Rmvu46Jze/iJrFpSOsD7afO8SDw== @@ -4180,10 +4381,10 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -nan@^2.9.2: - version "2.12.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" - integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== nanomatch@^1.2.9: version "1.2.13" @@ -4208,18 +4409,23 @@ natural-compare@^1.4.0: integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= needle@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e" - integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA== + version "2.4.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" + integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== dependencies: - debug "^2.1.2" + debug "^3.2.6" iconv-lite "^0.4.4" sax "^1.2.4" -negotiator@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" - integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk= +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +neo-async@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" + integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== nice-try@^1.0.4: version "1.0.5" @@ -4233,10 +4439,10 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" -node-fetch@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5" - integrity sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA== +node-fetch@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== node-int64@^0.4.0: version "0.4.0" @@ -4259,10 +4465,10 @@ node-notifier@^5.2.1: shellwords "^0.1.1" which "^1.3.0" -node-pre-gyp@^0.10.0: - version "0.10.3" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" - integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A== +node-pre-gyp@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" + integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== dependencies: detect-libc "^1.0.2" mkdirp "^0.5.1" @@ -4275,10 +4481,10 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" -node-releases@^1.1.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.7.tgz#b09a10394d0ed8f7778f72bb861dde68b146303b" - integrity sha512-bKdrwaqJUPHqlCzDD7so/R+Nk0jGv9a11ZhLrD9f6i947qGLrGAhU3OxRENa19QQmwzGy/g6zCDEuLGDO8HPvA== +node-releases@^1.1.23: + version "1.1.23" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.23.tgz#de7409f72de044a2fa59c097f436ba89c39997f0" + integrity sha512-uq1iL79YjfYC0WXoHbC/z28q/9pOl8kSHaXdWmAAc8No+bDwqkZbzIJz55g/MUsPgSGm9LZ7QSUbzTcH5tz47w== dependencies: semver "^5.3.0" @@ -4313,9 +4519,9 @@ npm-bundled@^1.0.1: integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== npm-packlist@^1.1.6: - version "1.3.0" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.3.0.tgz#7f01e8e44408341379ca98cfd756e7b29bd2626c" - integrity sha512-qPBc6CnxEzpOcc4bjoIBJbYdy0D/LFFPUdxvfwor4/w3vxeE0h6TiOVurCEPpQ6trjN77u/ShyfeJGsbAfB3dA== + version "1.4.1" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc" + integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw== dependencies: ignore-walk "^3.0.1" npm-bundled "^1.0.1" @@ -4355,16 +4561,16 @@ number-is-nan@^1.0.0: integrity sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ== nwsapi@^2.0.7: - version "2.1.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.0.tgz#781065940aed90d9bb01ca5d0ce0fcf81c32712f" - integrity sha512-ZG3bLAvdHmhIjaQ/Db1qvBxsGvFMLIRpQszyqbg31VJ53UP++uZX1/gf3Ut96pdwN9AuDwlMqIYLm0UPCdUeHg== + version "2.1.4" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.4.tgz#e006a878db23636f8e8a67d33ca0e4edf61a842f" + integrity sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw== oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -4379,9 +4585,9 @@ object-copy@^0.1.0: kind-of "^3.0.3" object-keys@^1.0.11, object-keys@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" - integrity sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag== + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== object-visit@^1.0.0: version "1.0.1" @@ -4432,10 +4638,10 @@ on-finished@^2.3.0, on-finished@~2.3.0: dependencies: ee-first "1.1.1" -on-headers@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" - integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c= +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" @@ -4476,7 +4682,7 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^3.0.0: +os-locale@^3.0.0, os-locale@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== @@ -4516,9 +4722,9 @@ p-finally@^1.0.0: integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= p-is-promise@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.0.0.tgz#7554e3d572109a87e1f3f53f6a7d85d1b194f4c5" - integrity sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg== + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" + integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== p-limit@^1.1.0: version "1.3.0" @@ -4528,9 +4734,9 @@ p-limit@^1.1.0: p-try "^1.0.0" p-limit@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.1.0.tgz#1d5a0d20fb12707c758a655f6bbc4386b5930d68" - integrity sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g== + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" + integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== dependencies: p-try "^2.0.0" @@ -4559,9 +4765,9 @@ p-try@^1.0.0: integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= p-try@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" - integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== param-case@2.1.x: version "2.1.1" @@ -4571,9 +4777,9 @@ param-case@2.1.x: no-case "^2.2.0" parent-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.0.tgz#df250bdc5391f4a085fb589dad761f5ad6b865b5" - integrity sha512-8Mf5juOMmiE4FcmzYc4IaiS9L3+9paz2KOiXzkRviCP6aDmN49Hz6EMWz0lGNp9pX80GvvAuLADtyGfW/Em3TA== + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== dependencies: callsites "^3.0.0" @@ -4609,10 +4815,10 @@ parse5@^3.0.1: dependencies: "@types/node" "*" -parseurl@~1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" - integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M= +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== pascalcase@^0.1.1: version "0.1.1" @@ -4688,10 +4894,10 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== -pirates@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.0.tgz#850b18781b4ac6ec58a43c9ed9ec5fe6796addbd" - integrity sha512-8t5BsXy1LUIjn3WWOlOuFDuKswhQb/tkak641lvBgmPOBUQHXveORtlMCp6OdPV1dtuTaEahKA8VNz6uLfKBtA== +pirates@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" + integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== dependencies: node-modules-regexp "^1.0.0" @@ -4724,13 +4930,15 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -pretty-format@^24.0.0: - version "24.0.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.0.0.tgz#cb6599fd73ac088e37ed682f61291e4678f48591" - integrity sha512-LszZaKG665djUcqg5ZQq+XzezHLKrxsA86ZABTozp+oNhkdqa+tG2dX4qa6ERl5c/sRDrAa3lHmwnvKoP+OG/g== +pretty-format@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2" + integrity sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw== dependencies: + "@jest/types" "^24.8.0" ansi-regex "^4.0.0" ansi-styles "^3.2.0" + react-is "^16.8.4" private@^0.1.6, private@~0.1.5: version "0.1.8" @@ -4738,9 +4946,9 @@ private@^0.1.6, private@~0.1.5: integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== process-nextick-args@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" - integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== progress@^2.0.0: version "2.0.3" @@ -4748,25 +4956,25 @@ progress@^2.0.0: integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== prompts@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.0.1.tgz#201b3718b4276fb407f037db48c0029d6465245c" - integrity sha512-8lnEOSIGQbgbnO47+13S+H204L8ISogGulyi0/NNEFAQ9D1VMNTrJ9SBX2Ra03V4iPn/zt36HQMndRYkaPoWiQ== + version "2.1.0" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.1.0.tgz#bf90bc71f6065d255ea2bdc0fe6520485c1b45db" + integrity sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg== dependencies: - kleur "^3.0.0" + kleur "^3.0.2" sisteransi "^1.0.0" -proxy-addr@~2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" - integrity sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA== +proxy-addr@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" + integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== dependencies: forwarded "~0.1.2" - ipaddr.js "1.8.0" + ipaddr.js "1.9.0" psl@^1.1.24, psl@^1.1.28: - version "1.1.31" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" - integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw== + version "1.1.33" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.33.tgz#5533d9384ca7aab86425198e10e8053ebfeab661" + integrity sha512-LTDP2uSrsc7XCb5lO7A8BI1qYxRe/8EqlRvMeEl6rsnYAqDOl8xHR+8lSAIVfrNaSAlTPTNOCgNjWcoUL3AZsw== pump@^3.0.0: version "3.0.0" @@ -4786,24 +4994,29 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -qs@6.5.2, qs@~6.5.2: +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -range-parser@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" - integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" - integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== dependencies: - bytes "3.0.0" - http-errors "1.6.3" - iconv-lite "0.4.23" + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" unpipe "1.0.0" rc@^1.2.7: @@ -4816,6 +5029,11 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-is@^16.8.4: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" + integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== + read-pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" @@ -4883,19 +5101,19 @@ readable-stream@^2.0.1, readable-stream@^2.0.6, readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6: - version "3.1.1" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06" - integrity sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA== +readable-stream@^3.1.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" + integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" util-deprecate "^1.0.1" -realpath-native@^1.0.0, realpath-native@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.2.tgz#cd51ce089b513b45cf9b1516c82989b51ccc6560" - integrity sha512-+S3zTvVt9yTntFrBpm7TQmQ3tzpCrnA1a/y+3cUHAc9ZR6aIjG0WNLR+Rj79QpJktY+VeW/TQtFlQ1bzsehI8g== +realpath-native@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" + integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== dependencies: util.promisify "^1.0.0" @@ -4909,10 +5127,10 @@ recast@~0.11.12: private "~0.1.5" source-map "~0.5.0" -regenerate-unicode-properties@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c" - integrity sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw== +regenerate-unicode-properties@^8.0.2: + version "8.1.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" + integrity sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA== dependencies: regenerate "^1.4.0" @@ -4926,10 +5144,10 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-transform@^0.13.3: - version "0.13.3" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb" - integrity sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA== +regenerator-transform@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.0.tgz#2ca9aaf7a2c239dd32e4761218425b8c7a86ecaf" + integrity sha512-rtOelq4Cawlbmq9xuMR5gdFmv7ku/sFoB7sRiywx7aq53bc52b4j6zvH7Te1Vt/X2YveDKnCGUbioieU7FEL3w== dependencies: private "^0.1.6" @@ -4941,31 +5159,27 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp-tree@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.1.tgz#27b455f9b138ca2e84c090e9aff1ffe2a04d97fa" - integrity sha512-HwRjOquc9QOwKTgbxvZTcddS5mlNlwePMQ3NFL8broajMLD5CXDAqas8Y5yxJH5QtZp5iRor3YCILd5pz71Cgw== - dependencies: - cli-table3 "^0.5.0" - colors "^1.1.2" - yargs "^12.0.5" +regexp-tree@^0.1.6: + version "0.1.10" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.10.tgz#d837816a039c7af8a8d64d7a7c3cf6a1d93450bc" + integrity sha512-K1qVSbcedffwuIslMwpe6vGlj+ZXRnGkvjAtFHfDZZZuEdA/h0dxljAPu9vhUo6Rrx2U2AwJ+nSQ6hK+lrP5MQ== regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== -regexpu-core@^4.1.3, regexpu-core@^4.2.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.4.0.tgz#8d43e0d1266883969720345e70c275ee0aec0d32" - integrity sha512-eDDWElbwwI3K0Lo6CqbQbA6FwgtCz4kYTarrri1okfkRLZAqstU+B3voZBCjg8Fl6iq0gXrJG6MvRgLthfvgOA== +regexpu-core@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.4.tgz#080d9d02289aa87fe1667a4f5136bc98a6aebaae" + integrity sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ== dependencies: regenerate "^1.4.0" - regenerate-unicode-properties "^7.0.0" + regenerate-unicode-properties "^8.0.2" regjsgen "^0.5.0" regjsparser "^0.6.0" unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.0.2" + unicode-match-property-value-ecmascript "^1.1.0" regjsgen@^0.5.0: version "0.5.0" @@ -5013,23 +5227,23 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request-promise-core@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6" - integrity sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY= +request-promise-core@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" + integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== dependencies: - lodash "^4.13.1" + lodash "^4.17.11" request-promise-native@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5" - integrity sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU= + version "1.0.7" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" + integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== dependencies: - request-promise-core "1.1.1" - stealthy-require "^1.1.0" - tough-cookie ">=2.3.3" + request-promise-core "1.1.2" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" -request@^2.55.0, request@^2.85.0, request@^2.87.0: +request@^2.55.0, request@^2.86.0, request@^2.87.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== @@ -5065,6 +5279,11 @@ require-main-filename@^1.0.1: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -5092,10 +5311,10 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.10.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1, resolve@^1.9.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" - integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== +resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1: + version "1.11.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232" + integrity sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw== dependencies: path-parse "^1.0.6" @@ -5112,17 +5331,17 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2: +rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== dependencies: glob "^7.1.3" -rsvp@^3.3.3: - version "3.6.2" - resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" - integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw== +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== run-async@^2.2.0: version "2.3.0" @@ -5132,9 +5351,9 @@ run-async@^2.2.0: is-promise "^2.1.0" rxjs@^6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.4.0.tgz#f3bb0fe7bda7fb69deac0c16f17b50b0b8790504" - integrity sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw== + version "6.5.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" + integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== dependencies: tslib "^1.9.0" @@ -5155,37 +5374,40 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sane@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/sane/-/sane-3.1.0.tgz#995193b7dc1445ef1fe41ddfca2faf9f111854c6" - integrity sha512-G5GClRRxT1cELXfdAq7UKtUsv8q/ZC5k8lQGmjEm4HcAl3HzBy68iglyNCmw4+0tiXPCBZntslHlRhbnsSws+Q== +sane@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== dependencies: + "@cnakazawa/watch" "^1.0.3" anymatch "^2.0.0" - capture-exit "^1.2.0" - exec-sh "^0.2.0" + capture-exit "^2.0.0" + exec-sh "^0.3.2" execa "^1.0.0" fb-watchman "^2.0.0" micromatch "^3.1.4" minimist "^1.1.1" walker "~1.0.5" - watch "~0.18.0" - optionalDependencies: - fsevents "^1.2.3" sax@^1.1.4, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1: - version "5.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" - integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" + integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== -send@0.16.2: - version "0.16.2" - resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" - integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== +semver@^6.0.0, semver@^6.1.0, semver@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b" + integrity sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== dependencies: debug "2.6.9" depd "~1.1.2" @@ -5194,22 +5416,22 @@ send@0.16.2: escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "~1.6.2" - mime "1.4.1" - ms "2.0.0" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.4.0" + range-parser "~1.2.1" + statuses "~1.5.0" -serve-static@1.13.2: - version "1.13.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" - integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" - parseurl "~1.3.2" - send "0.16.2" + parseurl "~1.3.3" + send "0.17.1" set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" @@ -5236,10 +5458,10 @@ set-value@^2.0.0: is-plain-object "^2.0.3" split-string "^3.0.1" -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== shebang-command@^1.2.0: version "1.2.0" @@ -5273,7 +5495,7 @@ slash@^2.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== -slice-ansi@^2.0.0: +slice-ansi@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== @@ -5324,9 +5546,9 @@ source-map-resolve@^0.5.0: urix "^0.1.0" source-map-support@^0.5.6: - version "0.5.10" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c" - integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ== + version "0.5.12" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" + integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -5368,9 +5590,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e" - integrity sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g== + version "3.0.4" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz#75ecd1a88de8c184ef015eafb51b5b48bfd11bb1" + integrity sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA== split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" @@ -5412,17 +5634,12 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -statuses@1.5.0, "statuses@>= 1.4.0 < 2": +statuses@1.5.0, "statuses@>= 1.5.0 < 2", statuses@^1.5.0, statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -statuses@~1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" - integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== - -stealthy-require@^1.1.0: +stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= @@ -5457,6 +5674,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + string_decoder@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" @@ -5490,12 +5716,12 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f" - integrity sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow== +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== dependencies: - ansi-regex "^4.0.0" + ansi-regex "^4.1.0" strip-bom@^3.0.0: version "3.0.0" @@ -5524,7 +5750,7 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^6.0.0, supports-color@^6.1.0: +supports-color@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== @@ -5532,19 +5758,19 @@ supports-color@^6.0.0, supports-color@^6.1.0: has-flag "^3.0.0" "symbol-tree@>= 3.1.0 < 4.0.0", symbol-tree@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" - integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY= + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -table@^5.0.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/table/-/table-5.2.2.tgz#61d474c9e4d8f4f7062c98c7504acb3c08aa738f" - integrity sha512-f8mJmuu9beQEDkKHLzOv4VxVYlU68NpdzjbGPl69i4Hx0sTopJuNxuzJd17iV2h24dAfa93u794OnDA5jqXvfQ== +table@^5.2.3: + version "5.4.1" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.1.tgz#0691ae2ebe8259858efb63e550b6d5f9300171e8" + integrity sha512-E6CK1/pZe2N75rGZQotFOdmzWQ1AILtgYbMAbAjvms0S1l5IDB47zG3nCnFGB/w+7nB3vKofbLXCH7HPBo864w== dependencies: - ajv "^6.6.1" + ajv "^6.9.1" lodash "^4.17.11" - slice-ansi "^2.0.0" - string-width "^2.1.1" + slice-ansi "^2.1.0" + string-width "^3.0.0" taffydb@2.7.2: version "2.7.2" @@ -5557,27 +5783,27 @@ taffydb@2.7.3: integrity sha1-KtNxaWKUmPylvIQkMJbTzeDsOjQ= tar@^4: - version "4.4.8" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d" - integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ== + version "4.4.10" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1" + integrity sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA== dependencies: chownr "^1.1.1" fs-minipass "^1.2.5" - minipass "^2.3.4" - minizlib "^1.1.1" + minipass "^2.3.5" + minizlib "^1.2.1" mkdirp "^0.5.0" safe-buffer "^5.1.2" - yallist "^3.0.2" + yallist "^3.0.3" -test-exclude@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.1.0.tgz#6ba6b25179d2d38724824661323b73e03c0c1de1" - integrity sha512-gwf0S2fFsANC55fSeSqpb8BYk6w3FDvwZxfNjeF6FRgvFa43r+7wRiA/Q0IxoRU37wB/LE8IQ4221BsNucTaCA== +test-exclude@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" + integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== dependencies: - arrify "^1.0.1" + glob "^7.1.3" minimatch "^3.0.4" read-pkg-up "^4.0.0" - require-main-filename "^1.0.1" + require-main-filename "^2.0.0" text-table@^0.2.0: version "0.2.0" @@ -5641,16 +5867,12 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -tough-cookie@>=2.3.3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" - integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== - dependencies: - ip-regex "^2.1.0" - psl "^1.1.28" - punycode "^2.1.1" +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -tough-cookie@^2.2.0, tough-cookie@^2.3.4: +tough-cookie@^2.2.0, tough-cookie@^2.3.3, tough-cookie@^2.3.4: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== @@ -5684,9 +5906,9 @@ trim-right@^1.0.1: integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= tslib@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" - integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== tunnel-agent@^0.6.0: version "0.6.0" @@ -5707,25 +5929,33 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-is@^1.6.4, type-is@~1.6.16: - version "1.6.16" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" - integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q== +type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== dependencies: media-typer "0.3.0" - mime-types "~2.1.18" + mime-types "~2.1.24" typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -uglify-js@3.4.x, uglify-js@^3.1.4: - version "3.4.9" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" - integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q== +uglify-js@3.4.x: + version "3.4.10" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" + integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== + dependencies: + commander "~2.19.0" + source-map "~0.6.1" + +uglify-js@^3.1.4: + version "3.6.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" + integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== dependencies: - commander "~2.17.1" + commander "~2.20.0" source-map "~0.6.1" unicode-canonical-property-names-ecmascript@^1.0.4: @@ -5741,15 +5971,15 @@ unicode-match-property-ecmascript@^1.0.4: unicode-canonical-property-names-ecmascript "^1.0.4" unicode-property-aliases-ecmascript "^1.0.4" -unicode-match-property-value-ecmascript@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz#9f1dc76926d6ccf452310564fd834ace059663d4" - integrity sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ== +unicode-match-property-value-ecmascript@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277" + integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g== unicode-property-aliases-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0" - integrity sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg== + version "1.0.5" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57" + integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw== union-value@^1.0.0: version "1.0.0" @@ -5791,7 +6021,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -urijs@1.19.1: +urijs@1.19.1, urijs@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a" integrity sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg== @@ -5858,21 +6088,13 @@ w3c-hr-time@^1.0.1: dependencies: browser-process-hrtime "^0.1.2" -walker@~1.0.5: +walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= dependencies: makeerror "1.0.x" -watch@~0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" - integrity sha1-KAlUdsbffJDJYxOJkMClQj60uYY= - dependencies: - exec-sh "^0.2.0" - minimist "^1.2.0" - webidl-conversions@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-2.0.1.tgz#3bf8258f7d318c7443c36f2e169402a1a6703506" @@ -5925,7 +6147,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@^1.2.12, which@^1.2.9, which@^1.3.0: +which@^1.2.9, which@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -5939,13 +6161,13 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" -wootils@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/wootils/-/wootils-2.0.0.tgz#bf9349775ab5a4b9e4ef88f2350ce7443e0bc818" - integrity sha512-Htp7bBJD4Dmnok1pu7XeDNg68JJdGhZHLPuv7edzNOAgvcG+LDDLOAFpH/J3XsHGWd/WDfNdwwWZicD4fkdGMw== +wootils@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/wootils/-/wootils-2.3.0.tgz#b628fd4b0b7644ce116456d2f3c223cfddc5e7c5" + integrity sha512-VtUPHMR+0NM/+rEtGVZjCbY5ajfI0BpBqpCkepFCqcxvMq9YB/coZX5VAdR/aJHup7HvfROLoABtDYRAe2F1jw== dependencies: colors "1.3.3" - fs-extra "7.0.1" + fs-extra "8.0.1" jimple "1.5.0" statuses "1.5.0" urijs "1.19.1" @@ -5982,10 +6204,10 @@ write-file-atomic@2.4.1: imurmurhash "^0.1.4" signal-exit "^3.0.2" -write@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" - integrity sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c= +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== dependencies: mkdirp "^0.5.1" @@ -6011,12 +6233,12 @@ xtend@^4.0.0: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= -"y18n@^3.2.1 || ^4.0.0": +"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== -yallist@^3.0.0, yallist@^3.0.2: +yallist@^3.0.0, yallist@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== @@ -6029,7 +6251,32 @@ yargs-parser@^11.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@12.0.5, yargs@^12.0.2, yargs@^12.0.5: +yargs-parser@^13.0.0: + version "13.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" + integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@13.2.2: + version "13.2.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.2.tgz#0c101f580ae95cea7f39d927e7770e3fdc97f993" + integrity sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA== + dependencies: + cliui "^4.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + os-locale "^3.1.0" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.0.0" + +yargs@^12.0.2: version "12.0.5" resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== From 4f4731db239cb22fd7d60280b1c938d0249e2e36 Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 03:25:50 -0300 Subject: [PATCH 02/78] fix(ESLint): fix syntax of the configuration file --- .eslintrc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.eslintrc b/.eslintrc index d3c9c97f..be7d911e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,16 +1,16 @@ { "extends": "airbnb-base", "rules": { - indent: ['error', 2, { - MemberExpression: 0, + "indent": ["error", 2, { + "MemberExpression": 0 }], "no-underscore-dangle": ["error", { "allowAfterThis": true }], "comma-dangle": ["error", { - arrays: "always-multiline", - objects: "always-multiline", - imports: "always-multiline", - exports: "always-multiline", - functions: "never", + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "never" }], "class-methods-use-this": "off", "import/no-unresolved": "off", From 7f76b475057114c397e76d6e707e85404049d9a0 Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 03:39:42 -0300 Subject: [PATCH 03/78] feat(app): add a 'listen' method to match the http module and express API --- src/app/index.js | 16 +++++++ tests/app/index.test.js | 95 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/app/index.js b/src/app/index.js index 18ccd8df..6360b265 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -163,6 +163,22 @@ class Jimpex extends Jimple { return this.instance; } + /** + * This is an alias of `start`. The idea is for it to be used on serverless platforms, where you + * don't get to start your app, you just have export it. + * @param {number} port The port where the app will run. In case the + * rest of the app needs to be aware of the port, + * this method will overwrite the `port` setting + * on the configuration. + * @param {function(config:AppConfiguration)} [fn] A callback function to be called when the + * server starts. + * @return {Object} The server instance + */ + listen(port, fn = () => {}) { + const config = this.get('appConfiguration'); + config.set('port', port); + return this.start(fn, port); + } /** * Emit an app event with a reference to this class instance. * @param {string} name The name of the event. diff --git a/tests/app/index.test.js b/tests/app/index.test.js index 1ddb1246..d81fd5be 100644 --- a/tests/app/index.test.js +++ b/tests/app/index.test.js @@ -734,7 +734,7 @@ describe('app:Jimpex', () => { expect(expressMock.mocks.closeInstance).toHaveBeenCalledTimes(1); }); - it('should start the server an fire a custom callback', () => { + it('should start the server and fire a custom callback', () => { // Given class Sut extends Jimpex { boot() {} @@ -772,6 +772,99 @@ describe('app:Jimpex', () => { expect(callback).toHaveBeenCalledWith(appConfiguration); }); + it('should start the server using `listen`', () => { + // Given + class Sut extends Jimpex { + boot() {} + } + const pathUtils = { + joinFrom: jest.fn((from, rest) => path.join(from, rest)), + }; + JimpleMock.service('pathUtils', pathUtils); + const defaultConfig = {}; + const rootRequire = jest.fn(() => defaultConfig); + JimpleMock.service('rootRequire', rootRequire); + const customPort = 8080; + const configuration = { + port: 2509, + changes: {}, + }; + const appConfiguration = { + loadFromEnvironment: jest.fn(), + get: jest.fn((prop) => configuration.changes[prop] || configuration[prop]), + set: jest.fn((prop, value) => { + configuration.changes[prop] = value; + }), + }; + JimpleMock.service('appConfiguration', appConfiguration); + const events = { + emit: jest.fn(), + }; + JimpleMock.service('events', events); + const appLogger = { + success: jest.fn(), + }; + JimpleMock.service('appLogger', appLogger); + let sut = null; + // When + sut = new Sut(); + sut.listen(customPort); + // Then + expect(appLogger.success).toHaveBeenCalledTimes(1); + expect(appLogger.success).toHaveBeenCalledWith(`Starting on port ${customPort}`); + expect(appConfiguration.set).toHaveBeenCalledTimes(1); + expect(appConfiguration.set).toHaveBeenCalledWith('port', customPort); + expect(configuration.changes.port).toBe(customPort); + }); + + it('should start the server using `listen` and fire a custom callback', () => { + // Given + class Sut extends Jimpex { + boot() {} + } + const pathUtils = { + joinFrom: jest.fn((from, rest) => path.join(from, rest)), + }; + JimpleMock.service('pathUtils', pathUtils); + const defaultConfig = {}; + const rootRequire = jest.fn(() => defaultConfig); + JimpleMock.service('rootRequire', rootRequire); + const customPort = 8080; + const configuration = { + port: 2509, + changes: {}, + }; + const appConfiguration = { + loadFromEnvironment: jest.fn(), + get: jest.fn((prop) => configuration.changes[prop] || configuration[prop]), + set: jest.fn((prop, value) => { + configuration.changes[prop] = value; + }), + }; + JimpleMock.service('appConfiguration', appConfiguration); + const events = { + emit: jest.fn(), + }; + JimpleMock.service('events', events); + const appLogger = { + success: jest.fn(), + }; + JimpleMock.service('appLogger', appLogger); + const callback = jest.fn(); + let sut = null; + // When + sut = new Sut(); + sut.listen(customPort, callback); + // Then + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(appConfiguration); + expect(appLogger.success).toHaveBeenCalledTimes(1); + expect(appLogger.success).toHaveBeenCalledWith(`Starting on port ${customPort}`); + expect(appConfiguration.set).toHaveBeenCalledTimes(1); + expect(appConfiguration.set).toHaveBeenCalledWith('port', customPort); + expect(configuration.changes.port).toBe(customPort); + }); + it('shouldn\'t do anything when trying to stop the server without starting it', () => { // Given class Sut extends Jimpex { From b584f4acd128c18b91f5a9a30fc08133fa92a54b Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 03:42:19 -0300 Subject: [PATCH 04/78] docs(project): add a line about the new 'listen' method on the README --- README-esdoc.md | 3 ++- README.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README-esdoc.md b/README-esdoc.md index b8113164..f8abd5fe 100644 --- a/README-esdoc.md +++ b/README-esdoc.md @@ -82,7 +82,8 @@ app.start(() => { }); ``` -> And like Express, you can send a callback to be executed after the server starts. +> - Like Express, you can send a callback to be executed after the server starts. +> - You also have a `listen` alias with the same signature as express (port and callback) for serverless platforms where you don't manually start the app. You can also stop the app by calling `stop()`: diff --git a/README.md b/README.md index 6a3e5713..42020a3a 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,8 @@ app.start(() => { }); ``` -> And like Express, you can send a callback to be executed after the server starts. +> - Like Express, you can send a callback to be executed after the server starts. +> - You also have a `listen` alias with the same signature as express (port and callback) for serverless platforms where you don't manually start the app. You can also stop the app by calling `stop()`: From e846700cdf90e259c3b4fc7273374e4c48b7c932 Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 03:47:01 -0300 Subject: [PATCH 05/78] refactor(project): use wootils' ObjectUtils instead of extend --- package.json | 1 - src/app/index.js | 4 ++-- src/controllers/common/rootStatics.js | 4 ++-- src/services/html/htmlGenerator.js | 4 ++-- yarn.lock | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index de1d3fdd..fd0777d9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "express": "^4.17.1", "body-parser": "^1.19.0", "compression": "^1.7.4", - "extend": "^3.0.2", "node-fetch": "^2.6.0", "urijs": "^1.19.1", "statuses": "^1.5.0", diff --git a/src/app/index.js b/src/app/index.js index 6360b265..0dbf24cb 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -1,5 +1,5 @@ const Jimple = require('jimple'); -const extend = require('extend'); +const ObjectUtils = require('wootils/shared/objectUtils'); const express = require('express'); const bodyParser = require('body-parser'); const compression = require('compression'); @@ -48,7 +48,7 @@ class Jimpex extends Jimple { * The app options. * @type {JimpexOptions} */ - this.options = extend(true, { + this.options = ObjectUtils.merge({ version: '0.0.0', filesizeLimit: '15MB', configuration: { diff --git a/src/controllers/common/rootStatics.js b/src/controllers/common/rootStatics.js index b06242fe..9cd43f9d 100644 --- a/src/controllers/common/rootStatics.js +++ b/src/controllers/common/rootStatics.js @@ -1,4 +1,4 @@ -const extend = require('extend'); +const ObjectUtils = require('wootils/shared/objectUtils'); const mime = require('mime'); const { controller } = require('../../utils/wrappers'); /** @@ -53,7 +53,7 @@ class RootStaticsController { const item = this.files[file]; const extension = item.output.split('.').pop().toLowerCase(); const baseHeaders = { 'Content-Type': mime.getType(extension) }; - const headers = extend(true, baseHeaders, item.headers); + const headers = ObjectUtils.merge(baseHeaders, item.headers); Object.keys(headers).forEach((headerName) => { res.setHeader(headerName, headers[headerName]); diff --git a/src/services/html/htmlGenerator.js b/src/services/html/htmlGenerator.js index 61d5ea4d..4a4faa82 100644 --- a/src/services/html/htmlGenerator.js +++ b/src/services/html/htmlGenerator.js @@ -1,4 +1,4 @@ -const extend = require('extend'); +const ObjectUtils = require('wootils/shared/objectUtils'); const { deferred } = require('wootils/shared'); const { provider } = require('../../utils/wrappers'); /** @@ -66,7 +66,7 @@ class HTMLGenerator { * The service options. * @type {HTMLGeneratorOptions} */ - this.options = extend({ + this.options = ObjectUtils.merge({ template: 'index.tpl.html', file: 'index.html', deleteTemplateAfter: true, diff --git a/yarn.lock b/yarn.lock index a07273d1..650e70ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2512,7 +2512,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.2, extend@~3.0.2: +extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== From 5ceff12cd2fe293c8c41bd7ac10f1484b4559f0e Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 05:11:55 -0300 Subject: [PATCH 06/78] feat(project): add a new version validator middleware --- src/middlewares/index.js | 2 + src/middlewares/utils/index.js | 6 + src/middlewares/utils/versionValidator.js | 117 ++++++ tests/middlewares/index.test.js | 1 + .../utils/versionValidator.test.js | 386 ++++++++++++++++++ 5 files changed, 512 insertions(+) create mode 100644 src/middlewares/utils/index.js create mode 100644 src/middlewares/utils/versionValidator.js create mode 100644 tests/middlewares/utils/versionValidator.test.js diff --git a/src/middlewares/index.js b/src/middlewares/index.js index cb24f5dc..cff96f76 100644 --- a/src/middlewares/index.js +++ b/src/middlewares/index.js @@ -1,7 +1,9 @@ const common = require('./common'); const html = require('./html'); +const utils = require('./utils'); module.exports = { common, html, + utils, }; diff --git a/src/middlewares/utils/index.js b/src/middlewares/utils/index.js new file mode 100644 index 00000000..c1216a68 --- /dev/null +++ b/src/middlewares/utils/index.js @@ -0,0 +1,6 @@ +const { versionValidator, versionValidatorCustom } = require('./versionValidator'); + +module.exports = { + versionValidator, + versionValidatorCustom, +}; diff --git a/src/middlewares/utils/versionValidator.js b/src/middlewares/utils/versionValidator.js new file mode 100644 index 00000000..3d0727c1 --- /dev/null +++ b/src/middlewares/utils/versionValidator.js @@ -0,0 +1,117 @@ +const ObjectUtils = require('wootils/shared/objectUtils'); +const statuses = require('statuses'); +const { middleware } = require('../../utils/wrappers'); + +class VersionValidator { + constructor( + version, + responsesBuilder, + AppError, + options = {} + ) { + this._responsesBuilder = responsesBuilder; + this._AppError = AppError; + this._options = ObjectUtils.merge( + { + latest: { + allow: true, + name: 'latest', + }, + errors: { + default: 'The application version doesn\'t match', + noVersion: 'No version was found on the route', + }, + popup: { + variable: 'popup', + title: 'Conflict', + message: 'vesion:conflict', + }, + version, + }, + options + ); + + if (!this._options.version) { + throw new Error('You need to supply a version'); + } + } + + middleware() { + return (req, res, next) => { + const { version } = req.params; + if (!version) { + next(new this._AppError( + this._options.errors.noVersion, + { + status: statuses['bad request'], + response: { + validation: true, + }, + } + )); + } else if (version === this._options.version || this._validateLatest(version)) { + next(); + } else if (this._isPopup(req)) { + this._responsesBuilder.htmlPostMessage( + res, + this._options.popup.title, + this._options.popup.message, + statuses.conflict + ); + } else { + next(new this._AppError( + this._options.errors.default, + { + status: statuses.conflict, + response: { + validation: true, + }, + } + )); + } + }; + } + + get options() { + return this._options; + } + + _isPopup(req) { + const popup = req.query[this._options.popup.variable]; + return !!(popup && popup.toLowerCase() === 'true'); + } + + _validateLatest(version) { + return this._options.latest.allow && version === this._options.latest.name; + } +} + +const versionValidatorCustom = (options) => middleware((app, point) => { + const validator = new VersionValidator( + app.get('appConfiguration').get('version'), + app.get('responsesBuilder'), + app.get('appError'), + options + ); + + const middlewareValidator = validator.middleware(); + let result; + if (point) { + const router = app.get('router'); + result = [ + router.all('/:version/*', middlewareValidator), + ]; + } else { + result = middlewareValidator; + } + + return result; +}); + +const versionValidator = versionValidatorCustom(); + +module.exports = { + VersionValidator, + versionValidator, + versionValidatorCustom, +}; diff --git a/tests/middlewares/index.test.js b/tests/middlewares/index.test.js index 9033c1de..21a23768 100644 --- a/tests/middlewares/index.test.js +++ b/tests/middlewares/index.test.js @@ -15,6 +15,7 @@ describe('middlewares', () => { const knownMiddlewares = [ 'common', 'html', + 'utils', ]; // When/Then expect(Object.keys(middlewares).length).toBe(knownMiddlewares.length); diff --git a/tests/middlewares/utils/versionValidator.test.js b/tests/middlewares/utils/versionValidator.test.js new file mode 100644 index 00000000..217b9c24 --- /dev/null +++ b/tests/middlewares/utils/versionValidator.test.js @@ -0,0 +1,386 @@ +const JimpleMock = require('/tests/mocks/jimple.mock'); + +jest.mock('jimple', () => JimpleMock); +jest.unmock('/src/utils/wrappers'); +jest.unmock('/src/middlewares/utils/versionValidator'); + +require('jasmine-expect'); +const statuses = require('statuses'); +const { + VersionValidator, + versionValidator, + versionValidatorCustom, +} = require('/src/middlewares/utils/versionValidator'); + +describe('services/api:versionValidator', () => { + it('should be instantiated with its default options', () => { + // Given + const version = '1.0'; + const responsesBuilder = 'responsesBuilder'; + const AppError = 'AppError'; + let sut = null; + // When + sut = new VersionValidator(version, responsesBuilder, AppError); + // Then + expect(sut).toBeInstanceOf(VersionValidator); + expect(sut.options).toEqual({ + latest: { + allow: true, + name: 'latest', + }, + errors: { + default: 'The application version doesn\'t match', + noVersion: 'No version was found on the route', + }, + popup: { + variable: 'popup', + title: 'Conflict', + message: 'vesion:conflict', + }, + version, + }); + }); + + it('should throw an error when instantiated without a version', () => { + // Given + const version = null; + const responsesBuilder = 'responsesBuilder'; + const AppError = 'AppError'; + // When/Then + expect(() => new VersionValidator(version, responsesBuilder, AppError)) + .toThrow(/You need to supply a version/i); + }); + + it('should be able to overwrite its default options', () => { + // Given + const version = '1.0'; + const responsesBuilder = 'responsesBuilder'; + const AppError = 'AppError'; + const options = { + latest: { + allow: false, + }, + errors: { + default: 'No way!', + }, + popup: { + title: 'So much conflict!', + }, + version: 'alpha.5', + }; + let sut = null; + // When + sut = new VersionValidator(version, responsesBuilder, AppError, options); + // Then + expect(sut).toBeInstanceOf(VersionValidator); + expect(sut.options).toEqual({ + latest: { + allow: options.latest.allow, + name: 'latest', + }, + errors: { + default: options.errors.default, + noVersion: 'No version was found on the route', + }, + popup: { + variable: 'popup', + title: options.popup.title, + message: 'vesion:conflict', + }, + version: options.version, + }); + }); + + describe('middleware', () => { + it('should generate an error when no version is found in the route', () => { + // Given + const version = '0.1'; + const options = { + errors: { + noVersion: 'No version!', + }, + }; + const appError = jest.fn(); + class AppError { + constructor(...args) { + appError(...args); + } + } + const request = { + params: {}, + query: {}, + }; + const response = 'response'; + const next = jest.fn(); + let sut = null; + let middleware = null; + // When + sut = new VersionValidator(version, 'responsesBuilder', AppError, options); + middleware = sut.middleware(); + middleware(request, response, next); + // Then + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.any(AppError)); + expect(appError).toHaveBeenCalledWith( + options.errors.noVersion, + { + status: statuses['bad request'], + response: { + validation: true, + }, + } + ); + }); + + it('should allow the current version', () => { + // Given + const version = '1.0'; + const request = { + params: { + version, + }, + query: {}, + }; + const response = 'response'; + const next = jest.fn(); + let sut = null; + let middleware = null; + // When + sut = new VersionValidator(version); + middleware = sut.middleware(); + middleware(request, response, next); + // Then + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + it('should generate an error when the version doesn\'t match', () => { + // Given + const version = '2.0'; + const options = { + errors: { + default: 'Missmatch!', + }, + }; + const appError = jest.fn(); + class AppError { + constructor(...args) { + appError(...args); + } + } + const request = { + params: { + version: '3.0', + }, + query: {}, + }; + const response = 'response'; + const next = jest.fn(); + let sut = null; + let middleware = null; + // When + sut = new VersionValidator(version, 'responsesBuilder', AppError, options); + middleware = sut.middleware(); + middleware(request, response, next); + // Then + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.any(AppError)); + expect(appError).toHaveBeenCalledWith( + options.errors.default, + { + status: statuses.conflict, + response: { + validation: true, + }, + } + ); + }); + + it('should allow \'latest\' as a version', () => { + // Given + const version = '1.0'; + const request = { + params: { + version: 'latest', + }, + query: {}, + }; + const response = 'response'; + const next = jest.fn(); + let sut = null; + let middleware = null; + // When + sut = new VersionValidator(version); + middleware = sut.middleware(); + middleware(request, response, next); + // Then + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + it('shouldn\'t allow \'latest\' as a version', () => { + // Given + const version = '2.0'; + const options = { + latest: { + allow: false, + }, + errors: { + default: 'No latest!', + }, + }; + const appError = jest.fn(); + class AppError { + constructor(...args) { + appError(...args); + } + } + const request = { + params: { + version: 'latest', + }, + query: {}, + }; + const response = 'response'; + const next = jest.fn(); + let sut = null; + let middleware = null; + // When + sut = new VersionValidator(version, 'responsesBuilder', AppError, options); + middleware = sut.middleware(); + middleware(request, response, next); + // Then + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.any(AppError)); + expect(appError).toHaveBeenCalledWith( + options.errors.default, + { + status: statuses.conflict, + response: { + validation: true, + }, + } + ); + }); + + it('should send an HTML post message if the validation fails on a popup', () => { + // Given + const version = '2.0'; + const options = { + popup: { + variable: 'is-popup', + title: 'MyPopUpConflict', + message: 'version-conflict', + }, + }; + const responsesBuilder = { + htmlPostMessage: jest.fn(), + }; + const appError = jest.fn(); + class AppError { + constructor(...args) { + appError(...args); + } + } + const request = { + params: { + version: '25.09', + }, + query: { + [options.popup.variable]: 'true', + }, + }; + const response = 'response'; + const next = jest.fn(); + let sut = null; + let middleware = null; + // When + sut = new VersionValidator(version, responsesBuilder, AppError, options); + middleware = sut.middleware(); + middleware(request, response, next); + // Then + expect(next).toHaveBeenCalledTimes(0); + expect(responsesBuilder.htmlPostMessage).toHaveBeenCalledTimes(1); + expect(responsesBuilder.htmlPostMessage).toHaveBeenCalledWith( + response, + options.popup.title, + options.popup.message, + statuses.conflict + ); + }); + }); + + describe('shorthand', () => { + it('should generate the middleware function', () => { + // Given + const appConfiguration = { + get: jest.fn(() => '25.09'), + }; + const services = { + appConfiguration, + }; + const app = { + get: jest.fn((name) => services[name] || name), + }; + let sut = null; + let toCompare = null; + const expectedServices = [ + 'appConfiguration', + 'responsesBuilder', + 'appError', + ]; + // When + sut = versionValidatorCustom().connect(app); + toCompare = new VersionValidator('25.09'); + // Then + expect(sut.toString()).toEqual(toCompare.middleware().toString()); + expect(app.get).toHaveBeenCalledTimes(expectedServices.length); + expectedServices.forEach((service) => { + expect(app.get).toHaveBeenCalledWith(service); + }); + expect(appConfiguration.get).toHaveBeenCalledTimes(1); + expect(appConfiguration.get).toHaveBeenCalledWith('version'); + }); + + it('should generate a controller route', () => { + // Given + const appConfiguration = { + get: jest.fn(() => '25.09'), + }; + const router = { + all: jest.fn(() => 'route'), + }; + const services = { + appConfiguration, + router, + }; + const app = { + get: jest.fn((name) => services[name] || name), + }; + let sut = null; + let middleware = null; + let toCompare = null; + const expectedServices = [ + 'appConfiguration', + 'responsesBuilder', + 'appError', + 'router', + ]; + // When + sut = versionValidator.connect(app, '/route'); + ([[, middleware]] = router.all.mock.calls); + toCompare = new VersionValidator('25.09'); + // Then + expect(sut).toEqual(['route']); + expect(middleware.toString()).toEqual(toCompare.middleware().toString()); + expect(router.all).toHaveBeenCalledTimes(1); + expect(router.all).toHaveBeenCalledWith('/:version/*', expect.any(Function)); + expect(app.get).toHaveBeenCalledTimes(expectedServices.length); + expectedServices.forEach((service) => { + expect(app.get).toHaveBeenCalledWith(service); + }); + expect(appConfiguration.get).toHaveBeenCalledTimes(1); + expect(appConfiguration.get).toHaveBeenCalledWith('version'); + }); + }); +}); From e0e0f2cdcc9307d3e1b3a5c55a9992a6df64d9fe Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 06:11:40 -0300 Subject: [PATCH 07/78] docs(middlewares/utils/versionValidator): add inline documentation --- src/middlewares/utils/versionValidator.js | 164 ++++++++++++++++++++-- src/typedef.js | 4 - 2 files changed, 150 insertions(+), 18 deletions(-) diff --git a/src/middlewares/utils/versionValidator.js b/src/middlewares/utils/versionValidator.js index 3d0727c1..7fa0dab6 100644 --- a/src/middlewares/utils/versionValidator.js +++ b/src/middlewares/utils/versionValidator.js @@ -2,25 +2,112 @@ const ObjectUtils = require('wootils/shared/objectUtils'); const statuses = require('statuses'); const { middleware } = require('../../utils/wrappers'); +/** + * @typdef {Object} VersionValidatorLatestOptions + * @description The options for how the middleware should behave if the requested version is + * `latest`. + * @property {boolean} [allow=true] Whether or not the middleware should validate the _"latest + * version"_. + * @property {string} [name='latest'] The name of the _"latest version"_. Basically, + * `req.params.version` must match with this property in order + * to be consider "latest". + */ + +/** + * @typdef {Object} VersionValidatorPopupOptions + * @description The options for how to detect if the request comes from a popup and how to compose + * the post message the middleware will use to respond. + * @property {string} [variable='popup'] The name of the query string variable the + * middleware will check in order to indentify + * whether the request comes from a popup or not. + * The variable must have `'true'` as its value. + * @property {string} [title='Conflict'] The title of the page that will be generated to + * respond in case the versions don't match. + * @property {string} [message='vesion:conflict'] The contents of the post message the generated + * page will send if the versions don't match. + */ + +/** + * @typedef {Object} VersionValidatorOptions + * @description The options used to customize a {@link VersionValidator} instance. + * @property {string} [error] The error message to show when the version + * is invalid. + * @property {VersionValidatorLatestOptions} [latest] The options for how the middleware should + * behave if the requested version is + * `latest`. + * @property {VersionValidatorPopupOptions} [popup] The options for how to detect if the request + * comes from a popup and how to compose the + * post message the middleware will use to + * respond. + * @property {string|number} [version] The version used to validate the requests. + * On the {@link VersionValidator} + * constructor, if specified via parameter, + * the class will take care of automatically + * add it to the options. + */ + +/** + * This is the handler for the middleware/controller that validates the app version. + * This is useful in cases where you want to restrict the access to the app to specific versions, + * for example: you have a frontend app which needs to be aligned with the "current" version of + * the app, since the frontend won't realize a new version was released, the validator can be + * used to let the frontend know. + * Also, it can be configured to handle requests from popups, in which case, instead of generating + * an error message, it will send a post message. + */ class VersionValidator { + /** + * @param {?string|?number} version The current version of the app. The reason + * this is nullable is because this comes + * directly from the app configuration, but + * you may want to re use this to validate + * "another version", so you can use the + * custom shorthand and send the version using + * the `options` parameter. + * @param {ResponsesBuilder} responsesBuilder To generate post message responses for + * popups. + * @param {Class} AppError To generate errors in case the version is + * invalid. + * @param {VersionValidatorOptions} [options={}] Custom options to modify the middleware + * behavior. + * @throws {Error} If the version is `null` and the `options` don't include one either. + */ constructor( version, responsesBuilder, AppError, options = {} ) { + /** + * A local reference for the `responsesBuilder` service. + * @type {ResponsesBuilder} + * @access protected + * @ignore + */ this._responsesBuilder = responsesBuilder; + /** + * A local reference for the class the app uses to generate errors. + * @type {Class} + * @access protected + * @ignore + */ this._AppError = AppError; + /** + * These are the "settings" the middleware will use in order to validate the requests. + * @type {VersionValidatorOptions} + * @access protected + * @ignore + */ this._options = ObjectUtils.merge( { - latest: { - allow: true, - name: 'latest', - }, errors: { default: 'The application version doesn\'t match', noVersion: 'No version was found on the route', }, + latest: { + allow: true, + name: 'latest', + }, popup: { variable: 'popup', title: 'Conflict', @@ -35,11 +122,16 @@ class VersionValidator { throw new Error('You need to supply a version'); } } - + /** + * Returns the Express middleware that will validate the `version` parameter. + * @return {ExpressMiddleware} + */ middleware() { return (req, res, next) => { + // Get the `version` parameter from the request. const { version } = req.params; if (!version) { + // If no version is present, move to the error handler. next(new this._AppError( this._options.errors.noVersion, { @@ -50,8 +142,16 @@ class VersionValidator { } )); } else if (version === this._options.version || this._validateLatest(version)) { + /** + * If the version matches the one on the options, or the requested version is "latest" + * (and the option is enabled), move on to the next middleware. + */ next(); } else if (this._isPopup(req)) { + /** + * If it doesn't match and the request is comming from a popup, send a response with a + * post message. + */ this._responsesBuilder.htmlPostMessage( res, this._options.popup.title, @@ -59,6 +159,7 @@ class VersionValidator { statuses.conflict ); } else { + // Finally, if it doesn't match and is not from a popup, move to the error handler. next(new this._AppError( this._options.errors.default, { @@ -71,43 +172,78 @@ class VersionValidator { } }; } - + /** + * The options used to customize the middleware behavior. + * @return {VersionValidatorOptions} + */ get options() { return this._options; } - + /** + * Helper method that checks if the incoming request is from a popup. + * @param {ExpressRequest} req The request information. + * @return {Boolean} + * @access protected + * @ignore + */ _isPopup(req) { const popup = req.query[this._options.popup.variable]; return !!(popup && popup.toLowerCase() === 'true'); } - + /** + * Helper method that checks if the "latest version" is enabled and if the given version is + * "the latest" (comparing it with the option name). + * @param {string|number} version The version to validate. + * @return {Boolean} + * @access protected + * @ignore + */ _validateLatest(version) { return this._options.latest.allow && version === this._options.latest.name; } } - +/** + * Generates a middleware/controller with customized options. + * The reason for "middleware/controller" is because the wrappers for both are the same, the + * difference is that, for controllers, Jimpex sends a second parameter with the point where they + * are mounted. + * By validating the point parameter, the function can know whether the implementation is going + * to use the middleware by itself or as a route middleware. + * If used as middleware, it will just return the result of {@link VersionValidator#middleware}; + * but if used as controller, it will mount it on `[point]/:version/*`. + * @param {VersionValidatorOptions} [options] Custom options to modify the middleware behavior. + * @return {Middleware} + */ const versionValidatorCustom = (options) => middleware((app, point) => { - const validator = new VersionValidator( + // Get the middleware function. + const middlewareValidator = (new VersionValidator( app.get('appConfiguration').get('version'), app.get('responsesBuilder'), app.get('appError'), options - ); - - const middlewareValidator = validator.middleware(); + )).middleware(); + // Set the variable to be returned. let result; if (point) { + // If the implementation will use it as a router, get the `router` service and mount it. const router = app.get('router'); + // Set the array of "routes" as the return value. result = [ router.all('/:version/*', middlewareValidator), ]; } else { + // If the implementation will use it stand alone, just set the function to be returned. result = middlewareValidator; } + // Return the route or the middleware. return result; }); - +/** + * A middleware that will validate a `version` request parameter against the app version and + * generate an error if they don't match. + * @var {Middleware} + */ const versionValidator = versionValidatorCustom(); module.exports = { diff --git a/src/typedef.js b/src/typedef.js index a79cc8fd..c73b9cbe 100644 --- a/src/typedef.js +++ b/src/typedef.js @@ -54,10 +54,6 @@ * @external {ExpressResponse} https://expressjs.com/en/4x/api.html#res */ -/** - * @external {ExpressResponse} https://expressjs.com/en/4x/api.html#res - */ - /** * @typedef {function(err:?Error)} ExpressNext A function to call the next middleware. If an * argument is specified, it will be handled as an error From de787c5d1264bd6b0892fc79c1910f48f66a7c62 Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 06:16:31 -0300 Subject: [PATCH 08/78] refactor(middlewares/utils/versionValidator): only validate if there's a version --- src/middlewares/utils/versionValidator.js | 21 ++------ .../utils/versionValidator.test.js | 52 ++++--------------- 2 files changed, 15 insertions(+), 58 deletions(-) diff --git a/src/middlewares/utils/versionValidator.js b/src/middlewares/utils/versionValidator.js index 7fa0dab6..067aca70 100644 --- a/src/middlewares/utils/versionValidator.js +++ b/src/middlewares/utils/versionValidator.js @@ -66,7 +66,7 @@ class VersionValidator { * the `options` parameter. * @param {ResponsesBuilder} responsesBuilder To generate post message responses for * popups. - * @param {Class} AppError To generate errors in case the version is + * @param {Class} AppError To generate the error in case the version is * invalid. * @param {VersionValidatorOptions} [options={}] Custom options to modify the middleware * behavior. @@ -100,10 +100,7 @@ class VersionValidator { */ this._options = ObjectUtils.merge( { - errors: { - default: 'The application version doesn\'t match', - noVersion: 'No version was found on the route', - }, + error: 'The application version doesn\'t match', latest: { allow: true, name: 'latest', @@ -131,16 +128,8 @@ class VersionValidator { // Get the `version` parameter from the request. const { version } = req.params; if (!version) { - // If no version is present, move to the error handler. - next(new this._AppError( - this._options.errors.noVersion, - { - status: statuses['bad request'], - response: { - validation: true, - }, - } - )); + // If no version is present, move on to the next middleware. + next(); } else if (version === this._options.version || this._validateLatest(version)) { /** * If the version matches the one on the options, or the requested version is "latest" @@ -161,7 +150,7 @@ class VersionValidator { } else { // Finally, if it doesn't match and is not from a popup, move to the error handler. next(new this._AppError( - this._options.errors.default, + this._options.error, { status: statuses.conflict, response: { diff --git a/tests/middlewares/utils/versionValidator.test.js b/tests/middlewares/utils/versionValidator.test.js index 217b9c24..86d401bc 100644 --- a/tests/middlewares/utils/versionValidator.test.js +++ b/tests/middlewares/utils/versionValidator.test.js @@ -24,14 +24,11 @@ describe('services/api:versionValidator', () => { // Then expect(sut).toBeInstanceOf(VersionValidator); expect(sut.options).toEqual({ + error: 'The application version doesn\'t match', latest: { allow: true, name: 'latest', }, - errors: { - default: 'The application version doesn\'t match', - noVersion: 'No version was found on the route', - }, popup: { variable: 'popup', title: 'Conflict', @@ -57,12 +54,10 @@ describe('services/api:versionValidator', () => { const responsesBuilder = 'responsesBuilder'; const AppError = 'AppError'; const options = { + error: 'No way!', latest: { allow: false, }, - errors: { - default: 'No way!', - }, popup: { title: 'So much conflict!', }, @@ -74,14 +69,11 @@ describe('services/api:versionValidator', () => { // Then expect(sut).toBeInstanceOf(VersionValidator); expect(sut.options).toEqual({ + error: options.error, latest: { allow: options.latest.allow, name: 'latest', }, - errors: { - default: options.errors.default, - noVersion: 'No version was found on the route', - }, popup: { variable: 'popup', title: options.popup.title, @@ -92,20 +84,9 @@ describe('services/api:versionValidator', () => { }); describe('middleware', () => { - it('should generate an error when no version is found in the route', () => { + it('shouldn\'t do anything when no version is found in the route', () => { // Given const version = '0.1'; - const options = { - errors: { - noVersion: 'No version!', - }, - }; - const appError = jest.fn(); - class AppError { - constructor(...args) { - appError(...args); - } - } const request = { params: {}, query: {}, @@ -115,21 +96,12 @@ describe('services/api:versionValidator', () => { let sut = null; let middleware = null; // When - sut = new VersionValidator(version, 'responsesBuilder', AppError, options); + sut = new VersionValidator(version, 'responsesBuilder', 'AppError'); middleware = sut.middleware(); middleware(request, response, next); // Then expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.any(AppError)); - expect(appError).toHaveBeenCalledWith( - options.errors.noVersion, - { - status: statuses['bad request'], - response: { - validation: true, - }, - } - ); + expect(next).toHaveBeenCalledWith(); }); it('should allow the current version', () => { @@ -158,9 +130,7 @@ describe('services/api:versionValidator', () => { // Given const version = '2.0'; const options = { - errors: { - default: 'Missmatch!', - }, + error: 'Missmatch!', }; const appError = jest.fn(); class AppError { @@ -186,7 +156,7 @@ describe('services/api:versionValidator', () => { expect(next).toHaveBeenCalledTimes(1); expect(next).toHaveBeenCalledWith(expect.any(AppError)); expect(appError).toHaveBeenCalledWith( - options.errors.default, + options.error, { status: statuses.conflict, response: { @@ -222,12 +192,10 @@ describe('services/api:versionValidator', () => { // Given const version = '2.0'; const options = { + error: 'No latest!', latest: { allow: false, }, - errors: { - default: 'No latest!', - }, }; const appError = jest.fn(); class AppError { @@ -253,7 +221,7 @@ describe('services/api:versionValidator', () => { expect(next).toHaveBeenCalledTimes(1); expect(next).toHaveBeenCalledWith(expect.any(AppError)); expect(appError).toHaveBeenCalledWith( - options.errors.default, + options.error, { status: statuses.conflict, response: { From 48bfa0280ce6a1fbba43dbfeb1c6cd73343c8f6d Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 06:19:24 -0300 Subject: [PATCH 09/78] refactor(project): remove old version validator service and controller --- src/controllers/api/index.js | 5 - src/controllers/api/versionValidator.js | 48 ----- src/controllers/index.js | 2 - src/services/api/index.js | 3 - src/services/api/versionValidator.js | 102 ---------- .../controllers/api/versionValidator.test.js | 60 ------ tests/controllers/index.test.js | 1 - tests/services/api/index.test.js | 1 - tests/services/api/versionValidator.test.js | 174 ------------------ 9 files changed, 396 deletions(-) delete mode 100644 src/controllers/api/index.js delete mode 100644 src/controllers/api/versionValidator.js delete mode 100644 src/services/api/versionValidator.js delete mode 100644 tests/controllers/api/versionValidator.test.js delete mode 100644 tests/services/api/versionValidator.test.js diff --git a/src/controllers/api/index.js b/src/controllers/api/index.js deleted file mode 100644 index c085e26d..00000000 --- a/src/controllers/api/index.js +++ /dev/null @@ -1,5 +0,0 @@ -const { versionValidatorController } = require('./versionValidator'); - -module.exports = { - versionValidatorController, -}; diff --git a/src/controllers/api/versionValidator.js b/src/controllers/api/versionValidator.js deleted file mode 100644 index 8a084820..00000000 --- a/src/controllers/api/versionValidator.js +++ /dev/null @@ -1,48 +0,0 @@ -const { controller } = require('../../utils/wrappers'); -/** - * Provides the handler to validate versions on the app routes. - * The reason this controller exists instead of just using the `versionValidator` middlware is - * because as a controller, it can mounted on an specific route, otherwise it would have to be - * implemented on EVERY sub route. - */ -class VersionValidatorController { - /** - * Class constructor. - * @param {ExpressMiddleware} versionValidator The validation middleware. - */ - constructor(versionValidator) { - /** - * A local reference for the `versionValidator` middleware. - * @type {ExpressMiddleware} - */ - this.versionValidator = versionValidator; - } - /** - * It just returns the validation middleware. - * @return {ExpressMiddleware} - */ - validate() { - return this.versionValidator; - } -} -/** - * This controller registers implements the `versionValidator` middlware on all the sub routes of - * the mount point. This way the first path component of all the routes on the mount point should - * match with the app current version. - * @type {Controller} - */ -const versionValidatorController = controller((app) => { - const router = app.get('router'); - const ctrl = new VersionValidatorController( - app.get('versionValidator') - ); - - return [ - router.all('/:version/*', ctrl.validate()), - ]; -}); - -module.exports = { - VersionValidatorController, - versionValidatorController, -}; diff --git a/src/controllers/index.js b/src/controllers/index.js index b6bd80fe..8fbd164c 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -1,7 +1,5 @@ -const api = require('./api'); const common = require('./common'); module.exports = { - api, common, }; diff --git a/src/services/api/index.js b/src/services/api/index.js index 2c4fd076..7961719c 100644 --- a/src/services/api/index.js +++ b/src/services/api/index.js @@ -1,6 +1,5 @@ const { apiClient, apiClientCustom } = require('./client'); const { ensureBearerAuthentication } = require('./ensureBearerAuthentication'); -const { versionValidator } = require('./versionValidator'); const { provider } = require('../../utils/wrappers'); /** * A single service provider that once registered on the app container will take care of @@ -11,13 +10,11 @@ const { provider } = require('../../utils/wrappers'); const all = provider((app) => { app.register(apiClient); app.register(ensureBearerAuthentication); - app.register(versionValidator); }); module.exports = { apiClient, apiClientCustom, ensureBearerAuthentication, - versionValidator, all, }; diff --git a/src/services/api/versionValidator.js b/src/services/api/versionValidator.js deleted file mode 100644 index b96a994c..00000000 --- a/src/services/api/versionValidator.js +++ /dev/null @@ -1,102 +0,0 @@ -const statuses = require('statuses'); -const { provider } = require('../../utils/wrappers'); -/** - * This service provides a middleware to validate a `version` parameter on a route request by - * comparing it with the app version. - * This can be used to prevent frontend applications running with a different version of a Jimpex - * server. - * If the request includes a query string parameter named `popup` with the value of `true`, instead - * of sending an error to the next middleware, it will respond with an HTML post message saying - * `api:conflict`. - * @todo change the post message and make it configurable. - */ -class VersionValidator { - /** - * Class constructor. - * @param {AppConfiguration} appConfiguration To get the app version. - * @param {ResponsesBuilder} responsesBuilder To generate the responses in case the received - * version is invalid. - * @param {Class} AppError To generate the error in case the received version - * is invalid. - */ - constructor(appConfiguration, responsesBuilder, AppError) { - /** - * A local reference for the `appConfiguration` service. - * @type {AppConfiguration} - */ - this.appConfiguration = appConfiguration; - /** - * A local reference for the `responsesBuilder` service. - * @type {ResponsesBuilder} - */ - this.responsesBuilder = responsesBuilder; - /** - * A local reference for the class the app uses to generate errors. - * @type {Class} - */ - this.AppError = AppError; - } - /** - * Returns the Express middleware that validates the `version` parameter on a route. - * @return {ExpressMiddleware} - */ - middleware() { - return (req, res, next) => { - // Get the `version` parameter from the request. - const reqVersion = req.params.version; - // Get the `popup` parameter query string. - const { popup } = req.query; - // Check whether the response should be on an HTML, for a popup, or not. - const isPopup = popup && popup === 'true'; - // If the version matches the one on the configuration, or the value is `latest`... - if ( - reqVersion === 'latest' || - reqVersion === this.appConfiguration.get('version') - ) { - // ...move to the next middleware. - next(); - } else if (isPopup) { - // ...if it doesn't match but it's a popup, then respond with a post message. - this.responsesBuilder.htmlPostMessage( - res, - 'Conflict', - 'api:conflict', - statuses.conflict - ); - } else { - // ...and if it doesn't match but it's not a popup, send an error to the next middleware. - next(new this.AppError( - 'The API version and the client version are different', - { - status: statuses.conflict, - response: { - api: true, - }, - } - )); - } - }; - } -} -/** - * The service provider that once registered on the app container will set the - * `VersionValidator` middleware as the `versionValidator` service. - * @example - * // Register it on the container - * container.register(versionValidator); - * // Getting access to the middleware - * const versionValidator = container.get('versionValidator'); - * @type {Provider} - */ -const versionValidator = provider((app) => { - app.set('versionValidator', () => new VersionValidator( - app.get('appConfiguration'), - app.get('responsesBuilder'), - app.get('appError') - ).middleware()); -}); - -module.exports = { - VersionValidator, - versionValidator, -}; diff --git a/tests/controllers/api/versionValidator.test.js b/tests/controllers/api/versionValidator.test.js deleted file mode 100644 index 77414d9a..00000000 --- a/tests/controllers/api/versionValidator.test.js +++ /dev/null @@ -1,60 +0,0 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); -jest.unmock('/src/utils/wrappers'); -jest.unmock('/src/controllers/api/versionValidator'); - -require('jasmine-expect'); -const { - VersionValidatorController, - versionValidatorController, -} = require('/src/controllers/api/versionValidator'); - -describe('controllers/api:versionValidator', () => { - it('should be instantiated with all its dependencies', () => { - // Given - const versionValidator = 'versionValidator'; - let sut = null; - // When - sut = new VersionValidatorController(versionValidator); - // Then - expect(sut).toBeInstanceOf(VersionValidatorController); - expect(sut.versionValidator).toBe(versionValidator); - }); - - it('should return the versionValidator middleware when `validate` is called', () => { - // Given - const versionValidator = 'versionValidator'; - let sut = null; - let result = null; - // When - sut = new VersionValidatorController(versionValidator); - result = sut.validate(); - // Then - expect(result).toBe(versionValidator); - }); - - it('should include a controller shorthand to return its routes', () => { - // Given - const services = { - router: { - all: jest.fn((route, middleware) => [`all:${route}`, middleware]), - }, - }; - const app = { - get: jest.fn((service) => (services[service] || service)), - }; - let routes = null; - const expectedGets = ['router', 'versionValidator']; - // When - routes = versionValidatorController.connect(app); - // Then - expect(routes).toEqual([ - ['all:/:version/*', 'versionValidator'], - ]); - expect(app.get).toHaveBeenCalledTimes(expectedGets.length); - expectedGets.forEach((service) => { - expect(app.get).toHaveBeenCalledWith(service); - }); - }); -}); diff --git a/tests/controllers/index.test.js b/tests/controllers/index.test.js index 082bd332..964bcba1 100644 --- a/tests/controllers/index.test.js +++ b/tests/controllers/index.test.js @@ -13,7 +13,6 @@ describe('controllers', () => { it('should export all the app controllers', () => { // Given const knownControllers = [ - 'api', 'common', ]; // When/Then diff --git a/tests/services/api/index.test.js b/tests/services/api/index.test.js index 801b7a97..ac312b1f 100644 --- a/tests/services/api/index.test.js +++ b/tests/services/api/index.test.js @@ -16,7 +16,6 @@ describe('services:api', () => { const expectedServices = [ 'apiClient', 'ensureBearerAuthentication', - 'versionValidator', ]; // When apiServices.all(app); diff --git a/tests/services/api/versionValidator.test.js b/tests/services/api/versionValidator.test.js deleted file mode 100644 index 3605fbc9..00000000 --- a/tests/services/api/versionValidator.test.js +++ /dev/null @@ -1,174 +0,0 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); -jest.unmock('/src/utils/wrappers'); -jest.unmock('/src/services/api/versionValidator'); - -require('jasmine-expect'); -const statuses = require('statuses'); -const { - VersionValidator, - versionValidator, -} = require('/src/services/api/versionValidator'); - -describe('services/api:versionValidator', () => { - it('should be instantiated with all its dependencies', () => { - // Given - const appConfiguration = 'appConfiguration'; - const responsesBuilder = 'responsesBuilder'; - const AppError = 'AppError'; - let sut = null; - // When - sut = new VersionValidator(appConfiguration, responsesBuilder, AppError); - // Then - expect(sut).toBeInstanceOf(VersionValidator); - expect(sut.appConfiguration).toBe(appConfiguration); - expect(sut.responsesBuilder).toBe(responsesBuilder); - expect(sut.AppError).toBe(AppError); - }); - - it('should have a middleware to validate a version on the route', () => { - // Given - const version = 'latest'; - const request = { - params: { - version, - }, - query: {}, - }; - const response = 'response'; - const next = jest.fn(); - let sut = null; - let middleware = null; - // When - sut = new VersionValidator('appConfiguration', 'responsesBuilder', 'AppError'); - middleware = sut.middleware(); - middleware(request, response, next); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); - }); - - it('should validate if the version is the same as the one on the configuration', () => { - // Given - const version = '25092015'; - const appConfiguration = { - get: jest.fn(() => version), - }; - const request = { - params: { - version, - }, - query: {}, - }; - const response = 'response'; - const next = jest.fn(); - let sut = null; - let middleware = null; - // When - sut = new VersionValidator(appConfiguration, 'responsesBuilder', 'AppError'); - middleware = sut.middleware(); - middleware(request, response, next); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); - }); - - it('should send an error if the version is not valid', () => { - // Given - const appError = jest.fn(); - class AppError { - constructor(...args) { - appError(...args); - } - } - const version = '25092015'; - const appConfiguration = { - get: jest.fn(), - }; - const request = { - params: { - version, - }, - query: {}, - }; - const response = 'response'; - const next = jest.fn(); - let sut = null; - let middleware = null; - // When - sut = new VersionValidator(appConfiguration, 'responsesBuilder', AppError); - middleware = sut.middleware(); - middleware(request, response, next); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.any(AppError)); - expect(appError).toHaveBeenCalledTimes(1); - expect(appError).toHaveBeenCalledWith( - 'The API version and the client version are different', - { - status: statuses.conflict, - response: { - api: true, - }, - } - ); - }); - - it('should send an HTML post message if the version is the route identifies as a popup', () => { - // Given - const version = '25092015'; - const appConfiguration = { - get: jest.fn(), - }; - const responsesBuilder = { - htmlPostMessage: jest.fn(), - }; - const request = { - params: { - version, - }, - query: { - popup: 'true', - }, - }; - const response = 'response'; - const next = jest.fn(); - let sut = null; - let middleware = null; - // When - sut = new VersionValidator(appConfiguration, responsesBuilder, 'AppError'); - middleware = sut.middleware(); - middleware(request, response, next); - expect(next).toHaveBeenCalledTimes(0); - expect(responsesBuilder.htmlPostMessage).toHaveBeenCalledTimes(1); - expect(responsesBuilder.htmlPostMessage).toHaveBeenCalledWith( - response, - 'Conflict', - 'api:conflict', - statuses.conflict - ); - }); - - it('should include a provider for the DIC', () => { - // Given - const services = {}; - const app = { - set: jest.fn(), - get: jest.fn((service) => (services[service] || service)), - }; - let sut = null; - let serviceName = null; - let serviceFn = null; - let toCompare = null; - // When - toCompare = new VersionValidator( - 'appConfiguration', - 'responsesBuilder', - 'AppError' - ); - versionValidator(app); - [[serviceName, serviceFn]] = app.set.mock.calls; - sut = serviceFn(); - // Then - expect(serviceName).toBe('versionValidator'); - expect(sut.toString()).toBe(toCompare.middleware().toString()); - }); -}); From 661984d7bd4be43c269de3fc4bd4b0ff291144ae Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 06:20:19 -0300 Subject: [PATCH 10/78] docs(project): update the documentation regarding the version validator --- README-esdoc.md | 3 +-- README.md | 3 +-- documents/controllers.md | 35 ------------------------ documents/middlewares.md | 57 +++++++++++++++++++++++++++++++++++++++- documents/services.md | 47 --------------------------------- 5 files changed, 58 insertions(+), 87 deletions(-) diff --git a/README-esdoc.md b/README-esdoc.md index f8abd5fe..926db63c 100644 --- a/README-esdoc.md +++ b/README-esdoc.md @@ -232,7 +232,6 @@ Jimpex comes with a few services, middlewares and controllers that you can impor ### Controllers -- **Version validator:** If you mount it on a route it will generate a `409` error if the request doesn't have a version parameter with the same version as the one on the configuration file. - **Configuration:** Allows you to see and switch the current configuration. It can be enabled or disabled by using a setting on the configuration. - **Health:** Shows the version and name of the configuration, just to check the app is running. - **Root statics:** It allows your app to server static files from the root directory, without having to use the `static` middleware on that directory. @@ -245,6 +244,7 @@ Jimpex comes with a few services, middlewares and controllers that you can impor - **Force HTTPS:** Redirect all incoming traffic from HTTP to HTTPS. It also allows you to set routes to ignore the redirection. - **Fast HTML:** Allows you to specify which routes will be handled and in case there are no controllers for a requested route, it sends out and HTML file, thus preventing the request to be unnecessarily processed by the middlewares. - **Show HTML:** A really simple middleware to serve an HTML file. Its true feature is that it can be hooked up to the **HTML Generator** service. +- **Version validator:** If you mount it on a route it will generate a `409` error if the request doesn't have a version parameter with the same version as the one on the configuration. [Read more about the built-in controllers](manual/middlewares.html) @@ -252,7 +252,6 @@ Jimpex comes with a few services, middlewares and controllers that you can impor - **API client:** An implementation of the [wootils API Client](https://github.com/homer0/wootils/blob/master/documents/shared/APIClient.md) but that is connected to the HTTP service, to allow logging and forwarding of the headers. - **Ensure bearer authentication:** A service-middleware that allows you to validate the incoming requests `Authorization` header. -- **Version validator:** A service-middleware to validate a `version` parameter against the configuration `version` setting. It's what the version validator middleware internally uses. - **Error:** A very simple subclass of `Error` to inject extra information on the errors so they can customize the error handler responses. - **Send File:** It allows you to send a file on a response with a path relative to the app executable. - **Frontend Fs:** Useful for when your app has a bundled frontend, it allows you to read, write and delete files with paths relative to the app executable. diff --git a/README.md b/README.md index 42020a3a..aae85a2e 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,6 @@ Jimpex comes with a few services, middlewares and controllers that you can impor ### Controllers -- **Version validator:** If you mount it on a route it will generate a `409` error if the request doesn't have a version parameter with the same version as the one on the configuration file. - **Configuration:** Allows you to see and switch the current configuration. It can be enabled or disabled by using a setting on the configuration. - **Health:** Shows the version and name of the configuration, just to check the app is running. - **Root statics:** It allows your app to server static files from the root directory, without having to use the `static` middleware on that directory. @@ -245,6 +244,7 @@ Jimpex comes with a few services, middlewares and controllers that you can impor - **Force HTTPS:** Redirect all incoming traffic from HTTP to HTTPS. It also allows you to set routes to ignore the redirection. - **Fast HTML:** Allows you to specify which routes will be handled and in case there are no controllers for a requested route, it sends out and HTML file, thus preventing the request to be unnecessarily processed by the middlewares. - **Show HTML:** A really simple middleware to serve an HTML file. Its true feature is that it can be hooked up to the **HTML Generator** service. +- **Version validator:** If you mount it on a route it will generate a `409` error if the request doesn't have a version parameter with the same version as the one on the configuration. [Read more about the built-in controllers](./documents/middlewares.md) @@ -252,7 +252,6 @@ Jimpex comes with a few services, middlewares and controllers that you can impor - **API client:** An implementation of the [wootils API Client](https://github.com/homer0/wootils/blob/master/documents/shared/APIClient.md) but that is connected to the HTTP service, to allow logging and forwarding of the headers. - **Ensure bearer authentication:** A service-middleware that allows you to validate the incoming requests `Authorization` header. -- **Version validator:** A service-middleware to validate a `version` parameter against the configuration `version` setting. It's what the version validator middleware internally uses. - **Error:** A very simple subclass of `Error` to inject extra information on the errors so they can customize the error handler responses. - **Send File:** It allows you to send a file on a response with a path relative to the app executable. - **Frontend Fs:** Useful for when your app has a bundled frontend, it allows you to read, write and delete files with paths relative to the app executable. diff --git a/documents/controllers.md b/documents/controllers.md index 1ece5994..35b900b9 100644 --- a/documents/controllers.md +++ b/documents/controllers.md @@ -2,41 +2,6 @@ All of these controllers are available on the Jimpex package and can be easily required and implemented. -## Version validator - -If you mount it on a route it will generate a `409` error if the request doesn't have a version parameter with the same version as the one on the configuration file. - -- Module: `api` -- Requires: `versionValidator` - -```js -const { - Jimpex, - services: { - api: { versionValidator }, - }, - controllers: { - api: { versionValidatorController }, - }, -}; - -class App extends Jimpex { - boot() { - // Register the dependencies... - this.register(versionValidator); - - // Add the controller. - this.mount('/api', versionValidatorController); - } -} -``` - -The controller will mount one route: - -- `* /:version/*`: To validate and protect any sub route. - -You can mount other routes on `/api/:version/...` and they'll be _"protected"_ by the version check. - ## Configuration Allows you to see and switch the current configuration. It can be enabled or disabled by using a setting on the configuration. diff --git a/documents/middlewares.md b/documents/middlewares.md index 13970a1c..a4d6b4d5 100644 --- a/documents/middlewares.md +++ b/documents/middlewares.md @@ -212,4 +212,59 @@ class App extends Jimpex { Now, as mentioned on the requirements, you can optionally use the `htmlGenerator` or an `HTMLGenerator` service to show the generated file. -The default implementation checks if there's an `htmlGenerator` service registered on the app and uses that file; and in the case of `showHTMLCustom `, you can specify a second parameter with the name of the `HTMLGenerator` service name you want to use. \ No newline at end of file +The default implementation checks if there's an `htmlGenerator` service registered on the app and uses that file; and in the case of `showHTMLCustom `, you can specify a second parameter with the name of the `HTMLGenerator` service name you want to use. + +## Version validator + +This can be used as a middleware and as controller. The idea is that it validates a `version` parameter against the version defined on the configuration. + +- Module: `utils` +- Requires: `appConfiguration`, `responsesBuilder` and `appError` + +```js +const { + Jimpex, + middlewares: { + utils: { versionValidator }, + }, +}; + +class App extends Jimpex { + boot() { + // Add the middleware before the routes you want to be protected. + this.use(versionValidator); + // or, protect a specific route. + this.mount('/to-protect', versionValidator); + } +} +``` + +By default, it comes with a lot of already defined options, like whether or not to allow `latest` as a version, but you can use the _"middleware generator"_ `versionValidatorCustom` to modify them, for example: + +```js +const { + Jimpex, + middlewares: { + utils: { versionValidatorCustom }, + }, +}; + +class App extends Jimpex { + boot() { + // Add the middleware before the routes you want to be protected. + this.use(versionValidatorCustom({ + latest: { + allow: false, + } + })); + // or, protect a specific route. + this.mount('/to-protect', versionValidatorCustom({ + latest: { + allow: false, + } + })); + } +} +``` + +**Very important:** The middleware will only validate if `req.params.version` is found. \ No newline at end of file diff --git a/documents/services.md b/documents/services.md index b47da8ad..05925e2a 100644 --- a/documents/services.md +++ b/documents/services.md @@ -107,53 +107,6 @@ const myCtrl = controller((app) => { }); ``` -## Version validator - -A service-middleware to validate a `version` parameter against the configuration `version` setting. It's what the version validator middleware internally uses. - -It's a _"service-middleware"_ because when you access the service, it doesn't return a class instance, but a middleware function for you to use on your controller routes. - -- Module: `api` -- Requires: `responsesBuilder` and `appError` - -```js -const { - Jimpex, - services: { - api: { versionValidator }, - common: { appError }, - http: { responsesBuilder }, - }, -}; - -class App extends Jimpex { - boot() { - // Register the dependencies... - this.register(appError); - this.register(responsesBuilder); - - // Register the service - this.register(versionValidator); - } -} -``` - -Now you can use it on your controllers routes to validate that the version being used is the same as the one the app is running on: - -```js -const myCtrl = controller((app) => { - const router = app.get('router'); - const versionValidator = app.get('versionValidator'); - return [router.get('/:version/something', [ - versionValidator, - (req, res, next) => { - console.log('The version is valid!'); - next(); - }, - ])]; -}); -``` - ## Error A very simple subclass of `Error` to inject extra information on the errors so they can customize the error handler responses. From 7ed67e52d07cfe32ea3dfc21d7c8fc891d3b21a8 Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 07:15:27 -0300 Subject: [PATCH 11/78] feat(project): add new error classes --- src/services/common/appError.js | 39 +++++++++ src/services/common/httpError.js | 30 +++++++ src/services/common/index.js | 5 +- tests/services/common/appError.test.js | 111 ++++++++++++++++++++++++ tests/services/common/httpError.test.js | 103 ++++++++++++++++++++++ tests/services/common/index.test.js | 1 + 6 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 src/services/common/appError.js create mode 100644 src/services/common/httpError.js create mode 100644 tests/services/common/appError.test.js create mode 100644 tests/services/common/httpError.test.js diff --git a/src/services/common/appError.js b/src/services/common/appError.js new file mode 100644 index 00000000..fa70ba53 --- /dev/null +++ b/src/services/common/appError.js @@ -0,0 +1,39 @@ +const { provider } = require('../../utils/wrappers'); + +class AppError extends Error { + constructor(message, context = {}) { + super(message); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + + this.name = this.constructor.name; + this._context = context; + this._date = new Date(); + } + + get context() { + return this._context; + } + + get date() { + return this._date; + } + + get response() { + return this._context.response || {}; + } +} + +const appErrorGenerator = (message, context) => new AppError(message, context); + +const appError = provider((app) => { + app.set('AppError', () => AppError); + app.set('appError', () => appErrorGenerator); +}); + +module.exports = { + AppError, + appError, +}; diff --git a/src/services/common/httpError.js b/src/services/common/httpError.js new file mode 100644 index 00000000..f9b3ac16 --- /dev/null +++ b/src/services/common/httpError.js @@ -0,0 +1,30 @@ +const statuses = require('statuses'); +const ObjectUtils = require('wootils/shared/objectUtils'); +const { provider } = require('../../utils/wrappers'); +const { AppError } = require('./appError'); + +class HTTPError extends AppError { + constructor(message, status = statuses.ok, context = {}) { + super(message, ObjectUtils.merge({ status }, context)); + } + + get status() { + return this.context.status; + } +} + +const httpErrorGenerator = (message, status, context) => new HTTPError( + message, + status, + context +); + +const httpError = provider((app) => { + app.set('HTTPError', () => HTTPError); + app.set('httpError', () => httpErrorGenerator); +}); + +module.exports = { + HTTPError, + httpError, +}; diff --git a/src/services/common/index.js b/src/services/common/index.js index 169e0556..f0a9b250 100644 --- a/src/services/common/index.js +++ b/src/services/common/index.js @@ -1,4 +1,5 @@ -const { appError } = require('./error'); +const { appError } = require('./appError'); +const { httpError } = require('./httpError'); const { sendFileProvider } = require('./sendFile'); const { provider } = require('../../utils/wrappers'); /** @@ -8,11 +9,13 @@ const { provider } = require('../../utils/wrappers'); */ const all = provider((app) => { app.register(appError); + app.register(httpError); app.register(sendFileProvider); }); module.exports = { appError, + httpError, sendFile: sendFileProvider, all, }; diff --git a/tests/services/common/appError.test.js b/tests/services/common/appError.test.js new file mode 100644 index 00000000..5eb54452 --- /dev/null +++ b/tests/services/common/appError.test.js @@ -0,0 +1,111 @@ +const JimpleMock = require('/tests/mocks/jimple.mock'); + +jest.mock('jimple', () => JimpleMock); +jest.unmock('/src/utils/wrappers'); +jest.unmock('/src/services/common/appError'); + +require('jasmine-expect'); +const { + AppError, + appError, +} = require('/src/services/common/appError'); + +const originalCaptureStackTrace = Error.captureStackTrace; + +describe('services/common:appError', () => { + afterEach(() => { + Error.captureStackTrace = originalCaptureStackTrace; + }); + + it('should be instantiated', () => { + // Given + const message = 'Something went wrong!'; + let sut = null; + // When + sut = new AppError(message); + // Then + expect(sut).toBeInstanceOf(AppError); + expect(sut).toBeInstanceOf(Error); + expect(sut.message).toBe(message); + expect(sut.date).toBeInstanceOf(Date); + expect(sut.response).toEqual({}); + }); + + it('should be instantiated with context information', () => { + // Given + const message = 'Something went wrong!'; + const context = { + age: 3, + name: 'Rosario', + response: 'Something in case a response needs to be generated', + }; + let sut = null; + // When + sut = new AppError(message, context); + // Then + expect(sut).toBeInstanceOf(AppError); + expect(sut.message).toBe(message); + expect(sut.context).toEqual(context); + expect(sut.response).toEqual(context.response); + }); + + it('should use `captureStackTrace` when avaiable', () => { + // Given + const captureStackTrace = jest.fn(); + Error.captureStackTrace = captureStackTrace; + let sut = null; + // When + sut = new AppError('With stack trace'); + Error.captureStackTrace = null; + // eslint-disable-next-line no-new + new AppError('Without stack trace'); + // Then + expect(captureStackTrace).toHaveBeenCalledTimes(1); + expect(captureStackTrace).toHaveBeenCalledWith(sut, sut.constructor); + }); + + describe('DIC provider', () => { + it('should register the class as a service', () => { + // Given + const app = { + set: jest.fn(), + }; + let sut = null; + let serviceName = null; + let serviceFn = null; + // When + appError(app); + [[serviceName, serviceFn]] = app.set.mock.calls; + sut = serviceFn(); + // Then + expect(serviceName).toBe('AppError'); + expect(sut).toBe(AppError); + }); + + it('should register a shorthand generator', () => { + // Given + const app = { + set: jest.fn(), + }; + const message = 'Something went wrong!'; + const context = { + name: 'Charo', + }; + let sut = null; + let serviceName = null; + let serviceFn = null; + let result = null; + // When + appError(app); + [, [serviceName, serviceFn]] = app.set.mock.calls; + sut = serviceFn(); + result = sut(message, context); + // Then + expect(serviceName).toBe('appError'); + expect(sut).toBeFunction(); + expect(result).toBeInstanceOf(AppError); + expect(result.message).toBe(message); + expect(result.context).toEqual(context); + }); + }); +}); diff --git a/tests/services/common/httpError.test.js b/tests/services/common/httpError.test.js new file mode 100644 index 00000000..d8102336 --- /dev/null +++ b/tests/services/common/httpError.test.js @@ -0,0 +1,103 @@ +const statuses = require('statuses'); +const JimpleMock = require('/tests/mocks/jimple.mock'); + +jest.mock('jimple', () => JimpleMock); +jest.unmock('/src/utils/wrappers'); +jest.unmock('/src/services/common/appError'); +jest.unmock('/src/services/common/httpError'); + +require('jasmine-expect'); +const { AppError } = require('/src/services/common/appError'); +const { + HTTPError, + httpError, +} = require('/src/services/common/httpError'); + +describe('services/common:httpError', () => { + it('should be instantiated', () => { + // Given + const message = 'Something went wrong with a request!'; + let sut = null; + // When + sut = new HTTPError(message); + // Then + expect(sut).toBeInstanceOf(HTTPError); + expect(sut).toBeInstanceOf(AppError); + expect(sut).toBeInstanceOf(Error); + expect(sut.message).toBe(message); + expect(sut.status).toBe(statuses.ok); + expect(sut.date).toBeInstanceOf(Date); + }); + + it('should be instantiated with a custom status', () => { + // Given + const message = 'Something went wrong with a request!'; + let sut = null; + // When + sut = new HTTPError(message, statuses.conflict); + // Then + expect(sut).toBeInstanceOf(HTTPError); + expect(sut.message).toBe(message); + expect(sut.status).toEqual(statuses.conflict); + }); + + it('should be instantiated with context information', () => { + // Given + const message = 'Something went wrong with a request!'; + const status = statuses['bad request']; + const context = { + age: 3, + name: 'Rosario', + }; + let sut = null; + // When + sut = new HTTPError(message, status, context); + // Then + expect(sut).toBeInstanceOf(HTTPError); + expect(sut.message).toBe(message); + expect(sut.status).toBe(status); + expect(sut.context).toEqual(Object.assign({ status }, context)); + }); + + describe('DIC provider', () => { + it('should register the class as a service', () => { + // Given + const app = { + set: jest.fn(), + }; + let sut = null; + let serviceName = null; + let serviceFn = null; + // When + httpError(app); + [[serviceName, serviceFn]] = app.set.mock.calls; + sut = serviceFn(); + // Then + expect(serviceName).toBe('HTTPError'); + expect(sut).toBe(HTTPError); + }); + + it('should register a shorthand generator', () => { + // Given + const app = { + set: jest.fn(), + }; + const message = 'Something went wrong with a request!'; + let sut = null; + let serviceName = null; + let serviceFn = null; + let result = null; + // When + httpError(app); + [, [serviceName, serviceFn]] = app.set.mock.calls; + sut = serviceFn(); + result = sut(message, statuses.conflict); + // Then + expect(serviceName).toBe('httpError'); + expect(sut).toBeFunction(); + expect(result).toBeInstanceOf(HTTPError); + expect(result.message).toBe(message); + expect(result.status).toEqual(statuses.conflict); + }); + }); +}); diff --git a/tests/services/common/index.test.js b/tests/services/common/index.test.js index ba14c99a..3c7e6a5d 100644 --- a/tests/services/common/index.test.js +++ b/tests/services/common/index.test.js @@ -15,6 +15,7 @@ describe('services:common', () => { }; const expectedServices = [ 'appError', + 'httpError', 'sendFile', ]; // When From 80bcd63b5941e360aa4cb194b2a3259f86b98344 Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 07:23:26 -0300 Subject: [PATCH 12/78] refactor(project): use the new AppError injection --- src/middlewares/common/errorHandler.js | 8 +++----- src/middlewares/utils/versionValidator.js | 2 +- src/services/api/client.js | 2 +- src/services/api/ensureBearerAuthentication.js | 2 +- tests/middlewares/common/errorHandler.test.js | 12 +++++------- tests/middlewares/utils/versionValidator.test.js | 4 ++-- tests/services/api/client.test.js | 2 +- 7 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/middlewares/common/errorHandler.js b/src/middlewares/common/errorHandler.js index f3e939fa..a8656367 100644 --- a/src/middlewares/common/errorHandler.js +++ b/src/middlewares/common/errorHandler.js @@ -60,11 +60,9 @@ class ErrorHandler { // If the error type is known... if (knownError) { // Try to get any extra information that should be included on the response. - if (err.extras && err.extras.response) { - data = Object.assign(data, err.extras.response); - } + data = Object.assign(data, err.response); // Try to obtain the response status from the error - status = (err.extras && err.extras.status) || statuses['Bad Request']; + status = err.status || statuses['Bad Request']; } // If the `showErrors` flag is enabled... if (this.showErrors) { @@ -99,7 +97,7 @@ const errorHandler = middleware((app) => { app.get('appLogger'), app.get('responsesBuilder'), showErrors, - app.get('appError') + app.get('AppError') ) .middleware(); }); diff --git a/src/middlewares/utils/versionValidator.js b/src/middlewares/utils/versionValidator.js index 067aca70..5e857a0f 100644 --- a/src/middlewares/utils/versionValidator.js +++ b/src/middlewares/utils/versionValidator.js @@ -208,7 +208,7 @@ const versionValidatorCustom = (options) => middleware((app, point) => { const middlewareValidator = (new VersionValidator( app.get('appConfiguration').get('version'), app.get('responsesBuilder'), - app.get('appError'), + app.get('AppError'), options )).middleware(); // Set the variable to be returned. diff --git a/src/services/api/client.js b/src/services/api/client.js index 226be204..86562fd8 100644 --- a/src/services/api/client.js +++ b/src/services/api/client.js @@ -62,7 +62,7 @@ const apiClientCustom = ( app.set(name, () => new ClientClass( app.get('appConfiguration').get(configurationKey), app.get('http'), - app.get('appError') + app.get('AppError') )); }); /** diff --git a/src/services/api/ensureBearerAuthentication.js b/src/services/api/ensureBearerAuthentication.js index 0b388dcc..fabd0693 100644 --- a/src/services/api/ensureBearerAuthentication.js +++ b/src/services/api/ensureBearerAuthentication.js @@ -60,7 +60,7 @@ class EnsureBearerAuthentication { */ const ensureBearerAuthentication = provider((app) => { app.set('ensureBearerAuthentication', () => new EnsureBearerAuthentication( - app.get('appError') + app.get('AppError') ).middleware()); }); diff --git a/tests/middlewares/common/errorHandler.test.js b/tests/middlewares/common/errorHandler.test.js index bbf4efc6..cf64a5bb 100644 --- a/tests/middlewares/common/errorHandler.test.js +++ b/tests/middlewares/common/errorHandler.test.js @@ -139,7 +139,7 @@ describe('middlewares/common:errorHandler', () => { expect(appLogger.info).toHaveBeenCalledWith(expectedData.stack); }); - it('should show extra information the AppError object may contain', () => { + it('should show context information the AppError object may contain', () => { // Given const appLogger = { error: jest.fn(), @@ -152,10 +152,8 @@ describe('middlewares/common:errorHandler', () => { class AppError { constructor(message, stack, status, response) { this.message = message; - this.extras = { - status, - response, - }; + this.status = status; + this.response = response; this.stack = `${message} ${stack}`; } @@ -242,7 +240,7 @@ describe('middlewares/common:errorHandler', () => { 'appConfiguration', 'appLogger', 'responsesBuilder', - 'appError', + 'AppError', ]; // When middleware = errorHandler.connect(app); @@ -250,7 +248,7 @@ describe('middlewares/common:errorHandler', () => { 'appLogger', 'responsesBuilder', 'showErrors', - 'appError' + 'AppError' ); // Then expect(middleware.toString()).toEqual(toCompare.middleware().toString()); diff --git a/tests/middlewares/utils/versionValidator.test.js b/tests/middlewares/utils/versionValidator.test.js index 86d401bc..4e473caa 100644 --- a/tests/middlewares/utils/versionValidator.test.js +++ b/tests/middlewares/utils/versionValidator.test.js @@ -295,7 +295,7 @@ describe('services/api:versionValidator', () => { const expectedServices = [ 'appConfiguration', 'responsesBuilder', - 'appError', + 'AppError', ]; // When sut = versionValidatorCustom().connect(app); @@ -331,7 +331,7 @@ describe('services/api:versionValidator', () => { const expectedServices = [ 'appConfiguration', 'responsesBuilder', - 'appError', + 'AppError', 'router', ]; // When diff --git a/tests/services/api/client.test.js b/tests/services/api/client.test.js index 1c196cbd..e679e357 100644 --- a/tests/services/api/client.test.js +++ b/tests/services/api/client.test.js @@ -108,7 +108,7 @@ describe('services/api:client', () => { expect(sut.apiConfig).toBe(appConfiguration.apiConfig); expect(sut.url).toBe(appConfiguration.apiConfig.url); expect(sut.endpoints).toEqual(appConfiguration.apiConfig.endpoints); - expect(sut.AppError).toBe('appError'); + expect(sut.AppError).toBe('AppError'); expect(sut.fetchClient).toBe(http.fetch); expect(serviceName).toBe(name); expect(appConfiguration.get).toHaveBeenCalledTimes(1); From b978886f73499c3f864dff11f68eb6b65ac2de69 Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 07:27:17 -0300 Subject: [PATCH 13/78] refactor(services/api/client): use the new HTTPError --- src/services/api/client.js | 14 +++++++------- tests/services/api/client.test.js | 22 +++++++++++----------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/services/api/client.js b/src/services/api/client.js index 86562fd8..d7a2c1e6 100644 --- a/src/services/api/client.js +++ b/src/services/api/client.js @@ -16,9 +16,9 @@ class APIClient extends APIClientBase { * the API entry point. * @param {HTTP} http To get the `fetch` function for this service * to use on all the requests. - * @param {Class} AppError To format the received errors. + * @param {Class} HTTPError To format the received errors. */ - constructor(apiConfig, http, AppError) { + constructor(apiConfig, http, HTTPError) { super(apiConfig.url, apiConfig.endpoints, http.fetch); /** * The configuration for the API the client will make requests to. @@ -29,19 +29,19 @@ class APIClient extends APIClientBase { */ this.apiConfig = apiConfig; /** - * A local reference for the class the app uses to generate errors. + * A local reference for the class the app uses to generate HTTP errors. * @type {Class} */ - this.AppError = AppError; + this.HTTPError = HTTPError; } /** * Formats a response error with the App error class. * @param {Object} response A received response from a request. * @param {number} status The HTTP status of the request. - * @return {Error} + * @return {HTTPError} */ error(response, status) { - return new this.AppError(response.data.message, { status }); + return new this.HTTPError(response.data.message, status); } } /** @@ -62,7 +62,7 @@ const apiClientCustom = ( app.set(name, () => new ClientClass( app.get('appConfiguration').get(configurationKey), app.get('http'), - app.get('AppError') + app.get('HTTPError') )); }); /** diff --git a/tests/services/api/client.test.js b/tests/services/api/client.test.js index e679e357..851625a2 100644 --- a/tests/services/api/client.test.js +++ b/tests/services/api/client.test.js @@ -23,20 +23,20 @@ describe('services/api:client', () => { const http = { fetch: () => {}, }; - const AppError = Error; + const HTTPError = Error; let sut = null; // When - sut = new APIClient(apiConfig, http, AppError); + sut = new APIClient(apiConfig, http, HTTPError); // Then expect(sut).toBeInstanceOf(APIClientBase); expect(sut).toBeInstanceOf(APIClient); expect(sut.apiConfig).toBe(apiConfig); expect(sut.url).toBe(apiConfig.url); expect(sut.endpoints).toEqual(apiConfig.endpoints); - expect(sut.AppError).toBe(AppError); + expect(sut.HTTPError).toBe(HTTPError); }); - it('should format error responses using the AppError service', () => { + it('should format error responses using the HTTPError service', () => { // Given const apiConfig = { url: 'my-api', @@ -47,10 +47,10 @@ describe('services/api:client', () => { const http = { fetch: () => {}, }; - class AppError { - constructor(message, extras) { + class HTTPError { + constructor(message, status) { this.message = message; - this.extras = extras; + this.status = status; } } const response = { @@ -62,12 +62,12 @@ describe('services/api:client', () => { let sut = null; let result = null; // When - sut = new APIClient(apiConfig, http, AppError); + sut = new APIClient(apiConfig, http, HTTPError); result = sut.error(response, status); // Then - expect(result).toBeInstanceOf(AppError); + expect(result).toBeInstanceOf(HTTPError); expect(result.message).toBe(response.data.message); - expect(result.extras).toEqual({ status }); + expect(result.status).toBe(status); }); it('should include a provider for the DIC', () => { @@ -108,7 +108,7 @@ describe('services/api:client', () => { expect(sut.apiConfig).toBe(appConfiguration.apiConfig); expect(sut.url).toBe(appConfiguration.apiConfig.url); expect(sut.endpoints).toEqual(appConfiguration.apiConfig.endpoints); - expect(sut.AppError).toBe('AppError'); + expect(sut.HTTPError).toBe('HTTPError'); expect(sut.fetchClient).toBe(http.fetch); expect(serviceName).toBe(name); expect(appConfiguration.get).toHaveBeenCalledTimes(1); From ef3efc7f79775c757af107a17f79138380d41ff4 Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 07:30:49 -0300 Subject: [PATCH 14/78] refactor(services/common): move the status getter to the main error class --- src/services/common/appError.js | 4 ++++ src/services/common/httpError.js | 4 ---- tests/services/common/appError.test.js | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/services/common/appError.js b/src/services/common/appError.js index fa70ba53..333040b2 100644 --- a/src/services/common/appError.js +++ b/src/services/common/appError.js @@ -24,6 +24,10 @@ class AppError extends Error { get response() { return this._context.response || {}; } + + get status() { + return this._context.status || null; + } } const appErrorGenerator = (message, context) => new AppError(message, context); diff --git a/src/services/common/httpError.js b/src/services/common/httpError.js index f9b3ac16..ff8c9a12 100644 --- a/src/services/common/httpError.js +++ b/src/services/common/httpError.js @@ -7,10 +7,6 @@ class HTTPError extends AppError { constructor(message, status = statuses.ok, context = {}) { super(message, ObjectUtils.merge({ status }, context)); } - - get status() { - return this.context.status; - } } const httpErrorGenerator = (message, status, context) => new HTTPError( diff --git a/tests/services/common/appError.test.js b/tests/services/common/appError.test.js index 5eb54452..921c5dea 100644 --- a/tests/services/common/appError.test.js +++ b/tests/services/common/appError.test.js @@ -29,6 +29,7 @@ describe('services/common:appError', () => { expect(sut.message).toBe(message); expect(sut.date).toBeInstanceOf(Date); expect(sut.response).toEqual({}); + expect(sut.status).toBeNull(); }); it('should be instantiated with context information', () => { @@ -38,6 +39,7 @@ describe('services/common:appError', () => { age: 3, name: 'Rosario', response: 'Something in case a response needs to be generated', + status: 500, }; let sut = null; // When @@ -47,6 +49,7 @@ describe('services/common:appError', () => { expect(sut.message).toBe(message); expect(sut.context).toEqual(context); expect(sut.response).toEqual(context.response); + expect(sut.status).toEqual(context.status); }); it('should use `captureStackTrace` when avaiable', () => { From 587c487ee592ed7799dc70a3ddf83a569311ebb4 Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 07:32:47 -0300 Subject: [PATCH 15/78] refactor(services/common): delete old error class --- src/services/common/error.js | 45 ----------------------- tests/services/common/error.test.js | 57 ----------------------------- 2 files changed, 102 deletions(-) delete mode 100644 src/services/common/error.js delete mode 100644 tests/services/common/error.test.js diff --git a/src/services/common/error.js b/src/services/common/error.js deleted file mode 100644 index 4f9febf8..00000000 --- a/src/services/common/error.js +++ /dev/null @@ -1,45 +0,0 @@ -const { provider } = require('../../utils/wrappers'); -/** - * A simple subclass of `Error` that supports extra properties. - * @extends {Error} - */ -class AppError extends Error { - /** - * Class constructor. - * @param {string} message The error message. - * @param {Object} [extras={}] Extra properties for the app. This can be used to send and HTTP - * status when the error is generated by a request, or any other - * context information the app can use. - */ - constructor(message, extras = {}) { - super(message); - /** - * Overwrite the name of the `Error` with the one from the class. - * @ignore - */ - this.name = this.constructor.name; - /** - * The extra properties sent on the constructor. - * @type {Object} - */ - this.extras = extras; - } -} -/** - * A service provider that instead of returning an instance of a service, returns the class itself. - * Once registered, it will set `AppError` as a service with the same name. - * @example - * // Register it on the container - * container.register(appError); - * // Getting access to the class. - * const AppError = container.get('AppError'); - * @type {Provider} - */ -const appError = provider((app) => { - app.set('appError', () => AppError); -}); - -module.exports = { - AppError, - appError, -}; diff --git a/tests/services/common/error.test.js b/tests/services/common/error.test.js deleted file mode 100644 index 3e74e4ea..00000000 --- a/tests/services/common/error.test.js +++ /dev/null @@ -1,57 +0,0 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); -jest.unmock('/src/utils/wrappers'); -jest.unmock('/src/services/common/error'); - -require('jasmine-expect'); -const { - AppError, - appError, -} = require('/src/services/common/error'); - -describe('services/common:error', () => { - it('should be instantiated', () => { - // Given - const message = 'Something went wrong!'; - let sut = null; - // When - sut = new AppError(message); - // Then - expect(sut).toBeInstanceOf(AppError); - expect(sut.message).toBe(message); - }); - - it('should be instantiated with custom properties', () => { - // Given - const message = 'Something went wrong!'; - const customProperties = { - status: 500, - api: true, - }; - let sut = null; - // When - sut = new AppError(message, customProperties); - // Then - expect(sut).toBeInstanceOf(AppError); - expect(sut.message).toBe(message); - expect(sut.extras).toEqual(customProperties); - }); - - it('should include a provider for the DIC', () => { - // Given - const app = { - set: jest.fn(), - }; - let sut = null; - let serviceName = null; - let serviceFn = null; - // When - appError(app); - [[serviceName, serviceFn]] = app.set.mock.calls; - sut = serviceFn(); - // Then - expect(serviceName).toBe('appError'); - expect(sut).toBe(AppError); - }); -}); From 47a43b1476d44bb01256551857884e087679afc7 Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 07:41:52 -0300 Subject: [PATCH 16/78] docs(project): add documentation about the new type of errors --- README-esdoc.md | 3 ++- README.md | 3 ++- documents/services.md | 61 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/README-esdoc.md b/README-esdoc.md index 926db63c..af40835f 100644 --- a/README-esdoc.md +++ b/README-esdoc.md @@ -251,8 +251,9 @@ Jimpex comes with a few services, middlewares and controllers that you can impor ### Services - **API client:** An implementation of the [wootils API Client](https://github.com/homer0/wootils/blob/master/documents/shared/APIClient.md) but that is connected to the HTTP service, to allow logging and forwarding of the headers. +- **App Error:** A very simple subclass of `Error` but with support for context information. It can be used to customize the error handler responses. - **Ensure bearer authentication:** A service-middleware that allows you to validate the incoming requests `Authorization` header. -- **Error:** A very simple subclass of `Error` to inject extra information on the errors so they can customize the error handler responses. +- **HTTP Error:** Another type of error, but specific for the HTTP requests the app does with the API client. - **Send File:** It allows you to send a file on a response with a path relative to the app executable. - **Frontend Fs:** Useful for when your app has a bundled frontend, it allows you to read, write and delete files with paths relative to the app executable. - **HTML Generator:** A service that allows you to generate an HTML file when the app gets started and inject contents of the configuration as a `window` variable. diff --git a/README.md b/README.md index aae85a2e..fe158f4c 100644 --- a/README.md +++ b/README.md @@ -251,8 +251,9 @@ Jimpex comes with a few services, middlewares and controllers that you can impor ### Services - **API client:** An implementation of the [wootils API Client](https://github.com/homer0/wootils/blob/master/documents/shared/APIClient.md) but that is connected to the HTTP service, to allow logging and forwarding of the headers. +- **App Error:** A very simple subclass of `Error` but with support for context information. It can be used to customize the error handler responses. - **Ensure bearer authentication:** A service-middleware that allows you to validate the incoming requests `Authorization` header. -- **Error:** A very simple subclass of `Error` to inject extra information on the errors so they can customize the error handler responses. +- **HTTP Error:** Another type of error, but specific for the HTTP requests the app does with the API client. - **Send File:** It allows you to send a file on a response with a path relative to the app executable. - **Frontend Fs:** Useful for when your app has a bundled frontend, it allows you to read, write and delete files with paths relative to the app executable. - **HTML Generator:** A service that allows you to generate an HTML file when the app gets started and inject contents of the configuration as a `window` variable. diff --git a/documents/services.md b/documents/services.md index 05925e2a..e55dcbf4 100644 --- a/documents/services.md +++ b/documents/services.md @@ -62,6 +62,44 @@ class App extends Jimpex { The first parameter is the name used to register the server and the second one is the setting key that has a `url` and an `endpoints` dictionary. +## App Error + +A very simple subclass of `Error` but with support for context information. It can be used to customize the error handler responses. + +- Module: `common` + +```js +const { + Jimpex, + services: { + common: { appError }, + }, +}; + +class App extends Jimpex { + boot() { + // Register the service + this.register(appError); + } +} +``` + +By registering the "service", two things are added to the container: The class declaration, so you can construct the errors, and a shorthand function that does the same: + +```js +const AppError = app.get('AppError'); +throw new AppError('Something happened', { + someProp: 'someValue', +}); +// or +const appError = app.get('appError'); +throw appError('Something happened', { + someProp: 'someValue', +}); +``` + +This is useful if you are building an app with multiple known exceptions, you can use the context to send useful information. + ## Ensure bearer authentication A service-middleware that allows you to validate the incoming requests `Authorization` header. @@ -107,11 +145,9 @@ const myCtrl = controller((app) => { }); ``` -## Error +## HTTP Error -A very simple subclass of `Error` to inject extra information on the errors so they can customize the error handler responses. - -Something important to remember is that the `appError` service doesn't return an instance of the service but the class so you can construct an error. +Another type of error, but specific for the HTTP requests the app does with the API client. This is a subclass of `AppError`. The only advantage over `AppError` is that you know the that the type of error is specific to requests and that it has a paramter for an HTTP status. - Module: `common` @@ -119,28 +155,27 @@ Something important to remember is that the `appError` service doesn't return an const { Jimpex, services: { - common: { appError }, + common: { httpError }, }, }; class App extends Jimpex { boot() { // Register the service - this.register(appError); + this.register(httpError); } } ``` -That's all, now you can do `get('appError')`, inject `AppError` and generate your custom errors: +By registering the "service", two things are added to the container: The class declaration, so you can construct the errors, and a shorthand function that does the same: ```js -new Error('Something happened', { - someProp: 'someValue', -}): +const HTTPError = app.get('HTTPError'); +throw new AppError('Not found', 404); +// or +const httpError = app.get('httpError'); +throw httpError('Not found', 404); ``` - -This is useful if you are building an app with multiple known exceptions, you can use the extra settings to send context information. - ## Send File It allows you to send a file on a response with a path relative to the app executable. From 1ed64d55d7360fb16468ce2bada934f3ac23274c Mon Sep 17 00:00:00 2001 From: homer0 Date: Thu, 20 Jun 2019 07:53:24 -0300 Subject: [PATCH 17/78] docs(services/common): add inline documentation for the new error types --- src/services/common/appError.js | 71 ++++++++++++++++++++++++++++---- src/services/common/httpError.js | 31 ++++++++++++-- 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/src/services/common/appError.js b/src/services/common/appError.js index 333040b2..da70bf1c 100644 --- a/src/services/common/appError.js +++ b/src/services/common/appError.js @@ -1,37 +1,90 @@ const { provider } = require('../../utils/wrappers'); - +/** + * A simple subclass of `Error` but with support for context information. + * @extends {Error} + */ class AppError extends Error { + /** + * @param {string} message The error message. + * @param {Object} [context={}] Context information related to the error. + */ constructor(message, context = {}) { super(message); + // Limit the stack trace if possible. if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } - - this.name = this.constructor.name; + /** + * Context information related to the error. + * @type {Object} + * @access protected + */ this._context = context; + /** + * The date of when the error was generated. + * @type {Date} + * @access protected + */ this._date = new Date(); + /** + * Overwrite the name of the `Error` with the one from the class. + * @ignore + */ + this.name = this.constructor.name; } - + /** + * Context information related to the error. + * @return {Object} + */ get context() { return this._context; } - + /** + * The date of when the error was generated. + * @return {Date} + */ get date() { return this._date; } - + /** + * Information about the error that can be shown on an app response. This is set using the + * `response` key on the `context`. The idea is that the error handler will read it and use it + * on the response. + * @return {Object} + */ get response() { return this._context.response || {}; } - + /** + * An HTTP status code related to the error. This is set using the `status` key on the + * `context`. If the error handler finds it, it will use it as the response status. + * and use it if necessary. + * @return {?number} + */ get status() { return this._context.status || null; } } - +/** + * A generator function to create {@link AppError} instances. + * @param {string} message The error message. + * @param {Object} [context] Context information related to the error. + */ const appErrorGenerator = (message, context) => new AppError(message, context); - +/** + * A service provider that will register both the {@link AppError} and a generator function on + * the container. `AppError` will be the key for class, and `appError` will be for the + * generator function. + * @example + * // Register it on the container + * container.register(appError); + * // Getting access to the class. + * const AppError = container.get('AppError'); + * // Getting access to the function. + * const appError = container.get('appError'); + * @type {Provider} + */ const appError = provider((app) => { app.set('AppError', () => AppError); app.set('appError', () => appErrorGenerator); diff --git a/src/services/common/httpError.js b/src/services/common/httpError.js index ff8c9a12..14d63cd2 100644 --- a/src/services/common/httpError.js +++ b/src/services/common/httpError.js @@ -2,19 +2,44 @@ const statuses = require('statuses'); const ObjectUtils = require('wootils/shared/objectUtils'); const { provider } = require('../../utils/wrappers'); const { AppError } = require('./appError'); - +/** + * A type of error to be used on HTTP requests. + * @extends {AppError} + */ class HTTPError extends AppError { + /** + * @param {string} message The error message. + * @param {number} status The HTTP status code of the request response. + * @param {Object} [context={}] Context information related to the error. + */ constructor(message, status = statuses.ok, context = {}) { super(message, ObjectUtils.merge({ status }, context)); } } - +/** + * A generator function to create {@link HTTPError} instances. + * @param {string} message The error message. + * @param {number} status The HTTP status code of the request response. + * @param {Object} [context] Context information related to the error. + */ const httpErrorGenerator = (message, status, context) => new HTTPError( message, status, context ); - +/** + * A service provider that will register both the {@link HTTPError} and a generator function on + * the container. `HTTPError` will be the key for class, and `httpError` will be for the + * generator function. + * @example + * // Register it on the container + * container.register(httpError); + * // Getting access to the class. + * const HTTPError = container.get('HTTPError'); + * // Getting access to the function. + * const httpError = container.get('httpError'); + * @type {Provider} + */ const httpError = provider((app) => { app.set('HTTPError', () => HTTPError); app.set('httpError', () => httpErrorGenerator); From 79d5b78417be9e31f76f8d84944f4209390b5f9a Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 00:23:48 -0300 Subject: [PATCH 18/78] feat(middlewares/common/errorHandler): add errorHandlerCustom to modify the middleware defaults --- src/middlewares/common/errorHandler.js | 88 ++++++++++++++++--- src/middlewares/common/index.js | 3 +- tests/middlewares/common/errorHandler.test.js | 78 +++++++++++++++- 3 files changed, 152 insertions(+), 17 deletions(-) diff --git a/src/middlewares/common/errorHandler.js b/src/middlewares/common/errorHandler.js index a8656367..facfb7d5 100644 --- a/src/middlewares/common/errorHandler.js +++ b/src/middlewares/common/errorHandler.js @@ -1,20 +1,49 @@ +const ObjectUtils = require('wootils/shared/objectUtils'); const statuses = require('statuses'); const { middleware } = require('../../utils/wrappers'); + +/** + * @typedef {Object} ErrorHandlerDefaultOptions + * @description Before reading the recevied information, these will be the settings for the + * response. + * @property {string} [message='Oops! Something went wrong, please try again'] + * The error message the response will show. + * @property {number} status + * The HTTP status code for the response. + */ + +/** + * @typedef {Object} ErrorHandlerOptions + * @description The options for how to build the middleware responses. + * @property {ErrorHandlerDefaultOptions} default The options to build the default response, + * before the middleware analyzes the recevied + * error. + */ + /** * Provides the middleware to handle error responses for the app. */ class ErrorHandler { /** * Class constructor. - * @param {Logger} appLogger To log the received errors. - * @param {ResponsesBuilder} responsesBuilder To generate the JSON response. - * @param {Boolean} showErrors If `false`, unknown errors will show a generic - * message instead of real message. And if `true`, it - * will not only show all kind of errors but it will - * also show the error stack. - * @param {Class} AppError To validate if the received errors are known or not. + * @param {Logger} appLogger To log the received errors. + * @param {ResponsesBuilder} responsesBuilder To generate the JSON response. + * @param {Boolean} showErrors If `false`, unknown errors will show a generic + * message instead of real message. And if `true`, + * it will not only show all kind of errors but it + * will also show the error stack. + * @param {Class} AppError To validate if the received errors are known or + * not. + * @param {ErrorHandlerOptions} [options={}] Custom options to modify the middleware + * behavior. */ - constructor(appLogger, responsesBuilder, showErrors, AppError) { + constructor( + appLogger, + responsesBuilder, + showErrors, + AppError, + options = {} + ) { /** * A local reference for the `appLogger` service. * @type {Logger} @@ -35,6 +64,21 @@ class ErrorHandler { * @type {Class} */ this.AppError = AppError; + /** + * These are the "settings" the middleware will use in order to display the errors. + * @type {ErrorHandlerOptions} + * @access protected + * @ignore + */ + this._options = ObjectUtils.merge( + { + default: { + message: 'Oops! Something went wrong, please try again', + status: statuses['internal server error'], + }, + }, + options + ); } /** * Returns the Express middleware that shows the errors. @@ -47,10 +91,10 @@ class ErrorHandler { // Define the error response basic template. let data = { error: true, - message: 'Oops! Something went wrong, please try again', + message: this._options.default.message, }; // Define the error response default status. - let status = statuses['Internal Server Error']; + let { status } = this._options.default; // Validate if the error is known or not. const knownError = err instanceof this.AppError; // If the `showErrors` flag is enabled or the error is a known error... @@ -85,24 +129,40 @@ class ErrorHandler { } }; } + /** + * The options used to customize the middleware behavior. + * @return {ErrorHandlerOptions} + */ + get options() { + return this._options; + } } /** - * This middleware generates responses for unhandled errors generated by the app. - * @type {Middleware} + * Generates a middleware that generates responses for unhandled errors thrown by the app. + * @param {ErrorHandlerOptions} [options] Custom options to modify the middleware behavior. + * @return {Middleware} */ -const errorHandler = middleware((app) => { +const errorHandlerCustom = (options) => middleware((app) => { const debugging = app.get('appConfiguration').get('debug'); const showErrors = debugging && debugging.showErrors; return new ErrorHandler( app.get('appLogger'), app.get('responsesBuilder'), showErrors, - app.get('AppError') + app.get('AppError'), + options ) .middleware(); }); +/** + * This middleware generates responses for unhandled errors thrown by the app. + * @type {Middleware} + */ +const errorHandler = errorHandlerCustom(); + module.exports = { ErrorHandler, errorHandler, + errorHandlerCustom, }; diff --git a/src/middlewares/common/index.js b/src/middlewares/common/index.js index ea85b47f..46dc033a 100644 --- a/src/middlewares/common/index.js +++ b/src/middlewares/common/index.js @@ -1,8 +1,9 @@ -const { errorHandler } = require('./errorHandler'); +const { errorHandler, errorHandlerCustom } = require('./errorHandler'); const { forceHTTPS, forceHTTPSCustom } = require('./forceHTTPS'); module.exports = { errorHandler, + errorHandlerCustom, forceHTTPS, forceHTTPSCustom, }; diff --git a/tests/middlewares/common/errorHandler.test.js b/tests/middlewares/common/errorHandler.test.js index cf64a5bb..798904d9 100644 --- a/tests/middlewares/common/errorHandler.test.js +++ b/tests/middlewares/common/errorHandler.test.js @@ -9,6 +9,7 @@ const statuses = require('statuses'); const { ErrorHandler, errorHandler, + errorHandlerCustom, } = require('/src/middlewares/common/errorHandler'); describe('middlewares/common:errorHandler', () => { @@ -26,6 +27,34 @@ describe('middlewares/common:errorHandler', () => { expect(sut.appLogger).toBe(appLogger); expect(sut.responsesBuilder).toBe(responsesBuilder); expect(sut.showErrors).toBe(showErrors); + expect(sut.options).toEqual(({ + default: { + message: 'Oops! Something went wrong, please try again', + status: statuses['internal server error'], + }, + })); + }); + + it('should be instantiated with custom options', () => { + // Given + const customOptions = { + default: { + message: 'Unknown error', + status: statuses['service unavailable'], + }, + }; + let sut = null; + // When + sut = new ErrorHandler( + 'appLogger', + 'responsesBuilder', + 'showErrors', + 'AppError', + customOptions + ); + // Then + expect(sut).toBeInstanceOf(ErrorHandler); + expect(sut.options).toEqual(customOptions); }); it('should return a middleware to format errors received by Express', () => { @@ -68,6 +97,11 @@ describe('middlewares/common:errorHandler', () => { }; const showErrors = false; const AppError = Error; + const options = { + default: { + message: 'Unknown error', + }, + }; const error = { message: 'Some weird and unexpected error', }; @@ -78,11 +112,11 @@ describe('middlewares/common:errorHandler', () => { let middleware = null; const expectedData = { error: true, - message: 'Oops! Something went wrong, please try again', + message: options.default.message, }; const expectedStatus = statuses['Internal Server Error']; // When - sut = new ErrorHandler(appLogger, responsesBuilder, showErrors, AppError); + sut = new ErrorHandler(appLogger, responsesBuilder, showErrors, AppError, options); middleware = sut.middleware(); middleware(error, request, response, next); // Then @@ -259,4 +293,44 @@ describe('middlewares/common:errorHandler', () => { expect(appConfiguration.get).toHaveBeenCalledTimes(1); expect(appConfiguration.get).toHaveBeenCalledWith('debug'); }); + + it('should include a middleware generator shorthand', () => { + // Given + const appConfiguration = { + debug: { + showErrors: true, + }, + get: jest.fn(() => appConfiguration.debug), + }; + const services = { + appConfiguration, + }; + const app = { + get: jest.fn((service) => (services[service] || service)), + }; + let middleware = null; + let toCompare = null; + const expectedGets = [ + 'appConfiguration', + 'appLogger', + 'responsesBuilder', + 'AppError', + ]; + // When + middleware = errorHandlerCustom().connect(app); + toCompare = new ErrorHandler( + 'appLogger', + 'responsesBuilder', + 'showErrors', + 'AppError' + ); + // Then + expect(middleware.toString()).toEqual(toCompare.middleware().toString()); + expect(app.get).toHaveBeenCalledTimes(expectedGets.length); + expectedGets.forEach((service) => { + expect(app.get).toHaveBeenCalledWith(service); + }); + expect(appConfiguration.get).toHaveBeenCalledTimes(1); + expect(appConfiguration.get).toHaveBeenCalledWith('debug'); + }); }); From 1071a70a3fe16e162fd7c633df4354099337bbfe Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 00:24:18 -0300 Subject: [PATCH 19/78] docs(project): document the new errorHandlerCustom --- documents/middlewares.md | 41 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/documents/middlewares.md b/documents/middlewares.md index a4d6b4d5..8bdca5bf 100644 --- a/documents/middlewares.md +++ b/documents/middlewares.md @@ -37,12 +37,45 @@ class App extends Jimpex { Now, there's a configuration setting for this controller: `debug.showErrors`. By enabling the setting, the middleware will show the message and the stack information of all kind of errors. -If the configuration setting is disabled (or not present), the errors stack will never be visible, and if the error is not an instance of the `appError` service, it will show a generic message: _"Oops! Something went wrong, please try again"_. +If the configuration setting is disabled (or not present), the errors stack will never be visible, and if the error is not an instance of the `AppError` service, it will show a generic message. -Now, when using errors of the type `appError`, you can add the following extra data: +By default, the generic message is _"Oops! Something went wrong, please try again"_ and the default HTTP status is `500`, but you can use the _"middleware generator"_ `errorHandlerCustom` to modify those defaults: ```js -// Assuming `AppError` is the injected `appError` and you are on the context of a middleware +const { + Jimpex, + services: { + http: { responsesBuilder }, + common: { appError }, + }, + middlewares: { + common: { errorHandlerCustom }, + }, +}; + +class App extends Jimpex { + boot() { + // Register the dependencies... + this.register(responsesBuilder); + this.register(appError); + + ... + + // Add the middleware at the end. + this.use(errorHandlerCustom({ + default: { + message: 'Unknown error', + status: 503, + }, + })); + } +} +``` + +Finally, when using errors of the type `AppError`, you can add the following context information: + +```js +// Assuming `AppError` is the injected `AppError` and you are on the context of a middleware next(new AppError('Something went wrong', { status: someHTTPStatus, response: someObject, @@ -267,4 +300,4 @@ class App extends Jimpex { } ``` -**Very important:** The middleware will only validate if `req.params.version` is found. \ No newline at end of file +**Very important:** The middleware will only validate if `req.params.version` is found. From f5d7d5136fd702107936774cfdeaa04e043f369a Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 03:06:27 -0300 Subject: [PATCH 20/78] refactor(utils/wrapper): introduce resource creators --- src/index.js | 14 ++- src/typedef.js | 36 +------- src/utils/wrappers.js | 208 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 214 insertions(+), 44 deletions(-) diff --git a/src/index.js b/src/index.js index cca52373..44743201 100644 --- a/src/index.js +++ b/src/index.js @@ -4,16 +4,22 @@ const middlewares = require('./middlewares'); const services = require('./services'); const { provider, + providerCreator, controller, + controllerCreator, middleware, + middlewareCreator, } = require('./utils/wrappers'); module.exports = { - Jimpex, - provider, + controller, + controllerCreator, controllers, + middleware, + middlewareCreator, middlewares, + provider, + providerCreator, services, - controller, - middleware, + Jimpex, }; diff --git a/src/typedef.js b/src/typedef.js index c73b9cbe..5adb7345 100644 --- a/src/typedef.js +++ b/src/typedef.js @@ -55,36 +55,8 @@ */ /** - * @typedef {function(err:?Error)} ExpressNext A function to call the next middleware. If an - * argument is specified, it will be handled as an error - * and sent to the `errorHandler` service. - */ - -/** - * @typedef {Object} Provider An object that when registered on Jimpex will take care of setting up - * services and/or configuring the app. - * The method Jimpex uses to register a provider is `register(provider)` - * and is inherit from Jimple. - * @property {function(app:Jimpex)} register The method that gets called by Jimpex when registering - * the provider. - */ - -/** - * @typedef {Object} Controller An object that when mounted on Jimpex will return a list of routes - * to handle an specific point. - * The method Jimpex uses to mount a controller is - * `mount(point, controller)`. - * @property {function(app:Jimpex,point:String):Array} connect The method that gets called by - * Jimpex when the controller is - * mounted. It should return a list - * of routes. - */ - -/** - * @typedef {Object} Middleware An object that when mounted on Jimpex will return an Express - * middleware for the app to use. - * The method Jimpex uses to mount a controller is `use(middleware)`. - * @property {function(app:Jimpex):?ExpressMiddleware} connect The method that gets called by Jimpex - * when the middleware is mounted. It - * should return an Express middleware. + * @typdef {function} ExpressNext + * @description A function to call the next middleware. If an argument is specified, it will be + * handled as an error and sent to the `errorHandler` service. + * @param {?Error} error The error to sent to the error handler. */ diff --git a/src/utils/wrappers.js b/src/utils/wrappers.js index ef56e748..3c133693 100644 --- a/src/utils/wrappers.js +++ b/src/utils/wrappers.js @@ -1,23 +1,215 @@ -const { provider } = require('jimple'); /** - * Generates a controller for the app container to mount. - * @param {function(app:Jimpex):Array} connect A function that will be called the moment the app - * mounts the controller. It should return a list - * of routes. + * @typedef {Object} Provider + * @description An object that when registered on Jimpex will take care of setting up services + * and/or configuring the app. The method Jimpex uses to register a provider is + * {@link Jimpex#register} and is inherit from {@link Jimple}. + * @property {ProviderRegistrationCallback} register The function Jimpex calls when registering + * the provider. + */ + +/** + * @typedef {function} ProviderCreator + * @description A special kind of {@link Provider} because it can be used with + * {@link Jimpex#register} as a regular provider, or it can be called as a function + * with custom paramters in order to obtain a "configured {@link Provider}" + * @return {Provider} + */ + +/** + * @typedef {function} ProviderRegistrationCallback + * @description The function called by the app container in order to register a service provider. + * @param {Jimpex} app The instance of the app container. + */ + +/** + * @typedef {function} ProviderCreatorCallback + * @description A function called in order to generate a {@link Provider}. They usually have + * different options that will be sent to the service registration. + * @return {ProviderRegistrationCallback} + */ + +/** + * @typedef {Object} Controller + * @description An object that when mounted on Jimpex will take care of handling a list of specific + * routes. The method Jimpex uses to mount a controller is {@link Jimpex#mount}. + * @property {ControllerMountCallback} connect The function Jimpex calls when mounting the + * controller. + */ + +/** + * @typedef {function} ControllerCreator + * @description A special kind of {@link Controller} because it can be used with + * {@link Jimpex#mount} as a regular controller, or it can be called as a function + * with custom paramters in order to obtain a "configured {@link Controller}" * @return {Controller} */ -const controller = (connect) => ({ connect }); + /** - * Generates a middleware for the app container to use. + * @typedef {function} ControllerMountCallback + * @description The function called by the app container in order to mount a routes controller. + * @param {Jimpex} app The instance of the app container. + * @param {string} point The point where the controller will be mounted. + * @return {Array} The list of routes the controller will manage. + */ + +/** + * @typedef {function} ControllerCreatorCallback + * @description A function called in order to generate a {@link Controller}. They usually have + * different options that will be sent to the controller creation. + * @return {ControllerMountCallback} + */ + +/** + * @typedef {Object} Middleware + * @description An object that when mounted on Jimpex add an {@link ExpressMiddleware} to the app. + * The method Jimpex uses to mount a middleware is {@link Jimpex#use}. + * @property {MiddlewareUseCallback} connect The function Jimpex calls when mounting the + * middleware. + */ + +/** + * @typedef {function} MiddlewareCreator + * @description A special kind of {@link Middleware} because it can be used with + * {@link Jimpex#use} as a regular middleware, or it can be called as a function + * with custom paramters in order to obtain a "configured {@link Middleware}" + * @return {Middleware} + */ + +/** + * @typedef {function} MiddlewareUseCallback + * @description The function called by the app container in order to use a middleware. + * @param {Jimpex} app The instance of the app container. + * @return {?ExpressMiddleware} A middleware for Express to use. It can also return `null` in case + * there's a reason for the middleware not to be active. + */ + +/** + * @typedef {function} MiddlewareCreatorCallback + * @description A function called in order to generate a {@link Middleware}. They usually have + * different options that will be sent to the middleware creation. + * @return {MiddlewareUseCallback} + */ + +/** + * This is a helper the wrappers use in order to create an object by placing a given function + * on an specific key. + * @param {string} key The key in which the function will be placed. + * @param {function} fn The function to insert in the object. + * @return {Object} + * @ignore + */ +const resource = (key, fn) => ({ + [key]: fn, +}); +/** + * Similar to `resource`, this helper creates an "object" and places a given function on an + * specify key. The difference is that instead of being an actual object what gets created, it's + * another function, then a proxy is added to that function in order to intercept the `key` + * property and return the result of the function. + * + * This is kind of hard to explain, so let's compare it with `resource` and use a proper example: + * - `resource`: (key, fn) => ({ [key]: fn }) + * - `resourceCreator`: ((key, creatorFn) => creatorFn(...) => fn)[key]: creatorFn() + * + * While `resource` is meant to create objects with a resource function, this is meant to create + * those resource functions by sending them "optional paramters", and they are optionals because + * if you access the `key` property, it would be the same as calling the `creatorFn` without + * paramters. + * @param {string} key The key in which the creator function will be placed in case it's + * used without parameters; and also the key in which the result + * function from the creator will be placed if called with parameters. + * @param {function} creatorFn The function that generates the "resource function". + * @return {function} + * @ignore + */ +const resourceCreator = (key, creatorFn) => new Proxy( + (...args) => resource(key, creatorFn(...args)), + { + resource: null, + get(target, property) { + let result; + if (property === key) { + if (this.resource === null) { + this.resource = creatorFn(); + } + result = this.resource; + } else { + result = target[property]; + } + + return result; + }, + } +); +/** + * Generates a service provider for the app container. + * @param {ProviderRegistrationCallback} registerFn A function that will be called the moment the + * app registers the provider. + * @return {Provider} + */ +const provider = (registerFn) => resource('register', registerFn); +/** + * Generates a configurable service provider for the app container. It's configurable because + * the creator, instead of just being sent to the container to register, it can also be called + * as a function with custom parameters the service can receive. + * @example + * const myService = providerCreator((options) => (app) => { + * app.set('myService', () => new MyService(options)); + * }); + * @param {ProviderCreatorCallback} creatorFn The function that generates the provider. + * @return {ProviderCreator} + */ +const providerCreator = (creatorFn) => resourceCreator('register', creatorFn); +/** + * Generates a routes controller for the app container to mount. + * @param {ControllerMountCallback} connect A function that will be called the moment the app + * mounts the controller. It should return a list of + * routes. + * @return {Controller} + */ +const controller = (connectFn) => resource('connect', connectFn); +/** + * Generates a configurable routes controller for the app to mount. It's configurable because + * the creator, instead of just being sent to the container to mount, it can also be called + * as a function with custom parameters the controller can receive. + * @example + * const myController = controllerCreator((options) => (app) => { + * const router = app.get('router'); + * const ctrl = new MyController(options); + * return [router.get('...', ctrl.doSomething())]; + * }); + * @param {ProviderCreatorCallback} creatorFn The function that generates the provider. + * @return {ProviderCreator} + */ +const controllerCreator = (creatorFn) => resourceCreator('connect', creatorFn); +/** + * Generates a middleware for the app to use. * @param {function(app:Jimpex):?ExpressMiddleware} connect A function that will be called the * moment the app mounts the middleware. * It should return an Express middleware. * @return {Middleware} */ -const middleware = (connect) => ({ connect }); +const middleware = (connectFn) => resource('connect', connectFn); +/** + * Generates a configurable middleware for the app to use. It's configurable because the creator, + * instead of just being sent to the container to use, it can also be called as a function with + * custom parameters the middleware can receive. + * @example + * const myMiddleware = middlewareCreator((options) => (app) => ( + * options.enabled ? + * (req, res, next) => {} : + * null + * )); + * @param {ProviderCreatorCallback} creatorFn The function that generates the provider. + * @return {ProviderCreator} + */ +const middlewareCreator = (creatorFn) => resourceCreator('connect', creatorFn); module.exports = { provider, + providerCreator, controller, + controllerCreator, middleware, + middlewareCreator, }; From 5d74aa033f0db1ed24db2c310762b0a2e839eb23 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 03:08:22 -0300 Subject: [PATCH 21/78] fix(tests): fix the tests that were using the Jimple mock for providers --- tests/services/api/client.test.js | 2 +- .../api/ensureBearerAuthentication.test.js | 5 +- tests/services/api/index.test.js | 6 +- tests/services/common/appError.test.js | 7 +- tests/services/common/httpError.test.js | 6 +- tests/services/common/index.test.js | 6 +- tests/services/common/sendFile.test.js | 5 +- tests/services/frontend/frontendFs.test.js | 5 +- tests/services/html/htmlGenerator.test.js | 6 +- tests/services/http/http.test.js | 7 +- tests/services/http/index.test.js | 6 +- tests/services/http/responsesBuilder.test.js | 5 +- tests/services/index.test.js | 5 - tests/utils/wrappers.test.js | 165 +++++++++++++++--- 14 files changed, 170 insertions(+), 66 deletions(-) diff --git a/tests/services/api/client.test.js b/tests/services/api/client.test.js index 851625a2..091f2267 100644 --- a/tests/services/api/client.test.js +++ b/tests/services/api/client.test.js @@ -99,7 +99,7 @@ describe('services/api:client', () => { let serviceName = null; let serviceFn = null; // When - apiClientCustom(name, configurationKey, ClientClass)(app); + apiClientCustom(name, configurationKey, ClientClass).register(app); [[serviceName, serviceFn]] = app.set.mock.calls; sut = serviceFn(); // Then diff --git a/tests/services/api/ensureBearerAuthentication.test.js b/tests/services/api/ensureBearerAuthentication.test.js index e027b5af..a174a50e 100644 --- a/tests/services/api/ensureBearerAuthentication.test.js +++ b/tests/services/api/ensureBearerAuthentication.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/services/api/ensureBearerAuthentication'); @@ -113,7 +110,7 @@ describe('services/api:ensureBearerAuthentication', () => { let toCompare = null; // When toCompare = new EnsureBearerAuthentication('AppError'); - ensureBearerAuthentication(app); + ensureBearerAuthentication.register(app); [[serviceName, serviceFn]] = app.set.mock.calls; sut = serviceFn(); // Then diff --git a/tests/services/api/index.test.js b/tests/services/api/index.test.js index ac312b1f..f7569498 100644 --- a/tests/services/api/index.test.js +++ b/tests/services/api/index.test.js @@ -18,12 +18,14 @@ describe('services:api', () => { 'ensureBearerAuthentication', ]; // When - apiServices.all(app); + apiServices.all.register(app); // When/Then expect(app.register).toHaveBeenCalledTimes(expectedServices.length); expectedServices.forEach((service, index) => { const registeredService = app.register.mock.calls[index][0]; - expect(registeredService).toBeFunction(); + expect(registeredService).toEqual({ + register: expect.any(Function), + }); expect(registeredService.toString()).toBe(apiServices[service].toString()); }); }); diff --git a/tests/services/common/appError.test.js b/tests/services/common/appError.test.js index 921c5dea..d98ae178 100644 --- a/tests/services/common/appError.test.js +++ b/tests/services/common/appError.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/services/common/appError'); @@ -77,7 +74,7 @@ describe('services/common:appError', () => { let serviceName = null; let serviceFn = null; // When - appError(app); + appError.register(app); [[serviceName, serviceFn]] = app.set.mock.calls; sut = serviceFn(); // Then @@ -99,7 +96,7 @@ describe('services/common:appError', () => { let serviceFn = null; let result = null; // When - appError(app); + appError.register(app); [, [serviceName, serviceFn]] = app.set.mock.calls; sut = serviceFn(); result = sut(message, context); diff --git a/tests/services/common/httpError.test.js b/tests/services/common/httpError.test.js index d8102336..b15c3883 100644 --- a/tests/services/common/httpError.test.js +++ b/tests/services/common/httpError.test.js @@ -1,7 +1,5 @@ const statuses = require('statuses'); -const JimpleMock = require('/tests/mocks/jimple.mock'); -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/services/common/appError'); jest.unmock('/src/services/common/httpError'); @@ -69,7 +67,7 @@ describe('services/common:httpError', () => { let serviceName = null; let serviceFn = null; // When - httpError(app); + httpError.register(app); [[serviceName, serviceFn]] = app.set.mock.calls; sut = serviceFn(); // Then @@ -88,7 +86,7 @@ describe('services/common:httpError', () => { let serviceFn = null; let result = null; // When - httpError(app); + httpError.register(app); [, [serviceName, serviceFn]] = app.set.mock.calls; sut = serviceFn(); result = sut(message, statuses.conflict); diff --git a/tests/services/common/index.test.js b/tests/services/common/index.test.js index 3c7e6a5d..a07ce157 100644 --- a/tests/services/common/index.test.js +++ b/tests/services/common/index.test.js @@ -19,12 +19,14 @@ describe('services:common', () => { 'sendFile', ]; // When - commonServices.all(app); + commonServices.all.register(app); // When/Then expect(app.register).toHaveBeenCalledTimes(expectedServices.length); expectedServices.forEach((service, index) => { const registeredService = app.register.mock.calls[index][0]; - expect(registeredService).toBeFunction(); + expect(registeredService).toEqual({ + register: expect.any(Function), + }); expect(registeredService.toString()).toBe(commonServices[service].toString()); }); }); diff --git a/tests/services/common/sendFile.test.js b/tests/services/common/sendFile.test.js index 47567dda..2393a8fb 100644 --- a/tests/services/common/sendFile.test.js +++ b/tests/services/common/sendFile.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/services/common/sendFile'); @@ -116,7 +113,7 @@ describe('services/common:sendFile', () => { let serviceName = null; let serviceFn = null; // When - sendFileProvider(app); + sendFileProvider.register(app); [[serviceName, serviceFn]] = app.set.mock.calls; sut = serviceFn(); // Then diff --git a/tests/services/frontend/frontendFs.test.js b/tests/services/frontend/frontendFs.test.js index f464b4db..7f2c7064 100644 --- a/tests/services/frontend/frontendFs.test.js +++ b/tests/services/frontend/frontendFs.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.mock('fs-extra'); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/services/frontend/frontendFs'); @@ -126,7 +123,7 @@ describe('services/frontend:frontendFs', () => { let serviceName = null; let serviceFn = null; // When - frontendFs(app); + frontendFs.register(app); [[serviceName, serviceFn]] = app.set.mock.calls; sut = serviceFn(); // Then diff --git a/tests/services/html/htmlGenerator.test.js b/tests/services/html/htmlGenerator.test.js index fd7b49ce..4f03036c 100644 --- a/tests/services/html/htmlGenerator.test.js +++ b/tests/services/html/htmlGenerator.test.js @@ -1,7 +1,5 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); const wootilsMock = require('/tests/mocks/wootils.mock'); -jest.mock('jimple', () => JimpleMock); jest.mock('wootils/shared', () => wootilsMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/services/html/htmlGenerator'); @@ -474,7 +472,7 @@ describe('services/html:htmlGenerator', () => { let eventName = null; let eventFn = null; // When - htmlGeneratorCustom({}, name)(app); + htmlGeneratorCustom({}, name).register(app); [[serviceName, serviceFn]] = app.set.mock.calls; [[eventName, eventFn]] = events.once.mock.calls; sut = serviceFn(); @@ -544,7 +542,7 @@ describe('services/html:htmlGenerator', () => { const expectedGets = Object.keys(services); const expectedEventName = 'after-start'; // When - htmlGeneratorCustom({}, name, myValuesServiceName)(app); + htmlGeneratorCustom({}, name, myValuesServiceName).register(app); [[serviceName, serviceFn]] = app.set.mock.calls; [[eventName, eventFn]] = events.once.mock.calls; sut = serviceFn(); diff --git a/tests/services/http/http.test.js b/tests/services/http/http.test.js index 8bb3abc5..7b0d7aba 100644 --- a/tests/services/http/http.test.js +++ b/tests/services/http/http.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.mock('node-fetch'); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/services/http/http'); @@ -46,7 +43,7 @@ describe('services/http:http', () => { let serviceName = null; let serviceFn = null; // When - http(app); + http.register(app); [[serviceName, serviceFn]] = app.set.mock.calls; sut = serviceFn(); // Then @@ -390,7 +387,7 @@ describe('services/http:http', () => { let serviceName = null; let serviceFn = null; // When - http(app); + http.register(app); [[serviceName, serviceFn]] = app.set.mock.calls; sut = serviceFn(); // Then diff --git a/tests/services/http/index.test.js b/tests/services/http/index.test.js index d1efee89..c22348dd 100644 --- a/tests/services/http/index.test.js +++ b/tests/services/http/index.test.js @@ -18,12 +18,14 @@ describe('services:http', () => { 'responsesBuilder', ]; // When - httpServices.all(app); + httpServices.all.register(app); // When/Then expect(app.register).toHaveBeenCalledTimes(expectedServices.length); expectedServices.forEach((service, index) => { const registeredService = app.register.mock.calls[index][0]; - expect(registeredService).toBeFunction(); + expect(registeredService).toEqual({ + register: expect.any(Function), + }); expect(registeredService.toString()).toBe(httpServices[service].toString()); }); }); diff --git a/tests/services/http/responsesBuilder.test.js b/tests/services/http/responsesBuilder.test.js index 0acc9b34..6f6a5910 100644 --- a/tests/services/http/responsesBuilder.test.js +++ b/tests/services/http/responsesBuilder.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/services/http/responsesBuilder'); @@ -188,7 +185,7 @@ describe('services/http:responsesBuilder', () => { let serviceName = null; let serviceFn = null; // When - responsesBuilder(app); + responsesBuilder.register(app); [[serviceName, serviceFn]] = app.set.mock.calls; sut = serviceFn(); // Then diff --git a/tests/services/index.test.js b/tests/services/index.test.js index ad79b914..550b28d8 100644 --- a/tests/services/index.test.js +++ b/tests/services/index.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/services'); @@ -21,8 +18,6 @@ describe('services', () => { expect(Object.keys(services).length).toBe(knownServices.length); knownServices.forEach((name) => { expect(services[name]).toBeObject(); - const modules = Object.keys(services[name]); - modules.forEach((mod) => expect(services[name][mod]).toBeFunction()); }); }); }); diff --git a/tests/utils/wrappers.test.js b/tests/utils/wrappers.test.js index 28d0cdad..e298ed17 100644 --- a/tests/utils/wrappers.test.js +++ b/tests/utils/wrappers.test.js @@ -6,36 +6,161 @@ jest.unmock('/src/utils/wrappers'); require('jasmine-expect'); const { provider, + providerCreator, controller, + controllerCreator, middleware, + middlewareCreator, } = require('/src/utils/wrappers'); describe('utils/wrappers', () => { - it('should export the Jimple `provider` method', () => { - expect(provider).toBe(JimpleMock.provider); + describe('provider', () => { + it('should create a service provider with a `register` function', () => { + // Given + const serviceProvider = 'serviceProvider'; + let result = null; + // When + result = provider(serviceProvider); + // Then + expect(result).toEqual({ + register: serviceProvider, + }); + }); + + it('should create a service provider creator', () => { + // Given + const serviceProvider = 'serviceProvider'; + const creatorFn = jest.fn(() => serviceProvider); + const options = 'options'; + let creator = null; + let result = null; + // When + creator = providerCreator(creatorFn); + result = creator(options); + // Then + expect(result).toEqual({ + register: serviceProvider, + }); + expect(creatorFn).toHaveBeenCalledTimes(1); + expect(creatorFn).toHaveBeenCalledWith(options); + }); + + it('should create a service provider creator that can be used without options', () => { + // Given + const serviceProvider = 'serviceProvider'; + const creatorFn = jest.fn(() => serviceProvider); + let creator = null; + // When + creator = providerCreator(creatorFn); + // Then + expect(creator.register).toBe(serviceProvider); + expect(creator.anyOtherProp).toBeUndefined(); // to validate the getter. + expect(creatorFn).toHaveBeenCalledTimes(1); + expect(creatorFn).toHaveBeenCalledWith(); + }); + + it('should create a service provider with default options just once', () => { + // Given + const serviceProvider = 'serviceProvider'; + const creatorFn = jest.fn(() => serviceProvider); + let creator = null; + // When + creator = providerCreator(creatorFn); + // Then + expect(creator.register).toBe(serviceProvider); + expect(creator.register).toBe(serviceProvider); // this is the second trigger for the proxy. + expect(creatorFn).toHaveBeenCalledTimes(1); + expect(creatorFn).toHaveBeenCalledWith(); + }); }); - it('should export a `controller` function that wraps a `connect` statement', () => { - // Given - const myController = 'my-controller'; - let result = null; - // When - result = controller(myController); - // Then - expect(result).toEqual({ - connect: myController, + describe('controller', () => { + it('should create a route controller with a `connect` function', () => { + // Given + const routeController = 'routeController'; + let result = null; + // When + result = controller(routeController); + // Then + expect(result).toEqual({ + connect: routeController, + }); + }); + + it('should create a route controller creator', () => { + // Given + const routeController = 'routeController'; + const creatorFn = jest.fn(() => routeController); + const options = 'options'; + let creator = null; + let result = null; + // When + creator = controllerCreator(creatorFn); + result = creator(options); + // Then + expect(result).toEqual({ + connect: routeController, + }); + expect(creatorFn).toHaveBeenCalledTimes(1); + expect(creatorFn).toHaveBeenCalledWith(options); + }); + + it('should create a route controller creator that can be used without options', () => { + // Given + const routeController = 'routeController'; + const creatorFn = jest.fn(() => routeController); + let creator = null; + // When + creator = controllerCreator(creatorFn); + // Then + expect(creator.connect).toBe(routeController); + expect(creatorFn).toHaveBeenCalledTimes(1); + expect(creatorFn).toHaveBeenCalledWith(); }); }); - it('should export a `middleware` function that wraps a `connect` statement', () => { - // Given - const myMiddleware = 'my-middleware'; - let result = null; - // When - result = middleware(myMiddleware); - // Then - expect(result).toEqual({ - connect: myMiddleware, + describe('middleware', () => { + it('should create a route middleware with a `connect` function', () => { + // Given + const appMiddleware = 'appMiddleware'; + let result = null; + // When + result = middleware(appMiddleware); + // Then + expect(result).toEqual({ + connect: appMiddleware, + }); + }); + + it('should create a route middleware creator', () => { + // Given + const appMiddleware = 'appMiddleware'; + const creatorFn = jest.fn(() => appMiddleware); + const options = 'options'; + let creator = null; + let result = null; + // When + creator = middlewareCreator(creatorFn); + result = creator(options); + // Then + expect(result).toEqual({ + connect: appMiddleware, + }); + expect(creatorFn).toHaveBeenCalledTimes(1); + expect(creatorFn).toHaveBeenCalledWith(options); + }); + + it('should create a route middleware creator that can be used without options', () => { + // Given + const appMiddleware = 'appMiddleware'; + const creatorFn = jest.fn(() => appMiddleware); + let creator = null; + // When + creator = middlewareCreator(creatorFn); + // Then + expect(creator.connect).toBe(appMiddleware); + expect(creatorFn).toHaveBeenCalledTimes(1); + expect(creatorFn).toHaveBeenCalledWith(); }); }); }); From dde6296bd7ad1b2e3b6723ee4e0f152cdee042bc Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 03:12:31 -0300 Subject: [PATCH 22/78] refactor(tests): remove the Jimple mock from all the other tests where is not necessary --- tests/controllers/common/configuration.test.js | 3 --- tests/controllers/common/health.test.js | 3 --- tests/controllers/common/rootStatics.test.js | 3 --- tests/controllers/index.test.js | 3 --- tests/middlewares/common/errorHandler.test.js | 3 --- tests/middlewares/common/forceHTTPS.test.js | 3 --- tests/middlewares/html/fastHTML.test.js | 3 --- tests/middlewares/html/showHTML.test.js | 3 --- tests/middlewares/index.test.js | 3 --- tests/middlewares/utils/versionValidator.test.js | 3 --- tests/services/api/client.test.js | 3 --- tests/services/api/index.test.js | 3 --- tests/services/common/index.test.js | 3 --- tests/services/http/index.test.js | 3 --- tests/utils/wrappers.test.js | 3 --- 15 files changed, 45 deletions(-) diff --git a/tests/controllers/common/configuration.test.js b/tests/controllers/common/configuration.test.js index df422bf4..f59bcafc 100644 --- a/tests/controllers/common/configuration.test.js +++ b/tests/controllers/common/configuration.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/controllers/common/configuration'); diff --git a/tests/controllers/common/health.test.js b/tests/controllers/common/health.test.js index eb28ae5b..6e6cd409 100644 --- a/tests/controllers/common/health.test.js +++ b/tests/controllers/common/health.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/controllers/common/health'); diff --git a/tests/controllers/common/rootStatics.test.js b/tests/controllers/common/rootStatics.test.js index 758db62f..6686b776 100644 --- a/tests/controllers/common/rootStatics.test.js +++ b/tests/controllers/common/rootStatics.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/controllers/common/rootStatics'); diff --git a/tests/controllers/index.test.js b/tests/controllers/index.test.js index 964bcba1..29b12049 100644 --- a/tests/controllers/index.test.js +++ b/tests/controllers/index.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.mock('/src/utils/wrappers', () => ({ controller: (connect) => connect, })); diff --git a/tests/middlewares/common/errorHandler.test.js b/tests/middlewares/common/errorHandler.test.js index 798904d9..95f06d41 100644 --- a/tests/middlewares/common/errorHandler.test.js +++ b/tests/middlewares/common/errorHandler.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/middlewares/common/errorHandler'); diff --git a/tests/middlewares/common/forceHTTPS.test.js b/tests/middlewares/common/forceHTTPS.test.js index c1f3b3eb..40ab7fa3 100644 --- a/tests/middlewares/common/forceHTTPS.test.js +++ b/tests/middlewares/common/forceHTTPS.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/middlewares/common/forceHTTPS'); diff --git a/tests/middlewares/html/fastHTML.test.js b/tests/middlewares/html/fastHTML.test.js index 691cee66..8045e666 100644 --- a/tests/middlewares/html/fastHTML.test.js +++ b/tests/middlewares/html/fastHTML.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/middlewares/html/fastHTML'); diff --git a/tests/middlewares/html/showHTML.test.js b/tests/middlewares/html/showHTML.test.js index d4b5b749..582662e0 100644 --- a/tests/middlewares/html/showHTML.test.js +++ b/tests/middlewares/html/showHTML.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/middlewares/html/showHTML'); diff --git a/tests/middlewares/index.test.js b/tests/middlewares/index.test.js index 21a23768..39cf96c1 100644 --- a/tests/middlewares/index.test.js +++ b/tests/middlewares/index.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.mock('/src/utils/wrappers', () => ({ middleware: (connect) => connect, })); diff --git a/tests/middlewares/utils/versionValidator.test.js b/tests/middlewares/utils/versionValidator.test.js index 4e473caa..dadb4f9d 100644 --- a/tests/middlewares/utils/versionValidator.test.js +++ b/tests/middlewares/utils/versionValidator.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/middlewares/utils/versionValidator'); diff --git a/tests/services/api/client.test.js b/tests/services/api/client.test.js index 091f2267..cdf57f11 100644 --- a/tests/services/api/client.test.js +++ b/tests/services/api/client.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/services/api/client'); diff --git a/tests/services/api/index.test.js b/tests/services/api/index.test.js index f7569498..17ddc977 100644 --- a/tests/services/api/index.test.js +++ b/tests/services/api/index.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/services/api'); diff --git a/tests/services/common/index.test.js b/tests/services/common/index.test.js index a07ce157..3906362c 100644 --- a/tests/services/common/index.test.js +++ b/tests/services/common/index.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/services/common'); diff --git a/tests/services/http/index.test.js b/tests/services/http/index.test.js index c22348dd..1a36b01b 100644 --- a/tests/services/http/index.test.js +++ b/tests/services/http/index.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/services/http'); diff --git a/tests/utils/wrappers.test.js b/tests/utils/wrappers.test.js index e298ed17..c8c13265 100644 --- a/tests/utils/wrappers.test.js +++ b/tests/utils/wrappers.test.js @@ -1,6 +1,3 @@ -const JimpleMock = require('/tests/mocks/jimple.mock'); - -jest.mock('jimple', () => JimpleMock); jest.unmock('/src/utils/wrappers'); require('jasmine-expect'); From 6c115684aac437ac5ea467d6b55643c573425897 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 03:26:01 -0300 Subject: [PATCH 23/78] refactor(controllers/common/rootStatics): use the new controller creator --- src/controllers/common/index.js | 6 +--- src/controllers/common/rootStatics.js | 22 ++++++-------- tests/controllers/common/rootStatics.test.js | 31 ++++++++++++++++++-- tests/controllers/index.test.js | 6 +--- 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/controllers/common/index.js b/src/controllers/common/index.js index 1ffff46c..6d63b4d9 100644 --- a/src/controllers/common/index.js +++ b/src/controllers/common/index.js @@ -1,13 +1,9 @@ const { configurationController } = require('./configuration'); const { healthController } = require('./health'); -const { - rootStaticsController, - rootStaticsControllerCustom, -} = require('./rootStatics'); +const { rootStaticsController } = require('./rootStatics'); module.exports = { configurationController, healthController, rootStaticsController, - rootStaticsControllerCustom, }; diff --git a/src/controllers/common/rootStatics.js b/src/controllers/common/rootStatics.js index 9cd43f9d..1cb29f54 100644 --- a/src/controllers/common/rootStatics.js +++ b/src/controllers/common/rootStatics.js @@ -1,6 +1,6 @@ const ObjectUtils = require('wootils/shared/objectUtils'); const mime = require('mime'); -const { controller } = require('../../utils/wrappers'); +const { controllerCreator } = require('../../utils/wrappers'); /** * Since the static files are inside a folder and we can't have the the `static` middleware pointing * to the app root directory, this service allows you to serve static files that are on the root @@ -95,28 +95,24 @@ class RootStaticsController { } } /** - * Generates a controller with an already defined list of files. - * The controller will get all the files, add route for each one of them and include a middleware - * provided by the `RootStaticsController` in order to serve them. + * This controller can be used to serve files from the root directory; the idea is to avoid + * declaring the root directory as a static folder. + * The files are sent to {@link RootStaticsController} and for each file, it will declare a route + * and mount a middleware in order to serve them. + * @type {ControllerCreator} * @param {Array} files The list of files. Each item can be a `string` or an `Object` with the * keys `origin` for the file route, `output` for the file location relative - * to the root, and `headers` with the file custom headers for the response. - * @return {Controller} + * to the root, and `headers` with the file custom headers in case they are + * needed on the response. */ -const rootStaticsControllerCustom = (files) => controller((app) => { +const rootStaticsController = controllerCreator((files) => (app) => { const router = app.get('router'); const ctrl = new RootStaticsController(app.get('sendFile'), files); return ctrl.getFileEntries() .map((file) => router.all(`/${file}`, ctrl.serveFile(file))); }); -/** - * Mount a controller to serve an `index.html` and `favicon.ico` files from the root directory. - * @type {Controller} - */ -const rootStaticsController = rootStaticsControllerCustom(); module.exports = { RootStaticsController, rootStaticsController, - rootStaticsControllerCustom, }; diff --git a/tests/controllers/common/rootStatics.test.js b/tests/controllers/common/rootStatics.test.js index 6686b776..ca41f95f 100644 --- a/tests/controllers/common/rootStatics.test.js +++ b/tests/controllers/common/rootStatics.test.js @@ -4,7 +4,7 @@ jest.unmock('/src/controllers/common/rootStatics'); require('jasmine-expect'); const { RootStaticsController, - rootStaticsControllerCustom, + rootStaticsController, } = require('/src/controllers/common/rootStatics'); describe('controllers/common:rootStatics', () => { @@ -127,6 +127,33 @@ describe('controllers/common:rootStatics', () => { }); it('should include a controller shorthand to return its routes', () => { + // Given + const files = ['index.html', 'favicon.ico']; + const services = { + router: { + all: jest.fn((route, middleware) => [`all:${route}`, middleware.toString()]), + }, + }; + const app = { + get: jest.fn((service) => (services[service] || service)), + }; + let routes = null; + let toCompare = null; + const expectedGets = ['router', 'sendFile']; + // When + routes = rootStaticsController.connect(app); + toCompare = new RootStaticsController('sendFile'); + // Then + expect(routes).toEqual(files.map((file) => ( + [`all:/${file}`, toCompare.serveFile(file).toString()] + ))); + expect(app.get).toHaveBeenCalledTimes(expectedGets.length); + expectedGets.forEach((service) => { + expect(app.get).toHaveBeenCalledWith(service); + }); + }); + + it('should include a controller creator shorthand to configure the files', () => { // Given const file = 'index.html'; const services = { @@ -141,7 +168,7 @@ describe('controllers/common:rootStatics', () => { let toCompare = null; const expectedGets = ['router', 'sendFile']; // When - routes = rootStaticsControllerCustom([file]).connect(app); + routes = rootStaticsController([file]).connect(app); toCompare = new RootStaticsController('sendFile', [file]); // Then expect(routes).toEqual([ diff --git a/tests/controllers/index.test.js b/tests/controllers/index.test.js index 29b12049..a03bb6ca 100644 --- a/tests/controllers/index.test.js +++ b/tests/controllers/index.test.js @@ -1,6 +1,4 @@ -jest.mock('/src/utils/wrappers', () => ({ - controller: (connect) => connect, -})); +jest.unmock('/src/utils/wrappers'); jest.unmock('/src/controllers'); require('jasmine-expect'); @@ -16,8 +14,6 @@ describe('controllers', () => { expect(Object.keys(controllers).length).toBe(knownControllers.length); knownControllers.forEach((name) => { expect(controllers[name]).toBeObject(); - const modules = Object.keys(controllers[name]); - modules.forEach((mod) => expect(controllers[name][mod]).toBeFunction()); }); }); }); From 4b9c125204623834fb120a15a99c5ffed198e44a Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 03:31:25 -0300 Subject: [PATCH 24/78] refactor(middlewares/common/errorHandler): use the new middleware creator --- src/middlewares/common/errorHandler.js | 15 ++++----------- src/middlewares/common/index.js | 3 +-- tests/middlewares/common/errorHandler.test.js | 5 ++--- tests/middlewares/index.test.js | 6 +----- 4 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/middlewares/common/errorHandler.js b/src/middlewares/common/errorHandler.js index facfb7d5..b4694c93 100644 --- a/src/middlewares/common/errorHandler.js +++ b/src/middlewares/common/errorHandler.js @@ -1,6 +1,6 @@ const ObjectUtils = require('wootils/shared/objectUtils'); const statuses = require('statuses'); -const { middleware } = require('../../utils/wrappers'); +const { middlewareCreator } = require('../../utils/wrappers'); /** * @typedef {Object} ErrorHandlerDefaultOptions @@ -138,11 +138,11 @@ class ErrorHandler { } } /** - * Generates a middleware that generates responses for unhandled errors thrown by the app. + * Generates a middleware that show responses for unhandled errors thrown by the app. + * @type {MiddlewareCreator} * @param {ErrorHandlerOptions} [options] Custom options to modify the middleware behavior. - * @return {Middleware} */ -const errorHandlerCustom = (options) => middleware((app) => { +const errorHandler = middlewareCreator((options) => (app) => { const debugging = app.get('appConfiguration').get('debug'); const showErrors = debugging && debugging.showErrors; return new ErrorHandler( @@ -155,14 +155,7 @@ const errorHandlerCustom = (options) => middleware((app) => { .middleware(); }); -/** - * This middleware generates responses for unhandled errors thrown by the app. - * @type {Middleware} - */ -const errorHandler = errorHandlerCustom(); - module.exports = { ErrorHandler, errorHandler, - errorHandlerCustom, }; diff --git a/src/middlewares/common/index.js b/src/middlewares/common/index.js index 46dc033a..ea85b47f 100644 --- a/src/middlewares/common/index.js +++ b/src/middlewares/common/index.js @@ -1,9 +1,8 @@ -const { errorHandler, errorHandlerCustom } = require('./errorHandler'); +const { errorHandler } = require('./errorHandler'); const { forceHTTPS, forceHTTPSCustom } = require('./forceHTTPS'); module.exports = { errorHandler, - errorHandlerCustom, forceHTTPS, forceHTTPSCustom, }; diff --git a/tests/middlewares/common/errorHandler.test.js b/tests/middlewares/common/errorHandler.test.js index 95f06d41..36088607 100644 --- a/tests/middlewares/common/errorHandler.test.js +++ b/tests/middlewares/common/errorHandler.test.js @@ -6,7 +6,6 @@ const statuses = require('statuses'); const { ErrorHandler, errorHandler, - errorHandlerCustom, } = require('/src/middlewares/common/errorHandler'); describe('middlewares/common:errorHandler', () => { @@ -291,7 +290,7 @@ describe('middlewares/common:errorHandler', () => { expect(appConfiguration.get).toHaveBeenCalledWith('debug'); }); - it('should include a middleware generator shorthand', () => { + it('should include a middleware creator shorthand to modify its options', () => { // Given const appConfiguration = { debug: { @@ -314,7 +313,7 @@ describe('middlewares/common:errorHandler', () => { 'AppError', ]; // When - middleware = errorHandlerCustom().connect(app); + middleware = errorHandler().connect(app); toCompare = new ErrorHandler( 'appLogger', 'responsesBuilder', diff --git a/tests/middlewares/index.test.js b/tests/middlewares/index.test.js index 39cf96c1..99aaeeb2 100644 --- a/tests/middlewares/index.test.js +++ b/tests/middlewares/index.test.js @@ -1,6 +1,4 @@ -jest.mock('/src/utils/wrappers', () => ({ - middleware: (connect) => connect, -})); +jest.unmock('/src/utils/wrappers'); jest.unmock('/src/middlewares'); require('jasmine-expect'); @@ -18,8 +16,6 @@ describe('middlewares', () => { expect(Object.keys(middlewares).length).toBe(knownMiddlewares.length); knownMiddlewares.forEach((name) => { expect(middlewares[name]).toBeObject(); - const modules = Object.keys(middlewares[name]); - modules.forEach((mod) => expect(middlewares[name][mod]).toBeFunction()); }); }); }); From 0fcf71999513a09aa6c676b87d298b671b941e22 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 03:34:15 -0300 Subject: [PATCH 25/78] refactor(middlewares/common/forceHTTPS): use the new middleware creator --- src/middlewares/common/forceHTTPS.js | 14 ++++---------- src/middlewares/common/index.js | 3 +-- tests/middlewares/common/forceHTTPS.test.js | 6 +++--- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/middlewares/common/forceHTTPS.js b/src/middlewares/common/forceHTTPS.js index 31ec6d85..63d515f2 100644 --- a/src/middlewares/common/forceHTTPS.js +++ b/src/middlewares/common/forceHTTPS.js @@ -1,4 +1,4 @@ -const { middleware } = require('../../utils/wrappers'); +const { middlewareCreator } = require('../../utils/wrappers'); /** * Force all the app traffice to be through HTTPS. */ @@ -35,24 +35,18 @@ class ForceHTTPS { } } /** - * Generates a middleware with an already defined list of ignored routes expressions. + * A middleware to force HTTPS redirections to all the routes. + * @type {MiddlewareCreator} * @param {Array} ignoredRoutes A list of regular expressions to match routes that should be * ignored. - * @return {Middleware} */ -const forceHTTPSCustom = (ignoredRoutes) => middleware((app) => ( +const forceHTTPS = middlewareCreator((ignoredRoutes) => (app) => ( app.get('appConfiguration').get('forceHTTPS') ? new ForceHTTPS(ignoredRoutes).middleware() : null )); -/** - * A middleware to force HTTPS redirections to all the routes. - * @type {Middleware} - */ -const forceHTTPS = forceHTTPSCustom(); module.exports = { ForceHTTPS, forceHTTPS, - forceHTTPSCustom, }; diff --git a/src/middlewares/common/index.js b/src/middlewares/common/index.js index ea85b47f..540ef864 100644 --- a/src/middlewares/common/index.js +++ b/src/middlewares/common/index.js @@ -1,8 +1,7 @@ const { errorHandler } = require('./errorHandler'); -const { forceHTTPS, forceHTTPSCustom } = require('./forceHTTPS'); +const { forceHTTPS } = require('./forceHTTPS'); module.exports = { errorHandler, forceHTTPS, - forceHTTPSCustom, }; diff --git a/tests/middlewares/common/forceHTTPS.test.js b/tests/middlewares/common/forceHTTPS.test.js index 40ab7fa3..aa451225 100644 --- a/tests/middlewares/common/forceHTTPS.test.js +++ b/tests/middlewares/common/forceHTTPS.test.js @@ -4,7 +4,7 @@ jest.unmock('/src/middlewares/common/forceHTTPS'); require('jasmine-expect'); const { ForceHTTPS, - forceHTTPSCustom, + forceHTTPS, } = require('/src/middlewares/common/forceHTTPS'); describe('middlewares/common:forceHTTPS', () => { @@ -135,7 +135,7 @@ describe('middlewares/common:forceHTTPS', () => { 'appConfiguration', ]; // When - middleware = forceHTTPSCustom().connect(app); + middleware = forceHTTPS.connect(app); toCompare = new ForceHTTPS(); // Then expect(middleware.toString()).toEqual(toCompare.middleware().toString()); @@ -164,7 +164,7 @@ describe('middlewares/common:forceHTTPS', () => { 'appConfiguration', ]; // When - middleware = forceHTTPSCustom().connect(app); + middleware = forceHTTPS().connect(app); // Then expect(middleware).toBeNull(); expect(app.get).toHaveBeenCalledTimes(expectedGets.length); From 58ec37d9d0e931dcd84e754369aa3b491cd4c4d8 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 03:37:54 -0300 Subject: [PATCH 26/78] refactor(middlewares/html/fastHTML): use the new middleware creator --- src/middlewares/html/fastHTML.js | 18 +++++--------- src/middlewares/html/index.js | 3 +-- tests/middlewares/html/fastHTML.test.js | 33 +++++++++++++++++++++++-- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/middlewares/html/fastHTML.js b/src/middlewares/html/fastHTML.js index 1cba6d6a..efc12f50 100644 --- a/src/middlewares/html/fastHTML.js +++ b/src/middlewares/html/fastHTML.js @@ -1,5 +1,5 @@ const mime = require('mime'); -const { middleware } = require('../../utils/wrappers'); +const { middlewareCreator } = require('../../utils/wrappers'); /** * It's common for an app to show an HTML view when no route was able to handle a request, so the * idea behind this middleware is to avoid going to every middleware and controller and just @@ -124,7 +124,9 @@ class FastHTML { } } /** - * Generates a middleware with customized options. + * A middleware for filtering routes and serve an HTML file when the requested route doesn't have + * a controller to handle it. + * @type {MiddlewareCreator} * @param {string} [file] The name of the file it will serve. * If the `HTMLGenerator` service * specified is avaialable, this will @@ -138,13 +140,12 @@ class FastHTML { * registered on the app, it won't throw * an error, but just send `null` to * the service constructor. - * @return {Middleware} */ -const fastHTMLCustom = ( +const fastHTML = middlewareCreator(( file, ignoredRoutes, htmlGeneratorServiceName = 'htmlGenerator' -) => middleware((app) => { +) => (app) => { let htmlGenerator; try { htmlGenerator = app.get(htmlGeneratorServiceName); @@ -159,15 +160,8 @@ const fastHTMLCustom = ( htmlGenerator ).middleware(); }); -/** - * A middleware for filtering routes and serve an HTML file when the requested route doesn't have - * a controller to handle it. - * @type {Middleware} - */ -const fastHTML = fastHTMLCustom(); module.exports = { FastHTML, fastHTML, - fastHTMLCustom, }; diff --git a/src/middlewares/html/index.js b/src/middlewares/html/index.js index a291cfb8..56642e6d 100644 --- a/src/middlewares/html/index.js +++ b/src/middlewares/html/index.js @@ -1,9 +1,8 @@ -const { fastHTML, fastHTMLCustom } = require('./fastHTML'); +const { fastHTML } = require('./fastHTML'); const { showHTML, showHTMLCustom } = require('./showHTML'); module.exports = { fastHTML, - fastHTMLCustom, showHTML, showHTMLCustom, }; diff --git a/tests/middlewares/html/fastHTML.test.js b/tests/middlewares/html/fastHTML.test.js index 8045e666..00f63d71 100644 --- a/tests/middlewares/html/fastHTML.test.js +++ b/tests/middlewares/html/fastHTML.test.js @@ -4,7 +4,7 @@ jest.unmock('/src/middlewares/html/fastHTML'); require('jasmine-expect'); const { FastHTML, - fastHTMLCustom, + fastHTML, } = require('/src/middlewares/html/fastHTML'); describe('middlewares/html:fastHTML', () => { @@ -171,7 +171,36 @@ describe('middlewares/html:fastHTML', () => { 'sendFile', ]; // When - middleware = fastHTMLCustom().connect(app); + middleware = fastHTML.connect(app); + toCompare = new FastHTML(); + // Then + expect(middleware.toString()).toEqual(toCompare.middleware().toString()); + expect(app.get).toHaveBeenCalledTimes(expectedGets.length); + expectedGets.forEach((service) => { + expect(app.get).toHaveBeenCalledWith(service); + }); + }); + + it('should include a middleware creator shorthand to configure its options', () => { + // Given + const services = {}; + const app = { + get: jest.fn((service) => { + if (service === 'htmlGenerator') { + throw Error(); + } + + return services[service] || service; + }), + }; + let middleware = null; + let toCompare = null; + const expectedGets = [ + 'htmlGenerator', + 'sendFile', + ]; + // When + middleware = fastHTML().connect(app); toCompare = new FastHTML(); // Then expect(middleware.toString()).toEqual(toCompare.middleware().toString()); From df2f87ce4fb332c5f6df16fbc24b28a7d9619762 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 03:43:58 -0300 Subject: [PATCH 27/78] refactor(middlewares/html/showHTML): use the new middleware creator --- src/middlewares/html/index.js | 3 +-- src/middlewares/html/showHTML.js | 16 ++++------- tests/middlewares/html/showHTML.test.js | 35 +++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/middlewares/html/index.js b/src/middlewares/html/index.js index 56642e6d..c1bfea6b 100644 --- a/src/middlewares/html/index.js +++ b/src/middlewares/html/index.js @@ -1,8 +1,7 @@ const { fastHTML } = require('./fastHTML'); -const { showHTML, showHTMLCustom } = require('./showHTML'); +const { showHTML } = require('./showHTML'); module.exports = { fastHTML, showHTML, - showHTMLCustom, }; diff --git a/src/middlewares/html/showHTML.js b/src/middlewares/html/showHTML.js index 1f620dd5..7dfb235a 100644 --- a/src/middlewares/html/showHTML.js +++ b/src/middlewares/html/showHTML.js @@ -1,5 +1,5 @@ const mime = require('mime'); -const { middleware } = require('../../utils/wrappers'); +const { middlewareCreator } = require('../../utils/wrappers'); /** * A very simple middleware service to send an HTML on a server response. The special _'feature'_ of * this service is that it can be hooked up to an `HTMLGenerator` service and it will automatically @@ -96,7 +96,8 @@ class ShowHTML { } } /** - * Generates a middleware with customized options. + * A middleware for showing an `index.html` file. + * @type {MiddlewareCreator} * @param {string} [file] The name of the file it will serve. * If the `HTMLGenerator` service * specified is avaialable, this will @@ -107,12 +108,11 @@ class ShowHTML { * registered on the app, it won't throw * an error, but just send `null` to * the service constructor. - * @return {Middleware} */ -const showHTMLCustom = ( +const showHTML = middlewareCreator(( file, htmlGeneratorServiceName = 'htmlGenerator' -) => middleware((app) => { +) => (app) => { let htmlGenerator; try { htmlGenerator = app.get(htmlGeneratorServiceName); @@ -126,14 +126,8 @@ const showHTMLCustom = ( htmlGenerator ).middleware(); }); -/** - * A middleware for showing an `index.html` file. - * @type {Middleware} - */ -const showHTML = showHTMLCustom(); module.exports = { ShowHTML, showHTML, - showHTMLCustom, }; diff --git a/tests/middlewares/html/showHTML.test.js b/tests/middlewares/html/showHTML.test.js index 582662e0..f2421f91 100644 --- a/tests/middlewares/html/showHTML.test.js +++ b/tests/middlewares/html/showHTML.test.js @@ -4,7 +4,7 @@ jest.unmock('/src/middlewares/html/showHTML'); require('jasmine-expect'); const { ShowHTML, - showHTMLCustom, + showHTML, } = require('/src/middlewares/html/showHTML'); describe('middlewares/html:showHTML', () => { @@ -144,7 +144,7 @@ describe('middlewares/html:showHTML', () => { 'sendFile', ]; // When - middleware = showHTMLCustom().connect(app); + middleware = showHTML.connect(app); toCompare = new ShowHTML(); // Then expect(middleware.toString()).toEqual(toCompare.middleware().toString()); @@ -153,4 +153,35 @@ describe('middlewares/html:showHTML', () => { expect(app.get).toHaveBeenCalledWith(service); }); }); + + it('should include a middleware creator shorthand to configure its options', () => { + // Given + const htmlGeneratorServiceName = 'someCustomService'; + const htmlGeneratorService = { + getFile: jest.fn(() => ''), + }; + const app = { + get: jest.fn((service) => ( + service === htmlGeneratorServiceName ? + htmlGeneratorService : + service + )), + }; + let middleware = null; + let toCompare = null; + const expectedGets = [ + htmlGeneratorServiceName, + 'sendFile', + ]; + // When + middleware = showHTML('some-file', htmlGeneratorServiceName).connect(app); + toCompare = new ShowHTML(); + // Then + expect(middleware.toString()).toEqual(toCompare.middleware().toString()); + expect(app.get).toHaveBeenCalledTimes(expectedGets.length); + expectedGets.forEach((service) => { + expect(app.get).toHaveBeenCalledWith(service); + }); + expect(htmlGeneratorService.getFile).toHaveBeenCalledTimes(1); + }); }); From 78e262bbdd87de232a88439088d8bb4709897396 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 03:46:48 -0300 Subject: [PATCH 28/78] refactor(middlewares/utils/versionValidator): use the new middleware creator --- src/middlewares/utils/versionValidator.js | 18 ++++++------------ .../middlewares/utils/versionValidator.test.js | 3 +-- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/middlewares/utils/versionValidator.js b/src/middlewares/utils/versionValidator.js index 5e857a0f..0f7f4ea2 100644 --- a/src/middlewares/utils/versionValidator.js +++ b/src/middlewares/utils/versionValidator.js @@ -1,6 +1,6 @@ const ObjectUtils = require('wootils/shared/objectUtils'); const statuses = require('statuses'); -const { middleware } = require('../../utils/wrappers'); +const { middlewareCreator } = require('../../utils/wrappers'); /** * @typdef {Object} VersionValidatorLatestOptions @@ -192,18 +192,19 @@ class VersionValidator { } } /** - * Generates a middleware/controller with customized options. - * The reason for "middleware/controller" is because the wrappers for both are the same, the + * A middleware that will validate a `version` request parameter against the app version and + * generate an error if they don't match. + * This is a "middleware/controller" is because the wrappers for both are the same, the * difference is that, for controllers, Jimpex sends a second parameter with the point where they * are mounted. * By validating the point parameter, the function can know whether the implementation is going * to use the middleware by itself or as a route middleware. * If used as middleware, it will just return the result of {@link VersionValidator#middleware}; * but if used as controller, it will mount it on `[point]/:version/*`. + * @type {MiddlewareCreator} * @param {VersionValidatorOptions} [options] Custom options to modify the middleware behavior. - * @return {Middleware} */ -const versionValidatorCustom = (options) => middleware((app, point) => { +const versionValidator = middlewareCreator((options) => (app, point) => { // Get the middleware function. const middlewareValidator = (new VersionValidator( app.get('appConfiguration').get('version'), @@ -228,15 +229,8 @@ const versionValidatorCustom = (options) => middleware((app, point) => { // Return the route or the middleware. return result; }); -/** - * A middleware that will validate a `version` request parameter against the app version and - * generate an error if they don't match. - * @var {Middleware} - */ -const versionValidator = versionValidatorCustom(); module.exports = { VersionValidator, versionValidator, - versionValidatorCustom, }; diff --git a/tests/middlewares/utils/versionValidator.test.js b/tests/middlewares/utils/versionValidator.test.js index dadb4f9d..b44ae505 100644 --- a/tests/middlewares/utils/versionValidator.test.js +++ b/tests/middlewares/utils/versionValidator.test.js @@ -6,7 +6,6 @@ const statuses = require('statuses'); const { VersionValidator, versionValidator, - versionValidatorCustom, } = require('/src/middlewares/utils/versionValidator'); describe('services/api:versionValidator', () => { @@ -295,7 +294,7 @@ describe('services/api:versionValidator', () => { 'AppError', ]; // When - sut = versionValidatorCustom().connect(app); + sut = versionValidator().connect(app); toCompare = new VersionValidator('25.09'); // Then expect(sut.toString()).toEqual(toCompare.middleware().toString()); From 7cbc988195c75c8996c4039fb98f450b3dd05fce Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 03:58:26 -0300 Subject: [PATCH 29/78] refactor(services/api/client): use the new provider creator --- src/services/api/client.js | 24 ++++------------ src/services/api/index.js | 3 +- tests/services/api/client.test.js | 46 +++++++++++++++++++++++++++++-- tests/services/api/index.test.js | 21 +++++++------- 4 files changed, 62 insertions(+), 32 deletions(-) diff --git a/src/services/api/client.js b/src/services/api/client.js index d7a2c1e6..099ad65a 100644 --- a/src/services/api/client.js +++ b/src/services/api/client.js @@ -1,5 +1,5 @@ const APIClientBase = require('wootils/shared/apiClient'); -const { provider } = require('../../utils/wrappers'); +const { providerCreator } = require('../../utils/wrappers'); /** * An API client for the app to use. What makes this service special is that its that it formats * the received errors using the `AppError` service class and as fetch function it uses the @@ -45,40 +45,28 @@ class APIClient extends APIClientBase { } } /** - * Generates a provider with customized options. This allows the app to have multiple clients for - * different APIs. + * An API Client service to make requests to an API using endpoints defined on the app + * configuration. + * @type {ProviderCreator} * @param {string} [name='apiClient'] The name of the service that will be registered into * the app. * @param {string} [configurationKey='api'] The name of the app configuration setting that has the * API information. * @param {Class} [ClientClass=APIClient] The Class the service will instantiate. - * @return {Provider} */ -const apiClientCustom = ( +const apiClient = providerCreator(( name = 'apiClient', configurationKey = 'api', ClientClass = APIClient -) => provider((app) => { +) => (app) => { app.set(name, () => new ClientClass( app.get('appConfiguration').get(configurationKey), app.get('http'), app.get('HTTPError') )); }); -/** - * The service provider that once registered on the app container will set an instance of - * `APIClient` as the `apiClient` service. - * @example - * // Register it on the container - * container.register(apiClient); - * // Getting access to the service instance - * const apiClient = container.get('apiClient'); - * @type {Provider} - */ -const apiClient = apiClientCustom(); module.exports = { APIClient, apiClient, - apiClientCustom, }; diff --git a/src/services/api/index.js b/src/services/api/index.js index 7961719c..191421dc 100644 --- a/src/services/api/index.js +++ b/src/services/api/index.js @@ -1,4 +1,4 @@ -const { apiClient, apiClientCustom } = require('./client'); +const { apiClient } = require('./client'); const { ensureBearerAuthentication } = require('./ensureBearerAuthentication'); const { provider } = require('../../utils/wrappers'); /** @@ -14,7 +14,6 @@ const all = provider((app) => { module.exports = { apiClient, - apiClientCustom, ensureBearerAuthentication, all, }; diff --git a/tests/services/api/client.test.js b/tests/services/api/client.test.js index cdf57f11..9b60d76f 100644 --- a/tests/services/api/client.test.js +++ b/tests/services/api/client.test.js @@ -4,7 +4,7 @@ jest.unmock('/src/services/api/client'); require('jasmine-expect'); const { APIClient, - apiClientCustom, + apiClient, } = require('/src/services/api/client'); const APIClientBase = require('wootils/shared/apiClient'); @@ -68,6 +68,48 @@ describe('services/api:client', () => { }); it('should include a provider for the DIC', () => { + // Given + const appConfiguration = { + api: { + url: 'my-api', + endpoints: { + info: 'api-info', + }, + }, + get: jest.fn(() => appConfiguration.api), + }; + const http = { + fetch: 'fetch', + }; + const services = { + appConfiguration, + http, + }; + const app = { + set: jest.fn(), + get: jest.fn((service) => (services[service] || service)), + }; + let sut = null; + let serviceName = null; + let serviceFn = null; + // When + apiClient.register(app); + [[serviceName, serviceFn]] = app.set.mock.calls; + sut = serviceFn(); + // Then + expect(sut).toBeInstanceOf(APIClientBase); + expect(sut).toBeInstanceOf(APIClient); + expect(sut.apiConfig).toBe(appConfiguration.api); + expect(sut.url).toBe(appConfiguration.api.url); + expect(sut.endpoints).toEqual(appConfiguration.api.endpoints); + expect(sut.HTTPError).toBe('HTTPError'); + expect(sut.fetchClient).toBe(http.fetch); + expect(serviceName).toBe('apiClient'); + expect(appConfiguration.get).toHaveBeenCalledTimes(1); + expect(appConfiguration.get).toHaveBeenCalledWith('api'); + }); + + it('should include a provider creactor to configure its options', () => { // Given const appConfiguration = { apiConfig: { @@ -96,7 +138,7 @@ describe('services/api:client', () => { let serviceName = null; let serviceFn = null; // When - apiClientCustom(name, configurationKey, ClientClass).register(app); + apiClient(name, configurationKey, ClientClass).register(app); [[serviceName, serviceFn]] = app.set.mock.calls; sut = serviceFn(); // Then diff --git a/tests/services/api/index.test.js b/tests/services/api/index.test.js index 17ddc977..46152eb7 100644 --- a/tests/services/api/index.test.js +++ b/tests/services/api/index.test.js @@ -10,19 +10,20 @@ describe('services:api', () => { const app = { register: jest.fn(), }; - const expectedServices = [ - 'apiClient', - 'ensureBearerAuthentication', - ]; + const expectedServices = { + apiClient: expect.any(Function), + ensureBearerAuthentication: { + register: expect.any(Function), + }, + }; + const expectedServicesNames = Object.keys(expectedServices); // When apiServices.all.register(app); // When/Then - expect(app.register).toHaveBeenCalledTimes(expectedServices.length); - expectedServices.forEach((service, index) => { - const registeredService = app.register.mock.calls[index][0]; - expect(registeredService).toEqual({ - register: expect.any(Function), - }); + expect(app.register).toHaveBeenCalledTimes(expectedServicesNames.length); + expectedServicesNames.forEach((service, index) => { + const [registeredService] = app.register.mock.calls[index]; + expect(registeredService).toEqual(expectedServices[service]); expect(registeredService.toString()).toBe(apiServices[service].toString()); }); }); From 3d69cc26e7a921eff174b9f7babed925597f07e7 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 04:03:27 -0300 Subject: [PATCH 30/78] refactor(services/html/htmlGenerator): use the new provider creator --- src/services/html/htmlGenerator.js | 27 +++++------------------ src/services/html/index.js | 3 +-- tests/services/html/htmlGenerator.test.js | 10 ++++----- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/src/services/html/htmlGenerator.js b/src/services/html/htmlGenerator.js index 4a4faa82..d242585d 100644 --- a/src/services/html/htmlGenerator.js +++ b/src/services/html/htmlGenerator.js @@ -1,6 +1,6 @@ const ObjectUtils = require('wootils/shared/objectUtils'); const { deferred } = require('wootils/shared'); -const { provider } = require('../../utils/wrappers'); +const { providerCreator } = require('../../utils/wrappers'); /** * @typedef {Object} HTMLGeneratorOptions The options to customize the an `HTMLGenerator` service. * @property {string} [template='index.tpl.html'] The name of the file it should @@ -288,22 +288,21 @@ class HTMLGenerator { } } /** - * Generates an `HTMLGenerator` service provider with customized options and that automatically - * hooks itself to the `after-start` event of the app server in order to trigger the generation of - * the html file when the server starts. + * A service that hooks itself to the `after-start` event of the app server in order to trigger + * the generation an the html file when the server starts. + * @type {ProviderCreator} * @param {HTMLGeneratorOptions} [options={}] Options to customize the service. * @param {string} [serviceName='htmlGenerator'] The name of the service that will * be register into the app. * @param {?string} [valuesServiceName=null] The name of a service used to read * the values that will be injected in * the generated file. - * @return {Provider} */ -const htmlGeneratorCustom = ( +const htmlGenerator = providerCreator(( options = {}, serviceName = 'htmlGenerator', valuesServiceName = null -) => provider((app) => { +) => (app) => { app.set(serviceName, () => { let valuesService = null; if (valuesServiceName) { @@ -322,22 +321,8 @@ const htmlGeneratorCustom = ( app.get('events') .once('after-start', () => app.get(serviceName).generateHTML()); }); -/** - * The service provider that once registered on the app container will set an instance of - * `HTMLGenerator` as the `htmlGenerator` service. It also hooks itself to the `after-start` - * event of the app server in order to trigger the generation of - * the html file when the server starts. - * @example - * // Register it on the container - * container.register(htmlGenerator); - * // Getting access to the service instance - * const htmlGenerator = container.get('htmlGenerator'); - * @type {Provider} - */ -const htmlGenerator = htmlGeneratorCustom(); module.exports = { HTMLGenerator, htmlGenerator, - htmlGeneratorCustom, }; diff --git a/src/services/html/index.js b/src/services/html/index.js index 49c6ff18..55a40717 100644 --- a/src/services/html/index.js +++ b/src/services/html/index.js @@ -1,6 +1,5 @@ -const { htmlGenerator, htmlGeneratorCustom } = require('./htmlGenerator'); +const { htmlGenerator } = require('./htmlGenerator'); module.exports = { htmlGenerator, - htmlGeneratorCustom, }; diff --git a/tests/services/html/htmlGenerator.test.js b/tests/services/html/htmlGenerator.test.js index 4f03036c..bf231e2f 100644 --- a/tests/services/html/htmlGenerator.test.js +++ b/tests/services/html/htmlGenerator.test.js @@ -7,7 +7,7 @@ jest.unmock('/src/services/html/htmlGenerator'); require('jasmine-expect'); const { HTMLGenerator, - htmlGeneratorCustom, + htmlGenerator, } = require('/src/services/html/htmlGenerator'); describe('services/html:htmlGenerator', () => { @@ -438,7 +438,7 @@ describe('services/html:htmlGenerator', () => { }); }); - it('should register the generator to be runned when the server starts', () => { + it('should register the generator to be executed when the server starts', () => { // Given const appConfiguration = { get: jest.fn(() => {}), @@ -456,7 +456,7 @@ describe('services/html:htmlGenerator', () => { once: jest.fn(), }; let sut = null; - const name = 'myHTMLGenerator'; + const name = 'htmlGenerator'; const services = { appConfiguration, appLogger, @@ -472,7 +472,7 @@ describe('services/html:htmlGenerator', () => { let eventName = null; let eventFn = null; // When - htmlGeneratorCustom({}, name).register(app); + htmlGenerator.register(app); [[serviceName, serviceFn]] = app.set.mock.calls; [[eventName, eventFn]] = events.once.mock.calls; sut = serviceFn(); @@ -542,7 +542,7 @@ describe('services/html:htmlGenerator', () => { const expectedGets = Object.keys(services); const expectedEventName = 'after-start'; // When - htmlGeneratorCustom({}, name, myValuesServiceName).register(app); + htmlGenerator({}, name, myValuesServiceName).register(app); [[serviceName, serviceFn]] = app.set.mock.calls; [[eventName, eventFn]] = events.once.mock.calls; sut = serviceFn(); From fba4a359ed80f523df56793337118b6f6ae8c161 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 04:04:22 -0300 Subject: [PATCH 31/78] fix(middlewares/utils): remove old export --- src/middlewares/utils/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/middlewares/utils/index.js b/src/middlewares/utils/index.js index c1216a68..8241f246 100644 --- a/src/middlewares/utils/index.js +++ b/src/middlewares/utils/index.js @@ -1,6 +1,5 @@ -const { versionValidator, versionValidatorCustom } = require('./versionValidator'); +const { versionValidator } = require('./versionValidator'); module.exports = { versionValidator, - versionValidatorCustom, }; From 7e176b324090c7b243ed7890a9d3d391ec97c515 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 04:16:47 -0300 Subject: [PATCH 32/78] feat(utils/wrappers): add flag properties to identify the type of resources --- src/utils/wrappers.js | 55 ++++++++++++++++++----------- tests/services/api/index.test.js | 1 + tests/services/common/index.test.js | 1 + tests/services/http/index.test.js | 1 + tests/utils/wrappers.test.js | 9 +++++ 5 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/utils/wrappers.js b/src/utils/wrappers.js index 3c133693..2e8c0abb 100644 --- a/src/utils/wrappers.js +++ b/src/utils/wrappers.js @@ -5,6 +5,8 @@ * {@link Jimpex#register} and is inherit from {@link Jimple}. * @property {ProviderRegistrationCallback} register The function Jimpex calls when registering * the provider. + * @property {Boolean} provider A flag set to `true` to identify the resource + * as a service provider. */ /** @@ -32,8 +34,10 @@ * @typedef {Object} Controller * @description An object that when mounted on Jimpex will take care of handling a list of specific * routes. The method Jimpex uses to mount a controller is {@link Jimpex#mount}. - * @property {ControllerMountCallback} connect The function Jimpex calls when mounting the - * controller. + * @property {ControllerMountCallback} connect The function Jimpex calls when mounting the + * controller. + * @property {Boolean} controller A flag set to `true` to identify the resource + * as a routes controller. */ /** @@ -63,8 +67,10 @@ * @typedef {Object} Middleware * @description An object that when mounted on Jimpex add an {@link ExpressMiddleware} to the app. * The method Jimpex uses to mount a middleware is {@link Jimpex#use}. - * @property {MiddlewareUseCallback} connect The function Jimpex calls when mounting the - * middleware. + * @property {MiddlewareUseCallback} connect The function Jimpex calls when mounting the + * middleware. + * @property {Boolean} middleware A flag set to `true` to identify the resource + * as a middleware. */ /** @@ -93,13 +99,16 @@ /** * This is a helper the wrappers use in order to create an object by placing a given function * on an specific key. - * @param {string} key The key in which the function will be placed. - * @param {function} fn The function to insert in the object. + * @param {string} name The name of the resource. It will also be added to the object as a + * property with the value of `true`. + * @param {string} key The key in which the function will be placed. + * @param {function} fn The function to insert in the object. * @return {Object} * @ignore */ -const resource = (key, fn) => ({ +const resource = (name, key, fn) => ({ [key]: fn, + [name]: true, }); /** * Similar to `resource`, this helper creates an "object" and places a given function on an @@ -108,13 +117,16 @@ const resource = (key, fn) => ({ * property and return the result of the function. * * This is kind of hard to explain, so let's compare it with `resource` and use a proper example: - * - `resource`: (key, fn) => ({ [key]: fn }) - * - `resourceCreator`: ((key, creatorFn) => creatorFn(...) => fn)[key]: creatorFn() + * - `resource`: (name, key, fn) => ({ [key]: fn, [name]: true }) + * - `resourceCreator`: ((name, key, creatorFn) => creatorFn(...) => fn)[key]: creatorFn() * * While `resource` is meant to create objects with a resource function, this is meant to create * those resource functions by sending them "optional paramters", and they are optionals because * if you access the `key` property, it would be the same as calling the `creatorFn` without * paramters. + * @param {string} name The name of the resource, to be added as a property of both the + * generated resource and the one with the proxy. The value of the + * property will be `true`. * @param {string} key The key in which the creator function will be placed in case it's * used without parameters; and also the key in which the result * function from the creator will be placed if called with parameters. @@ -122,13 +134,16 @@ const resource = (key, fn) => ({ * @return {function} * @ignore */ -const resourceCreator = (key, creatorFn) => new Proxy( - (...args) => resource(key, creatorFn(...args)), +const resourceCreator = (name, key, creatorFn) => new Proxy( + (...args) => resource(name, key, creatorFn(...args)), { + name, resource: null, get(target, property) { let result; - if (property === key) { + if (property === this.name) { + result = true; + } else if (property === key) { if (this.resource === null) { this.resource = creatorFn(); } @@ -147,7 +162,7 @@ const resourceCreator = (key, creatorFn) => new Proxy( * app registers the provider. * @return {Provider} */ -const provider = (registerFn) => resource('register', registerFn); +const provider = (registerFn) => resource('provider', 'register', registerFn); /** * Generates a configurable service provider for the app container. It's configurable because * the creator, instead of just being sent to the container to register, it can also be called @@ -159,7 +174,7 @@ const provider = (registerFn) => resource('register', registerFn); * @param {ProviderCreatorCallback} creatorFn The function that generates the provider. * @return {ProviderCreator} */ -const providerCreator = (creatorFn) => resourceCreator('register', creatorFn); +const providerCreator = (creatorFn) => resourceCreator('provider', 'register', creatorFn); /** * Generates a routes controller for the app container to mount. * @param {ControllerMountCallback} connect A function that will be called the moment the app @@ -167,7 +182,7 @@ const providerCreator = (creatorFn) => resourceCreator('register', creatorFn); * routes. * @return {Controller} */ -const controller = (connectFn) => resource('connect', connectFn); +const controller = (connectFn) => resource('controller', 'connect', connectFn); /** * Generates a configurable routes controller for the app to mount. It's configurable because * the creator, instead of just being sent to the container to mount, it can also be called @@ -178,10 +193,10 @@ const controller = (connectFn) => resource('connect', connectFn); * const ctrl = new MyController(options); * return [router.get('...', ctrl.doSomething())]; * }); - * @param {ProviderCreatorCallback} creatorFn The function that generates the provider. + * @param {ProviderCreatorCallback} creatorFn The function that generates the controller. * @return {ProviderCreator} */ -const controllerCreator = (creatorFn) => resourceCreator('connect', creatorFn); +const controllerCreator = (creatorFn) => resourceCreator('controller', 'connect', creatorFn); /** * Generates a middleware for the app to use. * @param {function(app:Jimpex):?ExpressMiddleware} connect A function that will be called the @@ -189,7 +204,7 @@ const controllerCreator = (creatorFn) => resourceCreator('connect', creatorFn); * It should return an Express middleware. * @return {Middleware} */ -const middleware = (connectFn) => resource('connect', connectFn); +const middleware = (connectFn) => resource('middleware', 'connect', connectFn); /** * Generates a configurable middleware for the app to use. It's configurable because the creator, * instead of just being sent to the container to use, it can also be called as a function with @@ -200,10 +215,10 @@ const middleware = (connectFn) => resource('connect', connectFn); * (req, res, next) => {} : * null * )); - * @param {ProviderCreatorCallback} creatorFn The function that generates the provider. + * @param {ProviderCreatorCallback} creatorFn The function that generates the middleware. * @return {ProviderCreator} */ -const middlewareCreator = (creatorFn) => resourceCreator('connect', creatorFn); +const middlewareCreator = (creatorFn) => resourceCreator('middleware', 'connect', creatorFn); module.exports = { provider, diff --git a/tests/services/api/index.test.js b/tests/services/api/index.test.js index 46152eb7..4e75a5e0 100644 --- a/tests/services/api/index.test.js +++ b/tests/services/api/index.test.js @@ -14,6 +14,7 @@ describe('services:api', () => { apiClient: expect.any(Function), ensureBearerAuthentication: { register: expect.any(Function), + provider: true, }, }; const expectedServicesNames = Object.keys(expectedServices); diff --git a/tests/services/common/index.test.js b/tests/services/common/index.test.js index 3906362c..f07f0d5a 100644 --- a/tests/services/common/index.test.js +++ b/tests/services/common/index.test.js @@ -23,6 +23,7 @@ describe('services:common', () => { const registeredService = app.register.mock.calls[index][0]; expect(registeredService).toEqual({ register: expect.any(Function), + provider: true, }); expect(registeredService.toString()).toBe(commonServices[service].toString()); }); diff --git a/tests/services/http/index.test.js b/tests/services/http/index.test.js index 1a36b01b..fa7aabbe 100644 --- a/tests/services/http/index.test.js +++ b/tests/services/http/index.test.js @@ -22,6 +22,7 @@ describe('services:http', () => { const registeredService = app.register.mock.calls[index][0]; expect(registeredService).toEqual({ register: expect.any(Function), + provider: true, }); expect(registeredService.toString()).toBe(httpServices[service].toString()); }); diff --git a/tests/utils/wrappers.test.js b/tests/utils/wrappers.test.js index c8c13265..69105f05 100644 --- a/tests/utils/wrappers.test.js +++ b/tests/utils/wrappers.test.js @@ -21,6 +21,7 @@ describe('utils/wrappers', () => { // Then expect(result).toEqual({ register: serviceProvider, + provider: true, }); }); @@ -37,6 +38,7 @@ describe('utils/wrappers', () => { // Then expect(result).toEqual({ register: serviceProvider, + provider: true, }); expect(creatorFn).toHaveBeenCalledTimes(1); expect(creatorFn).toHaveBeenCalledWith(options); @@ -51,6 +53,7 @@ describe('utils/wrappers', () => { creator = providerCreator(creatorFn); // Then expect(creator.register).toBe(serviceProvider); + expect(creator.provider).toBeTrue(); expect(creator.anyOtherProp).toBeUndefined(); // to validate the getter. expect(creatorFn).toHaveBeenCalledTimes(1); expect(creatorFn).toHaveBeenCalledWith(); @@ -81,6 +84,7 @@ describe('utils/wrappers', () => { // Then expect(result).toEqual({ connect: routeController, + controller: true, }); }); @@ -97,6 +101,7 @@ describe('utils/wrappers', () => { // Then expect(result).toEqual({ connect: routeController, + controller: true, }); expect(creatorFn).toHaveBeenCalledTimes(1); expect(creatorFn).toHaveBeenCalledWith(options); @@ -111,6 +116,7 @@ describe('utils/wrappers', () => { creator = controllerCreator(creatorFn); // Then expect(creator.connect).toBe(routeController); + expect(creator.controller).toBeTrue(); expect(creatorFn).toHaveBeenCalledTimes(1); expect(creatorFn).toHaveBeenCalledWith(); }); @@ -126,6 +132,7 @@ describe('utils/wrappers', () => { // Then expect(result).toEqual({ connect: appMiddleware, + middleware: true, }); }); @@ -142,6 +149,7 @@ describe('utils/wrappers', () => { // Then expect(result).toEqual({ connect: appMiddleware, + middleware: true, }); expect(creatorFn).toHaveBeenCalledTimes(1); expect(creatorFn).toHaveBeenCalledWith(options); @@ -156,6 +164,7 @@ describe('utils/wrappers', () => { creator = middlewareCreator(creatorFn); // Then expect(creator.connect).toBe(appMiddleware); + expect(creator.middleware).toBeTrue(); expect(creatorFn).toHaveBeenCalledTimes(1); expect(creatorFn).toHaveBeenCalledWith(); }); From 57c5db916bdf30624249ecd96b4c101afd987de9 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 04:17:42 -0300 Subject: [PATCH 33/78] docs(app): add the creators types --- src/app/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/index.js b/src/app/index.js index 0dbf24cb..c238f641 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -119,8 +119,8 @@ class Jimpex extends Jimple { } /** * Mount a controller on a route point. - * @param {string} point The route for the controller. - * @param {Controller} controller The route controller. + * @param {string} point The route for the controller. + * @param {Controller|ControllerCreator} controller The route controller. */ mount(point, controller) { this.mountQueue.push( @@ -131,7 +131,7 @@ class Jimpex extends Jimple { } /** * Add a middleware. - * @param {Middleware} middleware [description] + * @param {Middleware|MiddlewareCreator} middleware The middleware to use. */ use(middleware) { this.mountQueue.push((server) => { From 8350bfb1e99cf41a1723d952ae746ef6b57afaca Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 05:00:45 -0300 Subject: [PATCH 34/78] feat(utils/wrappers): add providers collections --- src/utils/wrappers.js | 38 +++++++++++++++++++++++++++++++ tests/utils/wrappers.test.js | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/src/utils/wrappers.js b/src/utils/wrappers.js index 2e8c0abb..7d49c933 100644 --- a/src/utils/wrappers.js +++ b/src/utils/wrappers.js @@ -163,6 +163,43 @@ const resourceCreator = (name, key, creatorFn) => new Proxy( * @return {Provider} */ const provider = (registerFn) => resource('provider', 'register', registerFn); +/** + * Generates a collection of service providers that can be registered all at once or one by one. + * You can send the collection directly to the `.register()` and it will register all its + * "children"; and if you want to register one provider at a time, you can access them by name, + * as the collection is a regular object. + * @example + * const collection = providers({ one: providerOne, two: providerTwo }); + * // Register all at once + * app.register(collection); + * // Register one by one + * app.register(collection.one); + * app.register(collection.two); + * @param {Object} items A dictionary of service providers; the keys will be for the collection + * object, and the values the one that will get send to `.register()`. + * @return {Provider} + */ +const providers = (items) => { + const invalidNames = Object.keys(items).some((name) => ( + ['register', 'providers'].includes(name) + )); + if (invalidNames) { + throw new Error( + 'You can\'t create a collection with a providers called `register` or `providers`' + ); + } + + return Object.assign( + resource( + 'providers', + 'register', + (app) => Object.keys(items).forEach((item) => { + app.register(items[item]); + }) + ), + items + ); +}; /** * Generates a configurable service provider for the app container. It's configurable because * the creator, instead of just being sent to the container to register, it can also be called @@ -223,6 +260,7 @@ const middlewareCreator = (creatorFn) => resourceCreator('middleware', 'connect' module.exports = { provider, providerCreator, + providers, controller, controllerCreator, middleware, diff --git a/tests/utils/wrappers.test.js b/tests/utils/wrappers.test.js index 69105f05..d935d27a 100644 --- a/tests/utils/wrappers.test.js +++ b/tests/utils/wrappers.test.js @@ -4,6 +4,7 @@ require('jasmine-expect'); const { provider, providerCreator, + providers, controller, controllerCreator, middleware, @@ -72,6 +73,49 @@ describe('utils/wrappers', () => { expect(creatorFn).toHaveBeenCalledTimes(1); expect(creatorFn).toHaveBeenCalledWith(); }); + + it('should create a providers collection', () => { + // Given + const serviceProviderOne = 'serviceProviderOne'; + const serviceProviderTwo = 'serviceProviderTwo'; + let collection = null; + // When + collection = providers({ + one: serviceProviderOne, + two: serviceProviderTwo, + }); + // Then + expect(collection.one).toBe(serviceProviderOne); + expect(collection.two).toBe(serviceProviderTwo); + expect(collection.three).toBeUndefined(); // to validate the getter. + }); + + it('should create a collection to register multiple providers at once', () => { + // Given + const app = { + register: jest.fn(), + }; + const services = { + one: 'serviceProviderOne', + two: 'serviceProviderTwo', + }; + const servicesNames = Object.keys(services); + let collection = null; + // When + collection = providers(services); + collection.register(app); + // Then + expect(app.register).toHaveBeenCalledTimes(servicesNames.length); + servicesNames.forEach((name) => { + expect(app.register).toHaveBeenCalledWith(services[name]); + }); + }); + + it('should throw an error when trying to create a collection with invalid providers', () => { + // Given/When/Then + expect(() => providers({ providers: 'something' })) + .toThrow(/You can't create a collection with a providers called `register` or `providers`/i); + }); }); describe('controller', () => { From bd8c9bfb3acd8019f9feab5711ac7d36cf29cba5 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 05:10:59 -0300 Subject: [PATCH 35/78] refactor(services): use the new providers collections --- src/app/index.js | 6 +++--- src/services/api/index.js | 23 +++++++++++------------ src/services/common/index.js | 21 +++++++++------------ src/services/http/index.js | 19 ++++++++----------- tests/app/index.test.js | 6 +++--- tests/services/api/index.test.js | 2 +- tests/services/common/index.test.js | 2 +- tests/services/http/index.test.js | 2 +- 8 files changed, 37 insertions(+), 44 deletions(-) diff --git a/src/app/index.js b/src/app/index.js index c238f641..93c18621 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -278,15 +278,15 @@ class Jimpex extends Jimple { const { defaultServices } = this.options; if (defaultServices.api) { - this.register(apiServices.all); + this.register(apiServices); } if (defaultServices.common) { - this.register(commonServices.all); + this.register(commonServices); } if (defaultServices.http) { - this.register(httpServices.all); + this.register(httpServices); } this.set('events', () => new EventsHub()); diff --git a/src/services/api/index.js b/src/services/api/index.js index 191421dc..9d57f035 100644 --- a/src/services/api/index.js +++ b/src/services/api/index.js @@ -1,19 +1,18 @@ const { apiClient } = require('./client'); const { ensureBearerAuthentication } = require('./ensureBearerAuthentication'); -const { provider } = require('../../utils/wrappers'); +const { providers } = require('../../utils/wrappers'); + /** - * A single service provider that once registered on the app container will take care of - * registering the providers for the `apiClient`, 'ensureBearerAuthentication' and - * `versionValidator` services. + * The providers collection for the API services. * @type {Provider} + * @property {Provider} apiClient + * The provider for {@link APIClient}. + * @property {Provider} ensureBearerAuthentication + * The provider for {@link EnsureBearerAuthentication}. */ -const all = provider((app) => { - app.register(apiClient); - app.register(ensureBearerAuthentication); -}); - -module.exports = { +const apiServices = providers({ apiClient, ensureBearerAuthentication, - all, -}; +}); + +module.exports = apiServices; diff --git a/src/services/common/index.js b/src/services/common/index.js index f0a9b250..f3d1a6a4 100644 --- a/src/services/common/index.js +++ b/src/services/common/index.js @@ -1,21 +1,18 @@ const { appError } = require('./appError'); const { httpError } = require('./httpError'); const { sendFileProvider } = require('./sendFile'); -const { provider } = require('../../utils/wrappers'); +const { providers } = require('../../utils/wrappers'); /** - * A single service provider that once registered on the app container will take care of - * registering the providers for the `appError` and `sendFileProvider` services. + * The providers collection for the common services. * @type {Provider} + * @property {Provider} appError The provider for {@link AppError}. + * @property {Provider} httpError The provider for {@link HTTPError}. + * @property {Provider} sendFile The provider for {@link SendFile}. */ -const all = provider((app) => { - app.register(appError); - app.register(httpError); - app.register(sendFileProvider); -}); - -module.exports = { +const commonServices = providers({ appError, httpError, sendFile: sendFileProvider, - all, -}; +}); + +module.exports = commonServices; diff --git a/src/services/http/index.js b/src/services/http/index.js index bd4003a3..b95e90d9 100644 --- a/src/services/http/index.js +++ b/src/services/http/index.js @@ -1,18 +1,15 @@ const { http } = require('./http'); const { responsesBuilder } = require('./responsesBuilder'); -const { provider } = require('../../utils/wrappers'); +const { providers } = require('../../utils/wrappers'); /** - * A single service provider that once registered on the app container will take care of - * registering the providers for the `http` and 'responsesBuilder' services. + * The providers collection for the HTTP services. * @type {Provider} + * @property {Provider} http The provider for {@link HTTP}. + * @property {Provider} responsesBuilder The provider for {@link ResponsesBuilder}. */ -const all = provider((app) => { - app.register(http); - app.register(responsesBuilder); -}); - -module.exports = { +const httpServices = providers({ http, responsesBuilder, - all, -}; +}); + +module.exports = httpServices; diff --git a/tests/app/index.test.js b/tests/app/index.test.js index d81fd5be..f9d2792c 100644 --- a/tests/app/index.test.js +++ b/tests/app/index.test.js @@ -11,9 +11,9 @@ jest.mock('compression', () => compressionMock); jest.mock('multer', () => multerMock); jest.mock('body-parser', () => bodyParserMock); jest.mock('wootils/node/providers', () => wootilsMock); -jest.mock('/src/services/api', () => ({ all: 'apiServices' })); -jest.mock('/src/services/common', () => ({ all: 'commonServices' })); -jest.mock('/src/services/http', () => ({ all: 'httpServices' })); +jest.mock('/src/services/api', () => 'apiServices'); +jest.mock('/src/services/common', () => 'commonServices'); +jest.mock('/src/services/http', () => 'httpServices'); jest.unmock('/src/app/index'); const path = require('path'); diff --git a/tests/services/api/index.test.js b/tests/services/api/index.test.js index 4e75a5e0..d47449a3 100644 --- a/tests/services/api/index.test.js +++ b/tests/services/api/index.test.js @@ -19,7 +19,7 @@ describe('services:api', () => { }; const expectedServicesNames = Object.keys(expectedServices); // When - apiServices.all.register(app); + apiServices.register(app); // When/Then expect(app.register).toHaveBeenCalledTimes(expectedServicesNames.length); expectedServicesNames.forEach((service, index) => { diff --git a/tests/services/common/index.test.js b/tests/services/common/index.test.js index f07f0d5a..5521f771 100644 --- a/tests/services/common/index.test.js +++ b/tests/services/common/index.test.js @@ -16,7 +16,7 @@ describe('services:common', () => { 'sendFile', ]; // When - commonServices.all.register(app); + commonServices.register(app); // When/Then expect(app.register).toHaveBeenCalledTimes(expectedServices.length); expectedServices.forEach((service, index) => { diff --git a/tests/services/http/index.test.js b/tests/services/http/index.test.js index fa7aabbe..7ef80249 100644 --- a/tests/services/http/index.test.js +++ b/tests/services/http/index.test.js @@ -15,7 +15,7 @@ describe('services:http', () => { 'responsesBuilder', ]; // When - httpServices.all.register(app); + httpServices.register(app); // When/Then expect(app.register).toHaveBeenCalledTimes(expectedServices.length); expectedServices.forEach((service, index) => { From 4e67abee1be047a6357ca86cd1fe2b156651c459 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 05:27:29 -0300 Subject: [PATCH 36/78] refator(app): make all properties protected with a public getter --- src/app/index.js | 73 ++++++++++++++++++++++++++--------------- tests/app/index.test.js | 7 ++++ 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/app/index.js b/src/app/index.js index 93c18621..a316f0c9 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -48,7 +48,7 @@ class Jimpex extends Jimple { * The app options. * @type {JimpexOptions} */ - this.options = ObjectUtils.merge({ + this._options = ObjectUtils.merge({ version: '0.0.0', filesizeLimit: '15MB', configuration: { @@ -81,15 +81,15 @@ class Jimpex extends Jimple { }, }, options); /** - * The Express app. + * The Express app Jimpex uses under the hood. * @type {Express} */ - this.express = express(); + this._express = express(); /** * When the app starts, this will be running instance. - * @type {Object} + * @type {?Object} */ - this.instance = null; + this._instance = null; /** * A list of functions that return controllers and middlewares. When the app starts, the * queue will be processed and those controllers and middlewares added to the app. @@ -98,7 +98,7 @@ class Jimpex extends Jimple { * could cause errors if they depend on services that are not yet registered. * @type {Array} */ - this.mountQueue = []; + this._mountQueue = []; this._setupCoreServices(); this._setupExpress(); @@ -109,6 +109,27 @@ class Jimpex extends Jimple { this.boot(); } } + /** + * The app options. + * @type {JimpexOptions} + */ + get options() { + return ObjectUtils.copy(this._options); + } + /** + * The Express app Jimpex uses under the hood. + * @type {Express} + */ + get express() { + return this._express; + } + /** + * The server instance that gets created when the app is started. + * @return {?Object} + */ + get instance() { + return this._instance; + } /** * This is where the app would register all its specific services, middlewares and controllers. * @throws {Error} if not overwritten. @@ -123,7 +144,7 @@ class Jimpex extends Jimple { * @param {Controller|ControllerCreator} controller The route controller. */ mount(point, controller) { - this.mountQueue.push( + this._mountQueue.push( (server) => controller.connect(this, point).forEach( (route) => server.use(point, route) ) @@ -134,7 +155,7 @@ class Jimpex extends Jimple { * @param {Middleware|MiddlewareCreator} middleware The middleware to use. */ use(middleware) { - this.mountQueue.push((server) => { + this._mountQueue.push((server) => { const middlewareHandler = middleware.connect(this); if (middlewareHandler) { server.use(middlewareHandler); @@ -151,7 +172,7 @@ class Jimpex extends Jimple { const config = this.get('appConfiguration'); const port = config.get('port'); this.emitEvent('before-start'); - this.instance = this.express.listen(port, () => { + this._instance = this._express.listen(port, () => { this.emitEvent('start'); this._mountResources(); this.get('appLogger').success(`Starting on port ${port}`); @@ -161,7 +182,7 @@ class Jimpex extends Jimple { return result; }); - return this.instance; + return this._instance; } /** * This is an alias of `start`. The idea is for it to be used on serverless platforms, where you @@ -198,10 +219,10 @@ class Jimpex extends Jimple { * Stops the server instance. */ stop() { - if (this.instance) { + if (this._instance) { this.emitEvent('before-stop'); - this.instance.close(); - this.instance = null; + this._instance.close(); + this._instance = null; this.emitEvent('after-stop'); } } @@ -232,17 +253,17 @@ class Jimpex extends Jimple { statics, filesizeLimit, express: expressOptions, - } = this.options; + } = this._options; if (expressOptions.trustProxy) { - this.express.enable('trust proxy'); + this._express.enable('trust proxy'); } if (expressOptions.disableXPoweredBy) { - this.express.disable('x-powered-by'); + this._express.disable('x-powered-by'); } if (expressOptions.compression) { - this.express.use(compression()); + this._express.use(compression()); } if (statics.enabled) { @@ -250,21 +271,21 @@ class Jimpex extends Jimple { const joinFrom = onHome ? 'home' : 'app'; const staticsRoute = route.startsWith('/') ? route.substr(1) : route; const staticsFolderPath = this.get('pathUtils').joinFrom(joinFrom, folder || staticsRoute); - this.express.use(`/${staticsRoute}`, express.static(staticsFolderPath)); + this._express.use(`/${staticsRoute}`, express.static(staticsFolderPath)); } if (expressOptions.bodyParser) { - this.express.use(bodyParser.json({ + this._express.use(bodyParser.json({ limit: filesizeLimit, })); - this.express.use(bodyParser.urlencoded({ + this._express.use(bodyParser.urlencoded({ extended: true, limit: filesizeLimit, })); } if (expressOptions.multer) { - this.express.use(multer().any()); + this._express.use(multer().any()); } this.set('router', this.factory(() => express.Router())); @@ -275,7 +296,7 @@ class Jimpex extends Jimple { * @access protected */ _setupDefaultServices() { - const { defaultServices } = this.options; + const { defaultServices } = this._options; if (defaultServices.api) { this.register(apiServices); @@ -297,7 +318,7 @@ class Jimpex extends Jimple { * @access protected */ _setupConfiguration() { - const { version, configuration: options } = this.options; + const { version, configuration: options } = this._options; const { name, environmentVariable, @@ -338,7 +359,7 @@ class Jimpex extends Jimple { } if (loadVersionFromConfiguration) { - this.options.version = this.get('appConfiguration').get('version'); + this._options.version = this.get('appConfiguration').get('version'); } } /** @@ -347,8 +368,8 @@ class Jimpex extends Jimple { * @access protected */ _mountResources() { - this.mountQueue.forEach((mountFn) => mountFn(this.express)); - this.mountQueue.length = 0; + this._mountQueue.forEach((mountFn) => mountFn(this._express)); + this._mountQueue.length = 0; } } diff --git a/tests/app/index.test.js b/tests/app/index.test.js index f9d2792c..b5ee1577 100644 --- a/tests/app/index.test.js +++ b/tests/app/index.test.js @@ -99,6 +99,8 @@ describe('app:Jimpex', () => { expectedServices.forEach((service) => { expect(sut.register).toHaveBeenCalledWith(service); }); + expect(sut.express).toBe(expressMock.mocks); + expect(expressMock).toHaveBeenCalledTimes(1); expect(expressMock.mocks.enable).toHaveBeenCalledTimes(1); expect(expressMock.mocks.enable).toHaveBeenCalledWith('trust proxy'); expect(expressMock.mocks.disable).toHaveBeenCalledTimes(1); @@ -706,6 +708,7 @@ describe('app:Jimpex', () => { }; JimpleMock.service('appLogger', appLogger); let sut = null; + let runningInstance = null; const expectedEvents = [ 'before-start', 'start', @@ -717,8 +720,12 @@ describe('app:Jimpex', () => { // When sut = new Sut(); sut.start(); + runningInstance = sut.instance; sut.stop(); // Then + expect(runningInstance).toEqual({ + close: expect.any(Function), + }); expect(events.emit).toHaveBeenCalledTimes(expectedEvents.length); expectedEvents.forEach((eventName) => { expect(events.emit).toHaveBeenCalledWith(eventName, sut); From 9e594c7a3d940140ce9f401ee1dd4523ba87bce4 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 05:33:06 -0300 Subject: [PATCH 37/78] refactor(controllers): make all the injected services protected --- src/controllers/common/configuration.js | 18 +++++++++------ src/controllers/common/health.js | 12 ++++++---- src/controllers/common/rootStatics.js | 22 +++++++++++-------- .../controllers/common/configuration.test.js | 7 +++--- tests/controllers/common/health.test.js | 5 ++--- tests/controllers/common/rootStatics.test.js | 5 +++-- 6 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/controllers/common/configuration.js b/src/controllers/common/configuration.js index 68a3e201..3ba4bc62 100644 --- a/src/controllers/common/configuration.js +++ b/src/controllers/common/configuration.js @@ -12,22 +12,26 @@ class ConfigurationController { /** * A local reference for the `appConfiguration` service. * @type {AppConfiguration} + * @access protected + * @ignore */ - this.appConfiguration = appConfiguration; + this._appConfiguration = appConfiguration; /** * A local reference for the `responsesBuilder` service. * @type {ResponsesBuilder} + * @access protected + * @ignore */ - this.responsesBuilder = responsesBuilder; + this._responsesBuilder = responsesBuilder; } /** * Send a response with the current app configuration as a body. * @param {ExpressResponse} res The server response. */ getConfigurationResponse(res) { - const name = this.appConfiguration.get('name'); - const data = Object.assign({ name }, this.appConfiguration.getConfig()); - return this.responsesBuilder.json(res, data); + const name = this._appConfiguration.get('name'); + const data = Object.assign({ name }, this._appConfiguration.getConfig()); + return this._responsesBuilder.json(res, data); } /** * Returns the middleware to show the current configuration. @@ -44,9 +48,9 @@ class ConfigurationController { */ switchConfiguration() { return (req, res, next) => { - if (this.appConfiguration.canSwitch()) { + if (this._appConfiguration.canSwitch()) { try { - this.appConfiguration.switch(req.params.name); + this._appConfiguration.switch(req.params.name); this.getConfigurationResponse(res); } catch (error) { next(error); diff --git a/src/controllers/common/health.js b/src/controllers/common/health.js index 497f25f7..3d953af9 100644 --- a/src/controllers/common/health.js +++ b/src/controllers/common/health.js @@ -15,13 +15,17 @@ class HealthController { /** * A local reference for the `appConfiguration` service. * @type {AppConfiguration} + * @access protected + * @ignore */ - this.appConfiguration = appConfiguration; + this._appConfiguration = appConfiguration; /** * A local reference for the `responsesBuilder` service. * @type {ResponsesBuilder} + * @access protected + * @ignore */ - this.responsesBuilder = responsesBuilder; + this._responsesBuilder = responsesBuilder; } /** * Returns the middleware that shows the health information. @@ -32,8 +36,8 @@ class HealthController { const { name: configuration, version, - } = this.appConfiguration.get(['name', 'version']); - this.responsesBuilder.json(res, { + } = this._appConfiguration.get(['name', 'version']); + this._responsesBuilder.json(res, { isHealthy: true, status: statuses.ok, configuration, diff --git a/src/controllers/common/rootStatics.js b/src/controllers/common/rootStatics.js index 1cb29f54..19154f79 100644 --- a/src/controllers/common/rootStatics.js +++ b/src/controllers/common/rootStatics.js @@ -23,20 +23,24 @@ class RootStaticsController { /** * A local reference for the `sendFile` service. * @type {SendFile} + * @access protected + * @ignore */ - this.sendFile = sendFile; + this._sendFile = sendFile; /** - * A dictionary with the file names as keys and information about the files as values. - * @type {Object} - */ - this.files = this._parseFiles(files); + * A dictionary with the file names as keys and information about the files as values. + * @type {Object} + * @access protected + * @ignore + */ + this._files = this._parseFiles(files); } /** * Gets the list of files the service will serve. * @return {Array} */ getFileEntries() { - return Object.keys(this.files); + return Object.keys(this._files); } /** * Generates a middleware to serve an specific file. @@ -45,12 +49,12 @@ class RootStaticsController { * @throws {Error} If the file wasn't sent on the constructor. */ serveFile(file) { - if (!this.files[file]) { + if (!this._files[file]) { throw new Error(`The required static file doesn't exist (${file})`); } return (req, res, next) => { - const item = this.files[file]; + const item = this._files[file]; const extension = item.output.split('.').pop().toLowerCase(); const baseHeaders = { 'Content-Type': mime.getType(extension) }; const headers = ObjectUtils.merge(baseHeaders, item.headers); @@ -59,7 +63,7 @@ class RootStaticsController { res.setHeader(headerName, headers[headerName]); }); - this.sendFile(res, item.output, next); + this._sendFile(res, item.output, next); }; } /** diff --git a/tests/controllers/common/configuration.test.js b/tests/controllers/common/configuration.test.js index f59bcafc..a10c62ce 100644 --- a/tests/controllers/common/configuration.test.js +++ b/tests/controllers/common/configuration.test.js @@ -8,7 +8,7 @@ const { } = require('/src/controllers/common/configuration'); describe('controllers/common:configuration', () => { - it('should be instantiated with all its dependencies', () => { + it('should be instantiated an have public methods', () => { // Given const appConfiguration = 'appConfiguration'; const responsesBuilder = 'responsesBuilder'; @@ -17,8 +17,9 @@ describe('controllers/common:configuration', () => { sut = new ConfigurationController(appConfiguration, responsesBuilder); // Then expect(sut).toBeInstanceOf(ConfigurationController); - expect(sut.appConfiguration).toBe(appConfiguration); - expect(sut.responsesBuilder).toBe(responsesBuilder); + expect(sut.getConfigurationResponse).toBeFunction(); + expect(sut.showConfiguration).toBeFunction(); + expect(sut.switchConfiguration).toBeFunction(); }); it('should have a generic method to generate a configuration reponse', () => { diff --git a/tests/controllers/common/health.test.js b/tests/controllers/common/health.test.js index 6e6cd409..c31d8275 100644 --- a/tests/controllers/common/health.test.js +++ b/tests/controllers/common/health.test.js @@ -9,7 +9,7 @@ const { } = require('/src/controllers/common/health'); describe('controllers/common:health', () => { - it('should be instantiated with all its dependencies', () => { + it('should be instantiated and have a public method', () => { // Given const appConfiguration = 'appConfiguration'; const responsesBuilder = 'responsesBuilder'; @@ -18,8 +18,7 @@ describe('controllers/common:health', () => { sut = new HealthController(appConfiguration, responsesBuilder); // Then expect(sut).toBeInstanceOf(HealthController); - expect(sut.appConfiguration).toBe(appConfiguration); - expect(sut.responsesBuilder).toBe(responsesBuilder); + expect(sut.health).toBeFunction(); }); it('should have a middleware to show "health" information', () => { diff --git a/tests/controllers/common/rootStatics.test.js b/tests/controllers/common/rootStatics.test.js index ca41f95f..542b95cf 100644 --- a/tests/controllers/common/rootStatics.test.js +++ b/tests/controllers/common/rootStatics.test.js @@ -8,7 +8,7 @@ const { } = require('/src/controllers/common/rootStatics'); describe('controllers/common:rootStatics', () => { - it('should be instantiated with all its dependencies', () => { + it('should be instantiated and have public methods', () => { // Given const sendFile = 'sendFile'; let sut = null; @@ -16,7 +16,8 @@ describe('controllers/common:rootStatics', () => { sut = new RootStaticsController(sendFile); // Then expect(sut).toBeInstanceOf(RootStaticsController); - expect(sut.sendFile).toBe(sendFile); + expect(sut.getFileEntries).toBeFunction(); + expect(sut.serveFile).toBeFunction(); }); it('should have a method to return all the files it will serve', () => { From 47e6890bef2b6e38a2641b661e8315419fabfeb7 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 05:47:24 -0300 Subject: [PATCH 38/78] refactor(middlewares): make all the injected services protected --- src/middlewares/common/errorHandler.js | 28 +++++++++------ src/middlewares/common/forceHTTPS.js | 13 +++++-- src/middlewares/html/fastHTML.js | 36 +++++++++++++------ src/middlewares/html/showHTML.js | 29 ++++++++++----- tests/middlewares/common/errorHandler.test.js | 6 ++-- tests/middlewares/common/forceHTTPS.test.js | 4 +-- tests/middlewares/html/fastHTML.test.js | 10 +++--- tests/middlewares/html/showHTML.test.js | 5 +-- 8 files changed, 87 insertions(+), 44 deletions(-) diff --git a/src/middlewares/common/errorHandler.js b/src/middlewares/common/errorHandler.js index b4694c93..b86afb32 100644 --- a/src/middlewares/common/errorHandler.js +++ b/src/middlewares/common/errorHandler.js @@ -47,23 +47,31 @@ class ErrorHandler { /** * A local reference for the `appLogger` service. * @type {Logger} + * @access protected + * @ignore */ - this.appLogger = appLogger; + this._appLogger = appLogger; /** * A local reference for the `responsesBuilder` service. * @type {ResponsesBuilder} + * @access protected + * @ignore */ - this.responsesBuilder = responsesBuilder; + this._responsesBuilder = responsesBuilder; /** * Whether or not to show unknown errors real messages. * @type {Boolean} + * @access protected + * @ignore */ - this.showErrors = showErrors; + this._showErrors = showErrors; /** * A local reference for the class the app uses to generate errors. * @type {Class} + * @access protected + * @ignore */ - this.AppError = AppError; + this._AppError = AppError; /** * These are the "settings" the middleware will use in order to display the errors. * @type {ErrorHandlerOptions} @@ -96,9 +104,9 @@ class ErrorHandler { // Define the error response default status. let { status } = this._options.default; // Validate if the error is known or not. - const knownError = err instanceof this.AppError; + const knownError = err instanceof this._AppError; // If the `showErrors` flag is enabled or the error is a known error... - if (this.showErrors || knownError) { + if (this._showErrors || knownError) { // ...set the error real message on the response. data.message = err.message; // If the error type is known... @@ -109,7 +117,7 @@ class ErrorHandler { status = err.status || statuses['Bad Request']; } // If the `showErrors` flag is enabled... - if (this.showErrors) { + if (this._showErrors) { // Get the error stack and format it into an `Array`. const stack = err.stack.split('\n').map((line) => line.trim()); // Add the stack to the response. @@ -117,12 +125,12 @@ class ErrorHandler { // Remove the first item of the stack, since it's the same as the message. stack.splice(0, 1); // Log the error. - this.appLogger.error(`ERROR: ${err.message}`); - this.appLogger.info(stack); + this._appLogger.error(`ERROR: ${err.message}`); + this._appLogger.info(stack); } } // Send the response. - this.responsesBuilder.json(res, data, status); + this._responsesBuilder.json(res, data, status); } else { // ...otherwise, move to the next middleware. next(); diff --git a/src/middlewares/common/forceHTTPS.js b/src/middlewares/common/forceHTTPS.js index 63d515f2..9e724c1b 100644 --- a/src/middlewares/common/forceHTTPS.js +++ b/src/middlewares/common/forceHTTPS.js @@ -12,8 +12,10 @@ class ForceHTTPS { /** * A list of regular expressions to match routes that should be ignored. * @type {Array} + * @access protected + * @ignore */ - this.ignoredRoutes = ignoredRoutes; + this._ignoredRoutes = ignoredRoutes; } /** * Returns the Express middleware that forces the redirection to HTTPS. @@ -24,7 +26,7 @@ class ForceHTTPS { if ( !req.secure && req.get('X-Forwarded-Proto') !== 'https' && - !this.ignoredRoutes.some((expression) => expression.test(req.originalUrl)) + !this._ignoredRoutes.some((expression) => expression.test(req.originalUrl)) ) { const host = req.get('Host'); res.redirect(`https://${host}${req.url}`); @@ -33,6 +35,13 @@ class ForceHTTPS { } }; } + /** + * A list of regular expressions to match routes that should be ignored. + * @type {Array} + */ + get ignoredRoutes() { + return this._ignoredRoutes.slice(); + } } /** * A middleware to force HTTPS redirections to all the routes. diff --git a/src/middlewares/html/fastHTML.js b/src/middlewares/html/fastHTML.js index efc12f50..8e3d2f8b 100644 --- a/src/middlewares/html/fastHTML.js +++ b/src/middlewares/html/fastHTML.js @@ -37,34 +37,36 @@ class FastHTML { /** * A local reference for the `sendFile` service. * @type {SendFile} + * @access protected + * @ignore */ - this.sendFile = sendFile; + this._sendFile = sendFile; /** * The name of the file to serve. * @type {string} */ - this.file = file; + this._file = file; /** * A list of regular expressions to match requests paths that should be ignored. * @type {Array} */ - this.ignoredRoutes = ignoredRoutes; + this._ignoredRoutes = ignoredRoutes; /** * If specified, a reference for a service that generates HTML files. * @type {HTMLGenerator} */ - this.htmlGenerator = htmlGenerator; + this._htmlGenerator = htmlGenerator; /** * Whether or not the file is ready to be served. * @type {Boolean} - * @ignore * @access protected + * @ignore */ this._ready = true; // If an `HTMLGenerator` service was specified... - if (this.htmlGenerator) { + if (this._htmlGenerator) { // ...get the name of the file from that service. - this.file = this.htmlGenerator.getFile(); + this._file = this._htmlGenerator.getFile(); /** * Mark the `_ready` flag as `false` as this service needs to wait for the generator to * create the file. @@ -79,7 +81,7 @@ class FastHTML { middleware() { return (req, res, next) => { // Validate if the route should be ignored. - const shouldIgnore = this.ignoredRoutes + const shouldIgnore = this._ignoredRoutes .some((expression) => expression.test(req.originalUrl)); // If the route should be ignored... if (shouldIgnore) { @@ -91,7 +93,7 @@ class FastHTML { * calls the method that will notify this service when the file has been created and is * ready to be loaded. */ - this.htmlGenerator.whenReady() + this._htmlGenerator.whenReady() .then(() => { // The file is ready to use, so mark the `_ready` flag as `true`. this._ready = true; @@ -111,6 +113,20 @@ class FastHTML { } }; } + /** + * The name of the file to serve. + * @type {string} + */ + get file() { + return this._file; + } + /** + * A list of regular expressions to match requests paths that should be ignored. + * @type {Array} + */ + get ignoredRoutes() { + return this._ignoredRoutes.slice(); + } /** * Serves the file on the response. * @param {ExpressResponse} res The server response. @@ -120,7 +136,7 @@ class FastHTML { */ _sendHTML(res, next) { res.setHeader('Content-Type', mime.getType('html')); - this.sendFile(res, this.file, next); + this._sendFile(res, this._file, next); } } /** diff --git a/src/middlewares/html/showHTML.js b/src/middlewares/html/showHTML.js index 7dfb235a..25b87444 100644 --- a/src/middlewares/html/showHTML.js +++ b/src/middlewares/html/showHTML.js @@ -20,29 +20,35 @@ class ShowHTML { /** * A local reference for the `sendFile` service. * @type {SendFile} + * @access protected + * @ignore */ - this.sendFile = sendFile; + this._sendFile = sendFile; /** * The name of the file to serve. * @type {string} + * @access protected + * @ignore */ - this.file = file; + this._file = file; /** * If specified, a reference for a service that generates HTML files. * @type {HTMLGenerator} + * @access protected + * @ignore */ - this.htmlGenerator = htmlGenerator; + this._htmlGenerator = htmlGenerator; /** * Whether or not the file is ready to be served. * @type {Boolean} - * @ignore * @access protected + * @ignore */ this._ready = true; // If an `HTMLGenerator` service was specified... - if (this.htmlGenerator) { + if (this._htmlGenerator) { // ...get the name of the file from that service. - this.file = this.htmlGenerator.getFile(); + this._file = this._htmlGenerator.getFile(); /** * Mark the `_ready` flag as `false` as this service needs to wait for the generator to * create the file. @@ -63,7 +69,7 @@ class ShowHTML { * calls the method that will notify this service when the file has been created and is * ready to be loaded. */ - this.htmlGenerator.whenReady() + this._htmlGenerator.whenReady() .then(() => { // The file is ready to use, so mark the `_ready` flag as `true`. this._ready = true; @@ -83,6 +89,13 @@ class ShowHTML { } }; } + /** + * The name of the file to serve. + * @type {string} + */ + get file() { + return this._file; + } /** * Serves the file on the response. * @param {ExpressResponse} res The server response. @@ -92,7 +105,7 @@ class ShowHTML { */ _sendHTML(res, next) { res.setHeader('Content-Type', mime.getType('html')); - return this.sendFile(res, this.file, next); + return this._sendFile(res, this._file, next); } } /** diff --git a/tests/middlewares/common/errorHandler.test.js b/tests/middlewares/common/errorHandler.test.js index 36088607..7788dd7b 100644 --- a/tests/middlewares/common/errorHandler.test.js +++ b/tests/middlewares/common/errorHandler.test.js @@ -9,7 +9,7 @@ const { } = require('/src/middlewares/common/errorHandler'); describe('middlewares/common:errorHandler', () => { - it('should be instantiated with all its dependencies', () => { + it('should be instantiated with default options and have a middleware method', () => { // Given const appLogger = 'appLogger'; const responsesBuilder = 'responsesBuilder'; @@ -20,9 +20,7 @@ describe('middlewares/common:errorHandler', () => { sut = new ErrorHandler(appLogger, responsesBuilder, showErrors, AppError); // Then expect(sut).toBeInstanceOf(ErrorHandler); - expect(sut.appLogger).toBe(appLogger); - expect(sut.responsesBuilder).toBe(responsesBuilder); - expect(sut.showErrors).toBe(showErrors); + expect(sut.middleware).toBeFunction(); expect(sut.options).toEqual(({ default: { message: 'Oops! Something went wrong, please try again', diff --git a/tests/middlewares/common/forceHTTPS.test.js b/tests/middlewares/common/forceHTTPS.test.js index aa451225..c2efe733 100644 --- a/tests/middlewares/common/forceHTTPS.test.js +++ b/tests/middlewares/common/forceHTTPS.test.js @@ -8,7 +8,7 @@ const { } = require('/src/middlewares/common/forceHTTPS'); describe('middlewares/common:forceHTTPS', () => { - it('should be instantiated with its default values', () => { + it('should be instantiated with its default options', () => { // Given let sut = null; // When @@ -26,7 +26,7 @@ describe('middlewares/common:forceHTTPS', () => { sut = new ForceHTTPS(ignoredFiles); // Then expect(sut).toBeInstanceOf(ForceHTTPS); - expect(sut.ignoredRoutes).toBe(ignoredFiles); + expect(sut.ignoredRoutes).toEqual(ignoredFiles); }); it('should return a middleware to force the traffic to HTTPS', () => { diff --git a/tests/middlewares/html/fastHTML.test.js b/tests/middlewares/html/fastHTML.test.js index 00f63d71..c8cf717c 100644 --- a/tests/middlewares/html/fastHTML.test.js +++ b/tests/middlewares/html/fastHTML.test.js @@ -8,7 +8,7 @@ const { } = require('/src/middlewares/html/fastHTML'); describe('middlewares/html:fastHTML', () => { - it('should be instantiated with all its dependencies', () => { + it('should be instantiated', () => { // Given const sendFile = 'sendFile'; let sut = null; @@ -16,7 +16,6 @@ describe('middlewares/html:fastHTML', () => { sut = new FastHTML(sendFile); // Then expect(sut).toBeInstanceOf(FastHTML); - expect(sut.sendFile).toBe(sendFile); }); it('should be instantiated with the optional htmlGenerator service', () => { @@ -31,8 +30,6 @@ describe('middlewares/html:fastHTML', () => { sut = new FastHTML(sendFile, 'index.html', [], htmlGenerator); // Then expect(sut).toBeInstanceOf(FastHTML); - expect(sut.sendFile).toBe(sendFile); - expect(sut.htmlGenerator).toBe(htmlGenerator); expect(sut.file).toBe(file); expect(htmlGenerator.getFile).toHaveBeenCalledTimes(1); }); @@ -55,6 +52,7 @@ describe('middlewares/html:fastHTML', () => { sut = new FastHTML(sendFile, file, ignoredRoutes); middleware = sut.middleware(); middleware(request, response, next); + // Then expect(response.setHeader).toHaveBeenCalledTimes(1); expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html'); expect(sendFile).toHaveBeenCalledTimes(1); @@ -77,6 +75,8 @@ describe('middlewares/html:fastHTML', () => { sut = new FastHTML(sendFile, file, ignoredRoutes); middleware = sut.middleware(); middleware(request, response, next); + // Then + expect(sut.ignoredRoutes).toEqual(ignoredRoutes); expect(next).toHaveBeenCalledTimes(1); expect(sendFile).toHaveBeenCalledTimes(0); }); @@ -104,6 +104,7 @@ describe('middlewares/html:fastHTML', () => { sut = new FastHTML(sendFile, '', ignoredRoutes, htmlGenerator); middleware = sut.middleware(); middleware(request, response, () => { + // Then expect(htmlGenerator.getFile).toHaveBeenCalledTimes(1); expect(htmlGenerator.whenReady).toHaveBeenCalledTimes(1); expect(sendFile).toHaveBeenCalledTimes(1); @@ -140,6 +141,7 @@ describe('middlewares/html:fastHTML', () => { sut = new FastHTML(sendFile, '', ignoredRoutes, htmlGenerator); middleware = sut.middleware(); middleware(request, response, (result) => { + // Then expect(result).toBe(error); expect(htmlGenerator.getFile).toHaveBeenCalledTimes(1); expect(htmlGenerator.whenReady).toHaveBeenCalledTimes(1); diff --git a/tests/middlewares/html/showHTML.test.js b/tests/middlewares/html/showHTML.test.js index f2421f91..e97437de 100644 --- a/tests/middlewares/html/showHTML.test.js +++ b/tests/middlewares/html/showHTML.test.js @@ -8,7 +8,7 @@ const { } = require('/src/middlewares/html/showHTML'); describe('middlewares/html:showHTML', () => { - it('should be instantiated with all its dependencies', () => { + it('should be instantiated', () => { // Given const sendFile = 'sendFile'; let sut = null; @@ -16,7 +16,6 @@ describe('middlewares/html:showHTML', () => { sut = new ShowHTML(sendFile); // Then expect(sut).toBeInstanceOf(ShowHTML); - expect(sut.sendFile).toBe(sendFile); }); it('should be instantiated with the optional htmlGenerator service', () => { @@ -31,8 +30,6 @@ describe('middlewares/html:showHTML', () => { sut = new ShowHTML(sendFile, 'index.html', htmlGenerator); // Then expect(sut).toBeInstanceOf(ShowHTML); - expect(sut.sendFile).toBe(sendFile); - expect(sut.htmlGenerator).toBe(htmlGenerator); expect(sut.file).toBe(file); expect(htmlGenerator.getFile).toHaveBeenCalledTimes(1); }); From c058c547e931aefc93297663a48ef7c9f011f35c Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 06:10:46 -0300 Subject: [PATCH 39/78] refactor(services): make all the injected services protected --- src/services/api/client.js | 20 ++++++- .../api/ensureBearerAuthentication.js | 12 ++-- src/services/common/appError.js | 2 +- src/services/frontend/frontendFs.js | 10 ++-- src/services/html/htmlGenerator.js | 57 ++++++++++++------- src/services/http/http.js | 23 ++++++-- src/services/http/responsesBuilder.js | 8 ++- tests/services/api/client.test.js | 9 +-- .../api/ensureBearerAuthentication.test.js | 3 +- tests/services/frontend/frontendFs.test.js | 4 +- tests/services/html/htmlGenerator.test.js | 11 +--- tests/services/http/http.test.js | 6 +- tests/services/http/responsesBuilder.test.js | 4 +- 13 files changed, 98 insertions(+), 71 deletions(-) diff --git a/src/services/api/client.js b/src/services/api/client.js index 099ad65a..126eb14c 100644 --- a/src/services/api/client.js +++ b/src/services/api/client.js @@ -26,13 +26,17 @@ class APIClient extends APIClientBase { * @property {string} url The API entry point. * @property {Object} endpoints A dictionary of named endpoints relative to the API * entry point. + * @access protected + * @ignore */ - this.apiConfig = apiConfig; + this._apiConfig = Object.freeze(apiConfig); /** * A local reference for the class the app uses to generate HTTP errors. * @type {Class} + * @access protected + * @ignore */ - this.HTTPError = HTTPError; + this._HTTPError = HTTPError; } /** * Formats a response error with the App error class. @@ -41,7 +45,17 @@ class APIClient extends APIClientBase { * @return {HTTPError} */ error(response, status) { - return new this.HTTPError(response.data.message, status); + return new this._HTTPError(response.data.message, status); + } + /** + * The configuration for the API the client will make requests to. + * @type {Object} + * @property {string} url The API entry point. + * @property {Object} endpoints A dictionary of named endpoints relative to the API + * entry point. + */ + get apiConfig() { + return this._apiConfig; } } /** diff --git a/src/services/api/ensureBearerAuthentication.js b/src/services/api/ensureBearerAuthentication.js index fabd0693..f4d99b8d 100644 --- a/src/services/api/ensureBearerAuthentication.js +++ b/src/services/api/ensureBearerAuthentication.js @@ -16,13 +16,17 @@ class EnsureBearerAuthentication { /** * A local reference for the class the app uses to generate errors. * @type {Class} + * @access protected + * @ignore */ - this.AppError = AppError; + this._AppError = AppError; /** * The regular expression used to validate the token. * @type {RegExp} + * @access protected + * @ignore */ - this.bearerRegex = /bearer .+$/i; + this._bearerRegex = /bearer .+$/i; } /** * Returns the Express middleware that validates the `Authorization` header. @@ -34,14 +38,14 @@ class EnsureBearerAuthentication { // Get the `Authorization` header. const { headers: { authorization } } = req; // If the header has content the RegExp says it's valid... - if (authorization && this.bearerRegex.test(authorization)) { + if (authorization && this._bearerRegex.test(authorization)) { // ...Set the token as the `bearerToken` property of the current request. req.bearerToken = authorization.trim().split(' ').pop(); // Move to the next middleware. next(); } else { // ...otherwise, send an unauthorized error to the next middleware. - next(new this.AppError('Unauthorized', { + next(new this._AppError('Unauthorized', { status: statuses.Unauthorized, })); } diff --git a/src/services/common/appError.js b/src/services/common/appError.js index da70bf1c..26d24178 100644 --- a/src/services/common/appError.js +++ b/src/services/common/appError.js @@ -20,7 +20,7 @@ class AppError extends Error { * @type {Object} * @access protected */ - this._context = context; + this._context = Object.freeze(context); /** * The date of when the error was generated. * @type {Date} diff --git a/src/services/frontend/frontendFs.js b/src/services/frontend/frontendFs.js index b28a5f31..a8cd35df 100644 --- a/src/services/frontend/frontendFs.js +++ b/src/services/frontend/frontendFs.js @@ -19,8 +19,10 @@ class FrontendFs { /** * A local reference for the `pathUtils` service. * @type {PathUtils} + * @access protected + * @ignore */ - this.pathUtils = pathUtils; + this._pathUtils = pathUtils; } /** * Read a file from the file system. @@ -29,7 +31,7 @@ class FrontendFs { * @return {Promise} */ read(filepath, encoding = 'utf-8') { - return fs.readFile(this.pathUtils.joinFrom('app', filepath), encoding); + return fs.readFile(this._pathUtils.joinFrom('app', filepath), encoding); } /** * Write a file on the file system. @@ -38,7 +40,7 @@ class FrontendFs { * @return {Promise} */ write(filepath, data) { - return fs.writeFile(this.pathUtils.joinFrom('app', filepath), data); + return fs.writeFile(this._pathUtils.joinFrom('app', filepath), data); } /** * Delete a file from the file system. @@ -46,7 +48,7 @@ class FrontendFs { * @return {Promise} */ delete(filepath) { - return fs.unlink(this.pathUtils.joinFrom('app', filepath)); + return fs.unlink(this._pathUtils.joinFrom('app', filepath)); } } /** diff --git a/src/services/html/htmlGenerator.js b/src/services/html/htmlGenerator.js index d242585d..8d2b1750 100644 --- a/src/services/html/htmlGenerator.js +++ b/src/services/html/htmlGenerator.js @@ -66,7 +66,7 @@ class HTMLGenerator { * The service options. * @type {HTMLGeneratorOptions} */ - this.options = ObjectUtils.merge({ + this._options = ObjectUtils.merge({ template: 'index.tpl.html', file: 'index.html', deleteTemplateAfter: true, @@ -82,7 +82,7 @@ class HTMLGenerator { * you end up with `['d', 'e', 'c']`, and in this case, that's not very useful. */ if (options.configurationKeys) { - this.options.configurationKeys = options.configurationKeys.slice(); + this._options.configurationKeys = options.configurationKeys.slice(); } // If `valuesService` was specified, check if it has a `getValues` method. if (valuesService && typeof valuesService.getValues !== 'function') { @@ -91,36 +91,44 @@ class HTMLGenerator { /** * A local reference for the `appConfiguration` service. * @type {AppConfiguration} + * @access protected + * @ignore */ - this.appConfiguration = appConfiguration; + this._appConfiguration = appConfiguration; /** * A local reference for the `appLogger` service. * @type {Logger} + * @access protected + * @ignore */ - this.appLogger = appLogger; + this._appLogger = appLogger; /** * A local reference for the `frontendFs` service. * @type {FrontendFs} + * @access protected + * @ignore */ - this.frontendFs = frontendFs; + this._frontendFs = frontendFs; /** * A local reference for the recieved `valuesService` service. * @type {?HTMLGeneratorValuesService} + * @access protected + * @ignore */ - this.valuesService = valuesService; + this._valuesService = valuesService; /** * Whether or not the file has been generated. * @type {Boolean} - * @ignore * @access protected + * @ignore */ this._fileReady = false; /** * A deferred promise to return when another service asks if the file has been generated. Once * this sevice finishes generating the file, the promise will be resolved. * @type {Object} - * @ignore * @access protected + * @ignore */ this._fileDeferred = deferred(); } @@ -138,7 +146,7 @@ class HTMLGenerator { * @return {string} */ getFile() { - return this.options.file; + return this._options.file; } /** * Get the values that are going to be injected on the file. @@ -147,16 +155,16 @@ class HTMLGenerator { getValues() { let valuesPromise; // If an `HTMLGeneratorValuesService` was specified... - if (this.valuesService) { + if (this._valuesService) { // ...get the values from there. - valuesPromise = this.valuesService.getValues(); - } else if (this.options.configurationKeys.length) { + valuesPromise = this._valuesService.getValues(); + } else if (this._options.configurationKeys.length) { /** * ...if there are configuration keys to be copied, set to return an already resolved * promise with the settings from the configuration. */ valuesPromise = Promise.resolve( - this.appConfiguration.get(this.options.configurationKeys) + this._appConfiguration.get(this._options.configurationKeys) ); } else { // ...otherwsie, return an already resolved promise with an empty object. @@ -175,11 +183,11 @@ class HTMLGenerator { template, deleteTemplateAfter, file, - } = this.options; + } = this._options; // Define the variable where the template contents will be saved. let templateContents = ''; // Read the template file. - return this.frontendFs.read(`./${template}`) + return this._frontendFs.read(`./${template}`) .then((contents) => { // Save the template contents. templateContents = contents; @@ -190,20 +198,20 @@ class HTMLGenerator { // Get the HTML code for the file. const html = this._processHTML(templateContents, values); // Write the generated file. - return this.frontendFs.write(file, html); + return this._frontendFs.write(file, html); }) .then(() => { - this.appLogger.success(`The HTML was successfully generated (${file})`); + this._appLogger.success(`The HTML was successfully generated (${file})`); /** * If the template needs to be deleted, return the call to the `delete` method, otherwise, * just an empty object to continue the promise chain. */ - return deleteTemplateAfter && this.frontendFs.delete(`./${template}`); + return deleteTemplateAfter && this._frontendFs.delete(`./${template}`); }) .then(() => { // If the template was deleted, log a message informing it. if (deleteTemplateAfter) { - this.appLogger.info(`The HTML template was successfully removed (${template})`); + this._appLogger.info(`The HTML template was successfully removed (${template})`); } /** * Mark the `_fileReady` flag as `true` so the next calls to `whenReady` won't get the @@ -214,10 +222,17 @@ class HTMLGenerator { this._fileDeferred.resolve(); }) .catch((error) => { - this.appLogger.error('There was an error while generating the HTML'); + this._appLogger.error('There was an error while generating the HTML'); return Promise.reject(error); }); } + /** + * The service options. + * @type {HTMLGeneratorOptions} + */ + get options() { + return Object.freeze(this._options); + } /** * Creates the code for the HTML file. * @param {string} template The template code where the values are going to be injected. @@ -231,7 +246,7 @@ class HTMLGenerator { replacePlaceholder, valuesExpression, variable, - } = this.options; + } = this._options; const htmlObject = JSON.stringify(values); let code = template .replace( diff --git a/src/services/http/http.js b/src/services/http/http.js index c5fe0cae..a8c1a64d 100644 --- a/src/services/http/http.js +++ b/src/services/http/http.js @@ -27,13 +27,17 @@ class HTTP { /** * Whether or not to log the requests and their responses. * @type {Boolean} + * @access protected + * @ignore */ - this.logRequests = logRequests; + this._logRequests = logRequests; /** * A local reference for the `appLogger` service. * @type {AppLogger} + * @access protected + * @ignore */ - this.appLogger = appLogger; + this._appLogger = appLogger; /** * So it can be sent to other services as a reference. * @ignore @@ -121,13 +125,13 @@ class HTTP { fetchOptions.headers = headers; } // If the `logRequests` flag is `true`, call the method to log the request. - if (this.logRequests) { + if (this._logRequests) { this._logRequest(fetchURL, fetchOptions); } // Make the request. let result = fetch(fetchURL, fetchOptions); // If the `logRequests` flag is `true`... - if (this.logRequests) { + if (this._logRequests) { // Add an extra step on the promise chain to log the response. result = result.then((response) => { this._logResponse(response); @@ -137,6 +141,13 @@ class HTTP { // Return the request promise. return result; } + /** + * Whether or not to log the requests and their responses. + * @type {Boolean} + */ + get logRequests() { + return this._logRequests; + } /** * Log a a request information using the `appLogger` service. * @param {string} url The request URL. @@ -161,7 +172,7 @@ class HTTP { lines.push(`${prefix}body: "${options.body}"`); } - this.appLogger.info(lines); + this._appLogger.info(lines); } /** * Log a a response information using the `appLogger` service. @@ -182,7 +193,7 @@ class HTTP { lines.push(`${prefix}${header}: ${value}`); }); - this.appLogger.info(lines); + this._appLogger.info(lines); } } /** diff --git a/src/services/http/responsesBuilder.js b/src/services/http/responsesBuilder.js index 242092f2..a5b2da5d 100644 --- a/src/services/http/responsesBuilder.js +++ b/src/services/http/responsesBuilder.js @@ -23,8 +23,10 @@ class ResponsesBuilder { /** * A local reference for the `appConfiguration` service. * @type {AppConfiguration} + * @access protected + * @ignore */ - this.appConfiguration = appConfiguration; + this._appConfiguration = appConfiguration; } /** * Generates and sends a JSON response. @@ -52,7 +54,7 @@ class ResponsesBuilder { .status(status) .json({ metadata: Object.assign({ - version: this.appConfiguration.get('version'), + version: this._appConfiguration.get('version'), status, }, metadata), data, @@ -83,7 +85,7 @@ class ResponsesBuilder { status = statuses.ok, options = {} ) { - const prefix = this.appConfiguration.get('postMessagesPrefix') || ''; + const prefix = this._appConfiguration.get('postMessagesPrefix') || ''; const target = options.target || 'window.opener'; const close = typeof options.close !== 'undefined' ? options.close : true; const defaultCloseDelay = 700; diff --git a/tests/services/api/client.test.js b/tests/services/api/client.test.js index 9b60d76f..f873c8d1 100644 --- a/tests/services/api/client.test.js +++ b/tests/services/api/client.test.js @@ -9,7 +9,7 @@ const { const APIClientBase = require('wootils/shared/apiClient'); describe('services/api:client', () => { - it('should be instantiated with all its dependencies', () => { + it('should be instantiated with its configuration', () => { // Given const apiConfig = { url: 'my-api', @@ -20,17 +20,16 @@ describe('services/api:client', () => { const http = { fetch: () => {}, }; - const HTTPError = Error; + const HTTPError = 'HTTPError'; let sut = null; // When sut = new APIClient(apiConfig, http, HTTPError); // Then expect(sut).toBeInstanceOf(APIClientBase); expect(sut).toBeInstanceOf(APIClient); - expect(sut.apiConfig).toBe(apiConfig); + expect(sut.apiConfig).toEqual(apiConfig); expect(sut.url).toBe(apiConfig.url); expect(sut.endpoints).toEqual(apiConfig.endpoints); - expect(sut.HTTPError).toBe(HTTPError); }); it('should format error responses using the HTTPError service', () => { @@ -102,7 +101,6 @@ describe('services/api:client', () => { expect(sut.apiConfig).toBe(appConfiguration.api); expect(sut.url).toBe(appConfiguration.api.url); expect(sut.endpoints).toEqual(appConfiguration.api.endpoints); - expect(sut.HTTPError).toBe('HTTPError'); expect(sut.fetchClient).toBe(http.fetch); expect(serviceName).toBe('apiClient'); expect(appConfiguration.get).toHaveBeenCalledTimes(1); @@ -147,7 +145,6 @@ describe('services/api:client', () => { expect(sut.apiConfig).toBe(appConfiguration.apiConfig); expect(sut.url).toBe(appConfiguration.apiConfig.url); expect(sut.endpoints).toEqual(appConfiguration.apiConfig.endpoints); - expect(sut.HTTPError).toBe('HTTPError'); expect(sut.fetchClient).toBe(http.fetch); expect(serviceName).toBe(name); expect(appConfiguration.get).toHaveBeenCalledTimes(1); diff --git a/tests/services/api/ensureBearerAuthentication.test.js b/tests/services/api/ensureBearerAuthentication.test.js index a174a50e..8722812e 100644 --- a/tests/services/api/ensureBearerAuthentication.test.js +++ b/tests/services/api/ensureBearerAuthentication.test.js @@ -9,7 +9,7 @@ const { } = require('/src/services/api/ensureBearerAuthentication'); describe('services/api:ensureBearerAuthentication', () => { - it('should be instantiated with all its dependencies', () => { + it('should be instantiated', () => { // Given const AppError = Error; let sut = null; @@ -17,7 +17,6 @@ describe('services/api:ensureBearerAuthentication', () => { sut = new EnsureBearerAuthentication(AppError); // Then expect(sut).toBeInstanceOf(EnsureBearerAuthentication); - expect(sut.AppError).toBe(AppError); }); it('should have a middleware to unauthorize requests without tokens', () => { diff --git a/tests/services/frontend/frontendFs.test.js b/tests/services/frontend/frontendFs.test.js index 7f2c7064..a8289264 100644 --- a/tests/services/frontend/frontendFs.test.js +++ b/tests/services/frontend/frontendFs.test.js @@ -16,7 +16,7 @@ describe('services/frontend:frontendFs', () => { fs.unlink.mockReset(); }); - it('should be instantiated with all its dependencies', () => { + it('should be instantiated', () => { // Given const pathUtils = 'pathUtils'; let sut = null; @@ -24,7 +24,6 @@ describe('services/frontend:frontendFs', () => { sut = new FrontendFs(pathUtils); // Then expect(sut).toBeInstanceOf(FrontendFs); - expect(sut.pathUtils).toBe(pathUtils); }); it('should read a file', () => { @@ -129,6 +128,5 @@ describe('services/frontend:frontendFs', () => { // Then expect(serviceName).toBe('frontendFs'); expect(sut).toBeInstanceOf(FrontendFs); - expect(sut.pathUtils).toBe('pathUtils'); }); }); diff --git a/tests/services/html/htmlGenerator.test.js b/tests/services/html/htmlGenerator.test.js index bf231e2f..ba78b188 100644 --- a/tests/services/html/htmlGenerator.test.js +++ b/tests/services/html/htmlGenerator.test.js @@ -15,7 +15,7 @@ describe('services/html:htmlGenerator', () => { wootilsMock.reset(); }); - it('should be instantiated with all its dependencies', () => { + it('should be instantiated', () => { // Given const options = {}; const appConfiguration = 'appConfiguration'; @@ -40,10 +40,6 @@ describe('services/html:htmlGenerator', () => { variable: expect.any(String), configurationKeys: expect.any(Array), }); - expect(sut.appConfiguration).toBe(appConfiguration); - expect(sut.appLogger).toBe(appLogger); - expect(sut.frontendFs).toBe(frontendFs); - expect(sut.valuesService).toBeNull(); }); it('should be instantiated with a custom service to get the template values', () => { @@ -66,11 +62,6 @@ describe('services/html:htmlGenerator', () => { ); // Then expect(sut).toBeInstanceOf(HTMLGenerator); - expect(sut.options).toBeObject(); - expect(sut.appConfiguration).toBe(appConfiguration); - expect(sut.appLogger).toBe(appLogger); - expect(sut.frontendFs).toBe(frontendFs); - expect(sut.valuesService).toEqual(valuesService); }); it('should throw an error if the values service doesnt have a `getValues` method', () => { diff --git a/tests/services/http/http.test.js b/tests/services/http/http.test.js index 7b0d7aba..c576b0e6 100644 --- a/tests/services/http/http.test.js +++ b/tests/services/http/http.test.js @@ -14,7 +14,7 @@ describe('services/http:http', () => { fetch.mockReset(); }); - it('should be instantiated with all its dependencies', () => { + it('should be instantiated', () => { // Given const logRequests = 'logRequests'; const appLogger = 'appLogger'; @@ -23,8 +23,6 @@ describe('services/http:http', () => { sut = new HTTP(logRequests, appLogger); // Then expect(sut).toBeInstanceOf(HTTP); - expect(sut.logRequests).toBe(logRequests); - expect(sut.appLogger).toBe(appLogger); }); it('should include a provider for the DIC', () => { @@ -50,7 +48,6 @@ describe('services/http:http', () => { expect(serviceName).toBe('http'); expect(sut).toBeInstanceOf(HTTP); expect(sut.logRequests).toBeFalse(); - expect(sut.appLogger).toBe('appLogger'); }); it('should get a request IP from an Express request object', () => { @@ -394,6 +391,5 @@ describe('services/http:http', () => { expect(serviceName).toBe('http'); expect(sut).toBeInstanceOf(HTTP); expect(sut.logRequests).toBeTrue(); - expect(sut.appLogger).toBe('appLogger'); }); }); diff --git a/tests/services/http/responsesBuilder.test.js b/tests/services/http/responsesBuilder.test.js index 6f6a5910..691aac15 100644 --- a/tests/services/http/responsesBuilder.test.js +++ b/tests/services/http/responsesBuilder.test.js @@ -9,7 +9,7 @@ const { } = require('/src/services/http/responsesBuilder'); describe('services/http:responsesBuilder', () => { - it('should be instantiated with all its dependencies', () => { + it('should be instantiated', () => { // Given const appConfiguration = 'appConfiguration'; let sut = null; @@ -17,7 +17,6 @@ describe('services/http:responsesBuilder', () => { sut = new ResponsesBuilder(appConfiguration); // Then expect(sut).toBeInstanceOf(ResponsesBuilder); - expect(sut.appConfiguration).toBe(appConfiguration); }); it('should generate and send a basic JSON response', () => { @@ -191,6 +190,5 @@ describe('services/http:responsesBuilder', () => { // Then expect(serviceName).toBe('responsesBuilder'); expect(sut).toBeInstanceOf(ResponsesBuilder); - expect(sut.appConfiguration).toBe('appConfiguration'); }); }); From 3f3926d2529b1367b558f98765dd47994ead1e2f Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 18:03:21 -0300 Subject: [PATCH 40/78] feat(services/utils): add a new and configurable ensureBearerToken service middleware --- src/services/index.js | 2 + src/services/utils/ensureBearerToken.js | 128 ++++++++++++ src/services/utils/index.js | 5 + tests/services/index.test.js | 1 + .../services/utils/ensureBearerToken.test.js | 183 ++++++++++++++++++ 5 files changed, 319 insertions(+) create mode 100644 src/services/utils/ensureBearerToken.js create mode 100644 src/services/utils/index.js create mode 100644 tests/services/utils/ensureBearerToken.test.js diff --git a/src/services/index.js b/src/services/index.js index b82741c9..55d8c893 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -3,6 +3,7 @@ const common = require('./common'); const frontend = require('./frontend'); const html = require('./html'); const http = require('./http'); +const utils = require('./utils'); module.exports = { api, @@ -10,4 +11,5 @@ module.exports = { frontend, html, http, + utils, }; diff --git a/src/services/utils/ensureBearerToken.js b/src/services/utils/ensureBearerToken.js new file mode 100644 index 00000000..00556797 --- /dev/null +++ b/src/services/utils/ensureBearerToken.js @@ -0,0 +1,128 @@ +const statuses = require('statuses'); +const ObjectUtils = require('wootils/shared/objectUtils'); +const { providerCreator } = require('../../utils/wrappers'); + +/** + * @typedef {Object} EnsureBearerTokenErrorOptions + * @description These options allow you to modify the error generated by the middleware when the + * request doesn't have a valid token. + * @property {string} [message='Unauthorized'] The error message for the response. + * @property {number} [status=401] The HTTP status that will be added to error context + * information. + * @property {Object} [response={}] Context information that the error handler can read + * and add to the default response. + */ + +/** + * @typedef {Object} EnsureBearerTokenOptions + * @descriptions The options for how to validate the token and, possibly, create the errors. + * @property {EnsureBearerTokenErrorOptions} [error] The options to modify the error + * generated by the middleware when the + * request doesn't have a valid token. + * @property {RegExp} [expression] The regular expression used to + * extract the token from the request + * authorization header. + * @property {string} [local='token'] The property inside the `res.locals` + * where the token, if found, will be + * saved. + */ + +/** + * This service gives you a middleware that verifies if a request has an `Authorization` header + * with a bearer token; if it does, the token will be saved on the `res.locals`, otherwise, it + * will generate an error. + */ +class EnsureBearerToken { + /** + * @param {Class} AppError To format the error caused when the request + * doesn't have a valid token. + * @param {EnsureBearerTokenOptions} [options={}] The options to customize the middleware + * behavior: how to validate the token, how to + * save it and what kind of error should + * generate. + */ + constructor(AppError, options = {}) { + /** + * A local reference for the class the app uses to generate errors. + * @type {Class} + * @access protected + * @ignore + */ + this._AppError = AppError; + /** + * The options that define how the middleware validates the token, saves it and generates + * the possible error. + * @type {EnsureBearerTokenOptions} + * @access protected + * @ignore + */ + this._options = ObjectUtils.merge( + { + error: { + message: 'Unauthorized', + status: statuses.unauthorized, + response: {}, + }, + expression: /bearer (.*?)(?:$|\s)/i, + local: 'token', + }, + options + ); + } + /** + * Creates the middleware that will validate the presence of a bearer token on the request + * authorization header. + * @return {ExpressMiddleware} + */ + middleware() { + return (req, res, next) => { + let unauthorized = true; + const { headers: { authorization } } = req; + if (authorization) { + const matches = this._options.expression.exec(authorization); + if (matches) { + const [, token] = matches; + res.locals[this._options.local] = token; + unauthorized = false; + } + } + + if (unauthorized) { + const { error } = this._options; + next(new this._AppError(error.message, { + status: error.status, + response: error.response, + })); + } else { + next(); + } + }; + } + /** + * The options that define how the middleware validates the token, saves it and generates + * the possible error. + * @type {EnsureBearerTokenOptions} + */ + get options() { + return this._options; + } +} +/** + * Generates a "service middleware" that can be used on route controllers in order to validate + * the presence of a bearer token on the requests authorization header. + * @type {ProviderCreator} + * @param {EnsureBearerTokenOptions} options The options to customize the middleware behavior: how + * to validate the token, how to save it and what kind + * of error should generate. + */ +const ensureBearerToken = providerCreator((options) => (app) => { + app.set( + 'ensureBearerToken', + () => (new EnsureBearerToken(app.get('AppError'), options)).middleware() + ); +}); + +module.exports = { + EnsureBearerToken, + ensureBearerToken, +}; diff --git a/src/services/utils/index.js b/src/services/utils/index.js new file mode 100644 index 00000000..5a93cdbd --- /dev/null +++ b/src/services/utils/index.js @@ -0,0 +1,5 @@ +const { ensureBearerToken } = require('./ensureBearerToken'); + +module.exports = { + ensureBearerToken, +}; diff --git a/tests/services/index.test.js b/tests/services/index.test.js index 550b28d8..46b98ce3 100644 --- a/tests/services/index.test.js +++ b/tests/services/index.test.js @@ -13,6 +13,7 @@ describe('services', () => { 'frontend', 'html', 'http', + 'utils', ]; // When/Then expect(Object.keys(services).length).toBe(knownServices.length); diff --git a/tests/services/utils/ensureBearerToken.test.js b/tests/services/utils/ensureBearerToken.test.js new file mode 100644 index 00000000..e183447f --- /dev/null +++ b/tests/services/utils/ensureBearerToken.test.js @@ -0,0 +1,183 @@ +jest.unmock('/src/utils/wrappers'); +jest.unmock('/src/services/utils/ensureBearerToken'); + +require('jasmine-expect'); +const statuses = require('statuses'); +const { + EnsureBearerToken, + ensureBearerToken, +} = require('/src/services/utils/ensureBearerToken'); + +describe('services/utils:ensureBearerToken', () => { + it('should be instantiated with its default options', () => { + // Given + const AppError = Error; + let sut = null; + // When + sut = new EnsureBearerToken(AppError); + // Then + expect(sut).toBeInstanceOf(EnsureBearerToken); + expect(sut.options).toEqual({ + expression: /bearer (.*?)(?:$|\s)/i, + local: 'token', + error: { + message: 'Unauthorized', + status: statuses.unauthorized, + response: {}, + }, + }); + }); + + it('should be instantiated with custom options', () => { + // Given + const AppError = Error; + const options = { + local: 'myToken', + error: { + message: 'Nop!', + }, + }; + let sut = null; + // When + sut = new EnsureBearerToken(AppError, options); + // Then + expect(sut).toBeInstanceOf(EnsureBearerToken); + expect(sut.options).toEqual({ + expression: /bearer (.*?)(?:$|\s)/i, + local: options.local, + error: { + message: options.error.message, + status: statuses.unauthorized, + response: {}, + }, + }); + }); + + it('should have a middleware to authorize requests with tokens', () => { + // Given + const token = 'abc'; + const request = { + headers: { + authorization: `bearer ${token}`, + }, + }; + const response = { locals: {} }; + const next = jest.fn(); + let sut = null; + let middleware = null; + // When + sut = new EnsureBearerToken(); + middleware = sut.middleware(); + middleware(request, response, next); + expect(response.locals.token).toBe(token); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + it('should send an error when the token is not present on the request', () => { + // Given + const appError = jest.fn(); + class AppError { + constructor(...args) { + appError(...args); + } + } + const options = { + error: { + message: 'Nop', + status: statuses['bad request'], + response: { unauthorized: true }, + }, + }; + const request = { + headers: {}, + }; + const response = 'response'; + const next = jest.fn(); + let sut = null; + let middleware = null; + // When + sut = new EnsureBearerToken(AppError, options); + middleware = sut.middleware(); + middleware(request, response, next); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.any(AppError)); + expect(appError).toHaveBeenCalledTimes(1); + expect(appError).toHaveBeenCalledWith(options.error.message, { + status: options.error.status, + response: options.error.response, + }); + }); + + it('should send an error if the authorization header doesn\'t match the expression', () => { + // Given + const appError = jest.fn(); + class AppError { + constructor(...args) { + appError(...args); + } + } + const request = { + headers: { + authorization: 'something', + }, + }; + const response = 'response'; + const next = jest.fn(); + let sut = null; + let middleware = null; + // When + sut = new EnsureBearerToken(AppError); + middleware = sut.middleware(); + middleware(request, response, next); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.any(AppError)); + expect(appError).toHaveBeenCalledTimes(1); + expect(appError).toHaveBeenCalledWith('Unauthorized', { + status: statuses.unauthorized, + response: {}, + }); + }); + + it('should include a provider for the DIC', () => { + // Given + const services = {}; + const app = { + set: jest.fn(), + get: jest.fn((service) => (services[service] || service)), + }; + let sut = null; + let serviceName = null; + let serviceFn = null; + let toCompare = null; + // When + toCompare = new EnsureBearerToken('AppError'); + ensureBearerToken.register(app); + [[serviceName, serviceFn]] = app.set.mock.calls; + sut = serviceFn(); + // Then + expect(serviceName).toBe('ensureBearerToken'); + expect(sut.toString()).toBe(toCompare.middleware().toString()); + }); + + it('should include a provider creator for the DIC', () => { + // Given + const services = {}; + const app = { + set: jest.fn(), + get: jest.fn((service) => (services[service] || service)), + }; + let sut = null; + let serviceName = null; + let serviceFn = null; + let toCompare = null; + // When + toCompare = new EnsureBearerToken('AppError'); + ensureBearerToken().register(app); + [[serviceName, serviceFn]] = app.set.mock.calls; + sut = serviceFn(); + // Then + expect(serviceName).toBe('ensureBearerToken'); + expect(sut.toString()).toBe(toCompare.middleware().toString()); + }); +}); From 0d87c1ff1138489aa0f956cca589471c681ba887 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 18:04:16 -0300 Subject: [PATCH 41/78] docs(middlewares/common/errorHandler): add missing default status --- src/middlewares/common/errorHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/common/errorHandler.js b/src/middlewares/common/errorHandler.js index b86afb32..9fb63cfd 100644 --- a/src/middlewares/common/errorHandler.js +++ b/src/middlewares/common/errorHandler.js @@ -8,7 +8,7 @@ const { middlewareCreator } = require('../../utils/wrappers'); * response. * @property {string} [message='Oops! Something went wrong, please try again'] * The error message the response will show. - * @property {number} status + * @property {number} [status=500] * The HTTP status code for the response. */ From de75c949ad47f750c6c626cdb9a018c29285af68 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 18:10:40 -0300 Subject: [PATCH 42/78] refactor(services/api): remove the old ensureBearerAuthentication service --- .../api/ensureBearerAuthentication.js | 74 ----------- src/services/api/index.js | 2 - .../api/ensureBearerAuthentication.test.js | 119 ------------------ tests/services/api/index.test.js | 4 - 4 files changed, 199 deletions(-) delete mode 100644 src/services/api/ensureBearerAuthentication.js delete mode 100644 tests/services/api/ensureBearerAuthentication.test.js diff --git a/src/services/api/ensureBearerAuthentication.js b/src/services/api/ensureBearerAuthentication.js deleted file mode 100644 index f4d99b8d..00000000 --- a/src/services/api/ensureBearerAuthentication.js +++ /dev/null @@ -1,74 +0,0 @@ -const statuses = require('statuses'); -const { provider } = require('../../utils/wrappers'); -/** - * This service provides a middleware that verifies if a request has a `Authorization` header - * with a bearer token. - * If a request has a valid bearer token, the middleware will set it as the `bearerToken` property - * of the current request object. - */ -class EnsureBearerAuthentication { - /** - * Class constructor. - * @param {Class} AppError To format the error caused when the request doesn't havve a valid - * token. - */ - constructor(AppError) { - /** - * A local reference for the class the app uses to generate errors. - * @type {Class} - * @access protected - * @ignore - */ - this._AppError = AppError; - /** - * The regular expression used to validate the token. - * @type {RegExp} - * @access protected - * @ignore - */ - this._bearerRegex = /bearer .+$/i; - } - /** - * Returns the Express middleware that validates the `Authorization` header. - * @return {ExpressMiddleware} - * @todo Extract the token with the same RegExp used to validate. - */ - middleware() { - return (req, res, next) => { - // Get the `Authorization` header. - const { headers: { authorization } } = req; - // If the header has content the RegExp says it's valid... - if (authorization && this._bearerRegex.test(authorization)) { - // ...Set the token as the `bearerToken` property of the current request. - req.bearerToken = authorization.trim().split(' ').pop(); - // Move to the next middleware. - next(); - } else { - // ...otherwise, send an unauthorized error to the next middleware. - next(new this._AppError('Unauthorized', { - status: statuses.Unauthorized, - })); - } - }; - } -} -/** - * The service provider that once registered on the app container will set the - * `EnsureBearerAuthentication` middleware as the `ensureBearerAuthentication` service. - * @example - * // Register it on the container - * container.register(ensureBearerAuthentication); - * // Getting access to the middleware - * const ensureBearerAuthentication = container.get('ensureBearerAuthentication'); - * @type {Provider} - */ -const ensureBearerAuthentication = provider((app) => { - app.set('ensureBearerAuthentication', () => new EnsureBearerAuthentication( - app.get('AppError') - ).middleware()); -}); - -module.exports = { - EnsureBearerAuthentication, - ensureBearerAuthentication, -}; diff --git a/src/services/api/index.js b/src/services/api/index.js index 9d57f035..cadbe750 100644 --- a/src/services/api/index.js +++ b/src/services/api/index.js @@ -1,5 +1,4 @@ const { apiClient } = require('./client'); -const { ensureBearerAuthentication } = require('./ensureBearerAuthentication'); const { providers } = require('../../utils/wrappers'); /** @@ -12,7 +11,6 @@ const { providers } = require('../../utils/wrappers'); */ const apiServices = providers({ apiClient, - ensureBearerAuthentication, }); module.exports = apiServices; diff --git a/tests/services/api/ensureBearerAuthentication.test.js b/tests/services/api/ensureBearerAuthentication.test.js deleted file mode 100644 index 8722812e..00000000 --- a/tests/services/api/ensureBearerAuthentication.test.js +++ /dev/null @@ -1,119 +0,0 @@ -jest.unmock('/src/utils/wrappers'); -jest.unmock('/src/services/api/ensureBearerAuthentication'); - -require('jasmine-expect'); -const statuses = require('statuses'); -const { - EnsureBearerAuthentication, - ensureBearerAuthentication, -} = require('/src/services/api/ensureBearerAuthentication'); - -describe('services/api:ensureBearerAuthentication', () => { - it('should be instantiated', () => { - // Given - const AppError = Error; - let sut = null; - // When - sut = new EnsureBearerAuthentication(AppError); - // Then - expect(sut).toBeInstanceOf(EnsureBearerAuthentication); - }); - - it('should have a middleware to unauthorize requests without tokens', () => { - // Given - const token = 'abc'; - const request = { - headers: { - authorization: `bearer ${token}`, - }, - }; - const response = 'response'; - const next = jest.fn(); - let sut = null; - let middleware = null; - // When - sut = new EnsureBearerAuthentication(); - middleware = sut.middleware(); - middleware(request, response, next); - expect(request.bearerToken).toBe(token); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); - }); - - it('should send an error when the token is not present on the request', () => { - // Given - const appError = jest.fn(); - class AppError { - constructor(...args) { - appError(...args); - } - } - const request = { - headers: {}, - }; - const response = 'response'; - const next = jest.fn(); - let sut = null; - let middleware = null; - // When - sut = new EnsureBearerAuthentication(AppError); - middleware = sut.middleware(); - middleware(request, response, next); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.any(AppError)); - expect(appError).toHaveBeenCalledTimes(1); - expect(appError).toHaveBeenCalledWith('Unauthorized', { - status: statuses.Unauthorized, - }); - }); - - it('should send an error if the authorization header doesn\'t say `bearer`', () => { - // Given - const appError = jest.fn(); - class AppError { - constructor(...args) { - appError(...args); - } - } - const request = { - headers: { - authorization: 'something', - }, - }; - const response = 'response'; - const next = jest.fn(); - let sut = null; - let middleware = null; - // When - sut = new EnsureBearerAuthentication(AppError); - middleware = sut.middleware(); - middleware(request, response, next); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.any(AppError)); - expect(appError).toHaveBeenCalledTimes(1); - expect(appError).toHaveBeenCalledWith('Unauthorized', { - status: statuses.Unauthorized, - }); - }); - - it('should include a provider for the DIC', () => { - // Given - const services = {}; - const app = { - set: jest.fn(), - get: jest.fn((service) => (services[service] || service)), - }; - let sut = null; - let serviceName = null; - let serviceFn = null; - let toCompare = null; - // When - toCompare = new EnsureBearerAuthentication('AppError'); - ensureBearerAuthentication.register(app); - [[serviceName, serviceFn]] = app.set.mock.calls; - sut = serviceFn(); - // Then - expect(serviceName).toBe('ensureBearerAuthentication'); - expect(sut.toString()).toBe(toCompare.middleware().toString()); - }); -}); diff --git a/tests/services/api/index.test.js b/tests/services/api/index.test.js index d47449a3..bdab6ddf 100644 --- a/tests/services/api/index.test.js +++ b/tests/services/api/index.test.js @@ -12,10 +12,6 @@ describe('services:api', () => { }; const expectedServices = { apiClient: expect.any(Function), - ensureBearerAuthentication: { - register: expect.any(Function), - provider: true, - }, }; const expectedServicesNames = Object.keys(expectedServices); // When From 4252a7bd92baaa2729edc1b10a0629c9d6396990 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 18:56:39 -0300 Subject: [PATCH 43/78] refactor(services): client -> apiClient and into the http services --- src/app/index.js | 12 ++-- src/app/typedef.js | 2 +- src/services/api/index.js | 16 ----- .../{api/client.js => http/apiClient.js} | 28 ++++++++- src/services/http/index.js | 3 + src/services/index.js | 2 - src/services/utils/index.js | 13 +++- tests/app/index.test.js | 6 +- .../client.test.js => http/apiClient.test.js} | 62 ++++++++++++++----- tests/services/http/index.test.js | 29 +++++---- tests/services/index.test.js | 1 - tests/services/{api => utils}/index.test.js | 12 ++-- 12 files changed, 119 insertions(+), 67 deletions(-) delete mode 100644 src/services/api/index.js rename src/services/{api/client.js => http/apiClient.js} (75%) rename tests/services/{api/client.test.js => http/apiClient.test.js} (75%) rename tests/services/{api => utils}/index.test.js (68%) diff --git a/src/app/index.js b/src/app/index.js index a316f0c9..33d0f6d1 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -15,9 +15,9 @@ const { } = require('wootils/node/providers'); const { EventsHub } = require('wootils/shared'); -const apiServices = require('../services/api'); const commonServices = require('../services/common'); const httpServices = require('../services/http'); +const utilsServices = require('../services/utils'); /** * Jimpex is a mix of Jimple, a Javascript port of Pimple dependency injection container, and * Express, one of the most popular web frameworks for Node. @@ -75,9 +75,9 @@ class Jimpex extends Jimple { multer: true, }, defaultServices: { - api: true, common: true, http: true, + utils: true, }, }, options); /** @@ -298,10 +298,6 @@ class Jimpex extends Jimple { _setupDefaultServices() { const { defaultServices } = this._options; - if (defaultServices.api) { - this.register(apiServices); - } - if (defaultServices.common) { this.register(commonServices); } @@ -310,6 +306,10 @@ class Jimpex extends Jimple { this.register(httpServices); } + if (defaultServices.utils) { + this.register(utilsServices); + } + this.set('events', () => new EventsHub()); } /** diff --git a/src/app/typedef.js b/src/app/typedef.js index 33fb91c1..05e1fca4 100644 --- a/src/app/typedef.js +++ b/src/app/typedef.js @@ -57,7 +57,7 @@ * @typedef {Object} JimpexDefaultServicesOptions * @property {boolean} [common=true] Whether or not to register all the `common` service providers. * @property {boolean} [http=true] Whether or not to register all the `http` service providers. - * @property {boolean} [api=true] Whether or not to register all the `api` service providers. + * @property {boolean} [utils=true] Whether or not to register all the `utils` service providers. */ /** diff --git a/src/services/api/index.js b/src/services/api/index.js deleted file mode 100644 index cadbe750..00000000 --- a/src/services/api/index.js +++ /dev/null @@ -1,16 +0,0 @@ -const { apiClient } = require('./client'); -const { providers } = require('../../utils/wrappers'); - -/** - * The providers collection for the API services. - * @type {Provider} - * @property {Provider} apiClient - * The provider for {@link APIClient}. - * @property {Provider} ensureBearerAuthentication - * The provider for {@link EnsureBearerAuthentication}. - */ -const apiServices = providers({ - apiClient, -}); - -module.exports = apiServices; diff --git a/src/services/api/client.js b/src/services/http/apiClient.js similarity index 75% rename from src/services/api/client.js rename to src/services/http/apiClient.js index 126eb14c..50d0b587 100644 --- a/src/services/api/client.js +++ b/src/services/http/apiClient.js @@ -1,3 +1,4 @@ +const ObjectUtils = require('wootils/shared/objectUtils'); const APIClientBase = require('wootils/shared/apiClient'); const { providerCreator } = require('../../utils/wrappers'); /** @@ -29,7 +30,7 @@ class APIClient extends APIClientBase { * @access protected * @ignore */ - this._apiConfig = Object.freeze(apiConfig); + this._apiConfig = ObjectUtils.copy(apiConfig); /** * A local reference for the class the app uses to generate HTTP errors. * @type {Class} @@ -45,7 +46,28 @@ class APIClient extends APIClientBase { * @return {HTTPError} */ error(response, status) { - return new this._HTTPError(response.data.message, status); + return new this._HTTPError(this.getErrorMessageFromResponse(response), status); + } + /** + * Helper method that tries to get an error message from a given response. + * @param {Object} response A received response from a request. + * @param {string} [fallback='Unexpected error'] A fallback message in case the method doesn't + * found one on the response. + * @return {string} + */ + getErrorMessageFromResponse(response, fallback = 'Unexpected error') { + let message; + if (response.error) { + message = response.error; + } else if (response.data && response.data && response.data.message) { + ({ message } = response.data); + } else if (response.data && response.data && response.data.error) { + message = response.data.error; + } else { + message = fallback; + } + + return message; } /** * The configuration for the API the client will make requests to. @@ -55,7 +77,7 @@ class APIClient extends APIClientBase { * entry point. */ get apiConfig() { - return this._apiConfig; + return Object.freeze(this._apiConfig); } } /** diff --git a/src/services/http/index.js b/src/services/http/index.js index b95e90d9..715fbbc4 100644 --- a/src/services/http/index.js +++ b/src/services/http/index.js @@ -1,13 +1,16 @@ +const { apiClient } = require('./apiClient'); const { http } = require('./http'); const { responsesBuilder } = require('./responsesBuilder'); const { providers } = require('../../utils/wrappers'); /** * The providers collection for the HTTP services. * @type {Provider} + * @property {Provider} apiClient The provider for {@link APIClient}. * @property {Provider} http The provider for {@link HTTP}. * @property {Provider} responsesBuilder The provider for {@link ResponsesBuilder}. */ const httpServices = providers({ + apiClient, http, responsesBuilder, }); diff --git a/src/services/index.js b/src/services/index.js index 55d8c893..2a6e3e59 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -1,4 +1,3 @@ -const api = require('./api'); const common = require('./common'); const frontend = require('./frontend'); const html = require('./html'); @@ -6,7 +5,6 @@ const http = require('./http'); const utils = require('./utils'); module.exports = { - api, common, frontend, html, diff --git a/src/services/utils/index.js b/src/services/utils/index.js index 5a93cdbd..ecf09532 100644 --- a/src/services/utils/index.js +++ b/src/services/utils/index.js @@ -1,5 +1,12 @@ const { ensureBearerToken } = require('./ensureBearerToken'); - -module.exports = { +const { providers } = require('../../utils/wrappers'); +/** + * The providers collection for the utils services. + * @type {Provider} + * @property {Provider} ensureBearerToken The provider for {@link EnsureBearerToken}. + */ +const utilsServices = providers({ ensureBearerToken, -}; +}); + +module.exports = utilsServices; diff --git a/tests/app/index.test.js b/tests/app/index.test.js index b5ee1577..768d045e 100644 --- a/tests/app/index.test.js +++ b/tests/app/index.test.js @@ -11,9 +11,9 @@ jest.mock('compression', () => compressionMock); jest.mock('multer', () => multerMock); jest.mock('body-parser', () => bodyParserMock); jest.mock('wootils/node/providers', () => wootilsMock); -jest.mock('/src/services/api', () => 'apiServices'); jest.mock('/src/services/common', () => 'commonServices'); jest.mock('/src/services/http', () => 'httpServices'); +jest.mock('/src/services/utils', () => 'utilsServices'); jest.unmock('/src/app/index'); const path = require('path'); @@ -72,7 +72,7 @@ describe('app:Jimpex', () => { 'packageInfo', 'pathUtils', 'rootRequire', - 'apiServices', + 'utilsServices', 'commonServices', 'httpServices', 'appConfiguration', @@ -474,9 +474,9 @@ describe('app:Jimpex', () => { // When sut = new Sut(true, { defaultServices: { - api: false, common: false, http: false, + utils: false, }, }); // Then diff --git a/tests/services/api/client.test.js b/tests/services/http/apiClient.test.js similarity index 75% rename from tests/services/api/client.test.js rename to tests/services/http/apiClient.test.js index f873c8d1..ac6a4f3a 100644 --- a/tests/services/api/client.test.js +++ b/tests/services/http/apiClient.test.js @@ -1,14 +1,14 @@ jest.unmock('/src/utils/wrappers'); -jest.unmock('/src/services/api/client'); +jest.unmock('/src/services/http/apiClient'); require('jasmine-expect'); const { APIClient, apiClient, -} = require('/src/services/api/client'); +} = require('/src/services/http/apiClient'); const APIClientBase = require('wootils/shared/apiClient'); -describe('services/api:client', () => { +describe('services/http:client', () => { it('should be instantiated with its configuration', () => { // Given const apiConfig = { @@ -49,21 +49,55 @@ describe('services/api:client', () => { this.status = status; } } - const response = { - data: { - message: 'Damn it!', + const message = 'Damn it!'; + const responses = [ + { + error: message, }, - }; + { + data: { + message, + }, + }, + { + data: { + error: message, + }, + }, + ]; const status = 409; let sut = null; - let result = null; + let results = null; // When sut = new APIClient(apiConfig, http, HTTPError); - result = sut.error(response, status); + results = responses.map((response) => sut.error(response, status)); + // Then + results.forEach((result) => { + expect(result).toBeInstanceOf(HTTPError); + expect(result.message).toBe(message); + expect(result.status).toBe(status); + }); + }); + + it('should have a fallback for when it can\'t find an error on a response', () => { + // Given + const apiConfig = { + url: 'my-api', + endpoints: { + info: 'api-info', + }, + }; + const http = { + fetch: () => {}, + }; + const fallback = 'Damn it!'; + let sut = null; + let result = null; + // When + sut = new APIClient(apiConfig, http, 'HTTPError'); + result = sut.getErrorMessageFromResponse({}, fallback) // Then - expect(result).toBeInstanceOf(HTTPError); - expect(result.message).toBe(response.data.message); - expect(result.status).toBe(status); + expect(result).toBe(fallback); }); it('should include a provider for the DIC', () => { @@ -98,7 +132,7 @@ describe('services/api:client', () => { // Then expect(sut).toBeInstanceOf(APIClientBase); expect(sut).toBeInstanceOf(APIClient); - expect(sut.apiConfig).toBe(appConfiguration.api); + expect(sut.apiConfig).toEqual(appConfiguration.api); expect(sut.url).toBe(appConfiguration.api.url); expect(sut.endpoints).toEqual(appConfiguration.api.endpoints); expect(sut.fetchClient).toBe(http.fetch); @@ -142,7 +176,7 @@ describe('services/api:client', () => { // Then expect(sut).toBeInstanceOf(APIClientBase); expect(sut).toBeInstanceOf(APIClient); - expect(sut.apiConfig).toBe(appConfiguration.apiConfig); + expect(sut.apiConfig).toEqual(appConfiguration.apiConfig); expect(sut.url).toBe(appConfiguration.apiConfig.url); expect(sut.endpoints).toEqual(appConfiguration.apiConfig.endpoints); expect(sut.fetchClient).toBe(http.fetch); diff --git a/tests/services/http/index.test.js b/tests/services/http/index.test.js index 7ef80249..d52826a7 100644 --- a/tests/services/http/index.test.js +++ b/tests/services/http/index.test.js @@ -5,25 +5,30 @@ require('jasmine-expect'); const httpServices = require('/src/services/http'); describe('services:http', () => { - it('should export a method to register all the API services', () => { + it('should export a method to register all the HTTP services', () => { // Given const app = { register: jest.fn(), }; - const expectedServices = [ - 'http', - 'responsesBuilder', - ]; + const expectedServices = { + apiClient: expect.any(Function), + http: { + register: expect.any(Function), + provider: true, + }, + responsesBuilder: { + register: expect.any(Function), + provider: true, + }, + }; + const expectedServicesNames = Object.keys(expectedServices); // When httpServices.register(app); // When/Then - expect(app.register).toHaveBeenCalledTimes(expectedServices.length); - expectedServices.forEach((service, index) => { - const registeredService = app.register.mock.calls[index][0]; - expect(registeredService).toEqual({ - register: expect.any(Function), - provider: true, - }); + expect(app.register).toHaveBeenCalledTimes(expectedServicesNames.length); + expectedServicesNames.forEach((service, index) => { + const [registeredService] = app.register.mock.calls[index]; + expect(registeredService).toEqual(expectedServices[service]); expect(registeredService.toString()).toBe(httpServices[service].toString()); }); }); diff --git a/tests/services/index.test.js b/tests/services/index.test.js index 46b98ce3..bfc2e92a 100644 --- a/tests/services/index.test.js +++ b/tests/services/index.test.js @@ -8,7 +8,6 @@ describe('services', () => { it('should export all the app sevices', () => { // Given const knownServices = [ - 'api', 'common', 'frontend', 'html', diff --git a/tests/services/api/index.test.js b/tests/services/utils/index.test.js similarity index 68% rename from tests/services/api/index.test.js rename to tests/services/utils/index.test.js index bdab6ddf..d96b3b1f 100644 --- a/tests/services/api/index.test.js +++ b/tests/services/utils/index.test.js @@ -1,27 +1,27 @@ jest.unmock('/src/utils/wrappers'); -jest.unmock('/src/services/api'); +jest.unmock('/src/services/utils'); require('jasmine-expect'); -const apiServices = require('/src/services/api'); +const utilsServices = require('/src/services/utils'); -describe('services:api', () => { +describe('services:utils', () => { it('should export a method to register all the API services', () => { // Given const app = { register: jest.fn(), }; const expectedServices = { - apiClient: expect.any(Function), + ensureBearerToken: expect.any(Function), }; const expectedServicesNames = Object.keys(expectedServices); // When - apiServices.register(app); + utilsServices.register(app); // When/Then expect(app.register).toHaveBeenCalledTimes(expectedServicesNames.length); expectedServicesNames.forEach((service, index) => { const [registeredService] = app.register.mock.calls[index]; expect(registeredService).toEqual(expectedServices[service]); - expect(registeredService.toString()).toBe(apiServices[service].toString()); + expect(registeredService.toString()).toBe(utilsServices[service].toString()); }); }); }); From 4b9ff2085e2e7d730cb07b8f946378e7a74a56a9 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 20:23:41 -0300 Subject: [PATCH 44/78] docs(project): update all the markdown documents with the latest changes --- README-esdoc.md | 137 ++++++++++++++++++++++++++++++++++++++- README.md | 137 ++++++++++++++++++++++++++++++++++++++- documents/controllers.md | 6 +- documents/middlewares.md | 38 +++++------ documents/options.md | 7 +- documents/services.md | 87 +++++++++++++++++++------ 6 files changed, 363 insertions(+), 49 deletions(-) diff --git a/README-esdoc.md b/README-esdoc.md index af40835f..1da0a2bd 100644 --- a/README-esdoc.md +++ b/README-esdoc.md @@ -137,6 +137,50 @@ class MyApp extends Jimpex { Done, your service is now available. +#### Defining a configurable service + +In case you want to create a service that could accept custom setting when instantiated, you can use a _"provider creator"_: + +```js +const { providerCreator } = require('jimpex'); + +// Create your service +class MyService { + constructor(depOne, depTwo, options = {}); +} + +// Define the provider +const myService = providerCreator((options) => (app) => { + app.set('myService', () => new MyService( + app.get('depOne'), + app.get('depTwo'), + settings + )); +}); + +// Export the service and its provider +module.exports = { + MyService, + myService, +}; +``` + +The special behavior the creators have, is that you can call them as a function, sending the settings, or just use them on the `register`, so **it's very important that the settings must be optional**: + +```js +const { Jimpex } = require('jimpex'); +const { myService } = require('...'); + +class MyApp extends Jimpex { + boot() { + ... + this.register(myService); + // or + this.register(myService({ ... })); + } +} +``` + ### Adding a controller To add controller you need to use the `controller` function and return a list of routes: @@ -188,6 +232,59 @@ class MyApp extends Jimpex { } ``` +#### Defining a configurable controller + +Like with _"providers creators", you can define controllers that accept custom settings when +instantiated, using a _"controller creator"_: + +```js +const { controllerCreator } = require('jimpex'); + +// (Optional) Define a class to organize your route handlers. +class HealthController { + constructor(settings = {}); + + health() { + return (req, res) => { + res.write('Everything works!'); + }; + } +} + +// Define the controller +const healthController = controllerCreator((settings) => (app) => { + const ctrl = new HealthController(settings); + // Get the router service + const router = app.get('router'); + // Return the list of routes this controller will handle + return [ + router.get('/', ctrl.health()), + ]; +}); + +// Export the controller class and the controller itself +module.exports = { + HealthController, + healthController, +}; +``` + +The special behavior the creators have, is that you can call them as a function, sending the settings, or just use them with `mount` as regular controllers; and since they can be used as regular controllers, **it's very important that the settings are optional**: + +```js +const { Jimpex } = require('jimpex'); +const { healthController } = require('...'); + +class MyApp extends Jimpex { + boot() { + ... + this.mount('/health', healthController); + // or + this.mount('/health', healthController({ ... })); + } +} +``` + ### Adding a middleware To add a new middleware you need to use the `middleware` function and return a function: @@ -226,6 +323,44 @@ class MyApp extends Jimpex { } ``` +#### Defining a configurable middleware + +Like with controllers and providers, you can also create a middleware that can accept settings when instantiated, with a _"middleware creator"_: + +```js +const { middlwareCreator } = require('jimpex'); + +// Define your middleware function (or class if it gets more complex) +const greetingsMiddleware = (message = 'Hello!') => (req, res, next) => { + console.log(message); +}; + +// Define the middleware +const greetings = middlewareCreator((message) => greetingsMiddleware(message)); + +// Export the function and the middleware +module.exports = { + greetingsMiddleware, + greetings, +}; +``` + +The special behavior the creators have, is that you can call them as a function, sending the settings, or just register them with `use` as regular middlewares, so **it's very important that the settings must be optional**: + +```js +const { Jimpex } = require('jimpex'); +const { greetings } = require('...'); + +class MyApp extends Jimpex { + boot() { + ... + this.use(greetings); + // or + this.use(greetings('Howdy!')); + } +} +``` + ## Built-in features Jimpex comes with a few services, middlewares and controllers that you can import and use on your app, some of them [are activated by default on the options](manual/options.html), but others you have to implement manually: @@ -252,7 +387,7 @@ Jimpex comes with a few services, middlewares and controllers that you can impor - **API client:** An implementation of the [wootils API Client](https://github.com/homer0/wootils/blob/master/documents/shared/APIClient.md) but that is connected to the HTTP service, to allow logging and forwarding of the headers. - **App Error:** A very simple subclass of `Error` but with support for context information. It can be used to customize the error handler responses. -- **Ensure bearer authentication:** A service-middleware that allows you to validate the incoming requests `Authorization` header. +- **Ensure bearer token:** A service-middleware that allows you to validate and retrieve a bearer token from the incoming requests `Authorization` header. - **HTTP Error:** Another type of error, but specific for the HTTP requests the app does with the API client. - **Send File:** It allows you to send a file on a response with a path relative to the app executable. - **Frontend Fs:** Useful for when your app has a bundled frontend, it allows you to read, write and delete files with paths relative to the app executable. diff --git a/README.md b/README.md index fe158f4c..3544c4d1 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,50 @@ class MyApp extends Jimpex { Done, your service is now available. +#### Defining a configurable service + +In case you want to create a service that could accept custom setting when instantiated, you can use a _"provider creator"_: + +```js +const { providerCreator } = require('jimpex'); + +// Create your service +class MyService { + constructor(depOne, depTwo, options = {}); +} + +// Define the provider +const myService = providerCreator((options) => (app) => { + app.set('myService', () => new MyService( + app.get('depOne'), + app.get('depTwo'), + settings + )); +}); + +// Export the service and its provider +module.exports = { + MyService, + myService, +}; +``` + +The special behavior the creators have, is that you can call them as a function, sending the settings, or just use them on the `register`, so **it's very important that the settings must be optional**: + +```js +const { Jimpex } = require('jimpex'); +const { myService } = require('...'); + +class MyApp extends Jimpex { + boot() { + ... + this.register(myService); + // or + this.register(myService({ ... })); + } +} +``` + ### Adding a controller To add controller you need to use the `controller` function and return a list of routes: @@ -188,6 +232,59 @@ class MyApp extends Jimpex { } ``` +#### Defining a configurable controller + +Like with _"providers creators", you can define controllers that accept custom settings when +instantiated, using a _"controller creator"_: + +```js +const { controllerCreator } = require('jimpex'); + +// (Optional) Define a class to organize your route handlers. +class HealthController { + constructor(settings = {}); + + health() { + return (req, res) => { + res.write('Everything works!'); + }; + } +} + +// Define the controller +const healthController = controllerCreator((settings) => (app) => { + const ctrl = new HealthController(settings); + // Get the router service + const router = app.get('router'); + // Return the list of routes this controller will handle + return [ + router.get('/', ctrl.health()), + ]; +}); + +// Export the controller class and the controller itself +module.exports = { + HealthController, + healthController, +}; +``` + +The special behavior the creators have, is that you can call them as a function, sending the settings, or just use them with `mount` as regular controllers; and since they can be used as regular controllers, **it's very important that the settings are optional**: + +```js +const { Jimpex } = require('jimpex'); +const { healthController } = require('...'); + +class MyApp extends Jimpex { + boot() { + ... + this.mount('/health', healthController); + // or + this.mount('/health', healthController({ ... })); + } +} +``` + ### Adding a middleware To add a new middleware you need to use the `middleware` function and return a function: @@ -226,6 +323,44 @@ class MyApp extends Jimpex { } ``` +#### Defining a configurable middleware + +Like with controllers and providers, you can also create a middleware that can accept settings when instantiated, with a _"middleware creator"_: + +```js +const { middlwareCreator } = require('jimpex'); + +// Define your middleware function (or class if it gets more complex) +const greetingsMiddleware = (message = 'Hello!') => (req, res, next) => { + console.log(message); +}; + +// Define the middleware +const greetings = middlewareCreator((message) => greetingsMiddleware(message)); + +// Export the function and the middleware +module.exports = { + greetingsMiddleware, + greetings, +}; +``` + +The special behavior the creators have, is that you can call them as a function, sending the settings, or just register them with `use` as regular middlewares, so **it's very important that the settings must be optional**: + +```js +const { Jimpex } = require('jimpex'); +const { greetings } = require('...'); + +class MyApp extends Jimpex { + boot() { + ... + this.use(greetings); + // or + this.use(greetings('Howdy!')); + } +} +``` + ## Built-in features Jimpex comes with a few services, middlewares and controllers that you can import and use on your app, some of them [are activated by default on the options](./documents/options.md), but others you have to implement manually: @@ -252,7 +387,7 @@ Jimpex comes with a few services, middlewares and controllers that you can impor - **API client:** An implementation of the [wootils API Client](https://github.com/homer0/wootils/blob/master/documents/shared/APIClient.md) but that is connected to the HTTP service, to allow logging and forwarding of the headers. - **App Error:** A very simple subclass of `Error` but with support for context information. It can be used to customize the error handler responses. -- **Ensure bearer authentication:** A service-middleware that allows you to validate the incoming requests `Authorization` header. +- **Ensure bearer token:** A service-middleware that allows you to validate and retrieve a bearer token from the incoming requests `Authorization` header. - **HTTP Error:** Another type of error, but specific for the HTTP requests the app does with the API client. - **Send File:** It allows you to send a file on a response with a path relative to the app executable. - **Frontend Fs:** Useful for when your app has a bundled frontend, it allows you to read, write and delete files with paths relative to the app executable. diff --git a/documents/controllers.md b/documents/controllers.md index 35b900b9..60141f0f 100644 --- a/documents/controllers.md +++ b/documents/controllers.md @@ -105,7 +105,7 @@ class App extends Jimpex { } ``` -By default, it serves an `index.html` and a `favicon.ico`, but you can use the _"controller generator"_ `rootStaticsControllerCustom` to modify those values: +By default, it serves an `index.html` and a `favicon.ico`, but you can use it as a function to modify those values: ```js const { @@ -114,7 +114,7 @@ const { common: { sendFile }, }, controllers: { - common: { rootStaticsControllerCustom }, + common: { rootStaticsController }, }, }; @@ -124,7 +124,7 @@ class App extends Jimpex { this.register(sendFile); // Add the controller. - this.mount('/', rootStaticsControllerCustom([ + this.mount('/', rootStaticsController([ 'my-file-one.html', 'favicon.icon', 'index.html', diff --git a/documents/middlewares.md b/documents/middlewares.md index 8bdca5bf..a25320d0 100644 --- a/documents/middlewares.md +++ b/documents/middlewares.md @@ -37,9 +37,9 @@ class App extends Jimpex { Now, there's a configuration setting for this controller: `debug.showErrors`. By enabling the setting, the middleware will show the message and the stack information of all kind of errors. -If the configuration setting is disabled (or not present), the errors stack will never be visible, and if the error is not an instance of the `AppError` service, it will show a generic message. +If the configuration setting is disabled (or not present), the errors stack will never be visible, and if the error is not an instance of `AppError`, it will show a generic message. -By default, the generic message is _"Oops! Something went wrong, please try again"_ and the default HTTP status is `500`, but you can use the _"middleware generator"_ `errorHandlerCustom` to modify those defaults: +By default, the generic message is _"Oops! Something went wrong, please try again"_ and the default HTTP status is `500`, but you can use it as a function to modify those defaults: ```js const { @@ -49,7 +49,7 @@ const { common: { appError }, }, middlewares: { - common: { errorHandlerCustom }, + common: { errorHandler }, }, }; @@ -62,7 +62,7 @@ class App extends Jimpex { ... // Add the middleware at the end. - this.use(errorHandlerCustom({ + this.use(errorHandler({ default: { message: 'Unknown error', status: 503, @@ -107,20 +107,20 @@ class App extends Jimpex { } ``` -By default, it redirects all the URLs that don't start with `/service/` from HTTP to HTTPs, but you can use the _"middleware generator"_ `forceHTTPSCustom` to modify the rules: +By default, it redirects all the URLs that don't start with `/service/` from HTTP to HTTPs, but you can use it as a function to modify the rules: ```js const { Jimpex, middlewares: { - common: { forceHTTPSCustom }, + common: { forceHTTPS }, }, }; class App extends Jimpex { boot() { // Add the middleware first. - this.use(forceHTTPSCustom([ + this.use(forceHTTPS([ /^\/service\//, /^\/api\//, ])); @@ -159,7 +159,7 @@ class App extends Jimpex { } ``` -By default, if the requested URL doesn't match `/^\/api\//` or `/\.ico$/` it serves an `index.html`, but you can use the _"middleware generator"_ `fastHTMLCustom` to modify those options: +By default, if the requested URL doesn't match `/^\/api\//` or `/\.ico$/` it serves an `index.html`, but you can use it as a function to modify those options: ```js const { @@ -168,7 +168,7 @@ const { common: { sendFile }, }, middlewares: { - html: { fastHTMLCustom }, + html: { fastHTML }, }, }; @@ -178,7 +178,7 @@ class App extends Jimpex { this.register(sendFile); // Add the middleware on one of the first positions. - this.use(fastHTMLCustom( + this.use(fastHTML( 'my-custom-index.html', [`/^\/service\//`] )); @@ -188,7 +188,7 @@ class App extends Jimpex { Now, as mentioned on the requirements, you can optionally use the `htmlGenerator` or an `HTMLGenerator` service to serve a generated file. -The default implementation checks if there's an `htmlGenerator` service registered on the app and uses that file; and in the case of `fastHTMLCustom`, you can specify a third parameter with the name of the `HTMLGenerator` service name you want to use. +The default implementation checks if there's an `htmlGenerator` service registered on the app and uses that file; and in the case of `fastHTML`, you can specify a third parameter with the name of the `HTMLGenerator` service name you want to use. ## Show HTML @@ -219,7 +219,7 @@ class App extends Jimpex { } ``` -By default, if the middleware is reached, it will show an `index.html`, but you can use the _"middleware generator"_ `showHTMLCustom` to modify the filename: +By default, if the middleware is reached, it will show an `index.html`, but you can use it as a function to modify the filename: ```js const { @@ -228,7 +228,7 @@ const { common: { sendFile }, }, middlewares: { - html: { showHTMLCustom }, + html: { showHTML }, }, }; @@ -238,14 +238,14 @@ class App extends Jimpex { this.register(sendFile); // Add the middleware at the end. - this.use(showHTMLCustom('my-file.html')); + this.use(showHTML('my-file.html')); } } ``` Now, as mentioned on the requirements, you can optionally use the `htmlGenerator` or an `HTMLGenerator` service to show the generated file. -The default implementation checks if there's an `htmlGenerator` service registered on the app and uses that file; and in the case of `showHTMLCustom `, you can specify a second parameter with the name of the `HTMLGenerator` service name you want to use. +The default implementation checks if there's an `htmlGenerator` service registered on the app and uses that file; and in the case of `showHTML`, you can specify a second parameter with the name of the `HTMLGenerator` service name you want to use. ## Version validator @@ -272,26 +272,26 @@ class App extends Jimpex { } ``` -By default, it comes with a lot of already defined options, like whether or not to allow `latest` as a version, but you can use the _"middleware generator"_ `versionValidatorCustom` to modify them, for example: +By default, it comes with a lot of already defined options, like whether or not to allow `latest` as a version, but you can use it as a function to modify them, for example: ```js const { Jimpex, middlewares: { - utils: { versionValidatorCustom }, + utils: { versionValidator }, }, }; class App extends Jimpex { boot() { // Add the middleware before the routes you want to be protected. - this.use(versionValidatorCustom({ + this.use(versionValidator({ latest: { allow: false, } })); // or, protect a specific route. - this.mount('/to-protect', versionValidatorCustom({ + this.mount('/to-protect', versionValidator({ latest: { allow: false, } diff --git a/documents/options.md b/documents/options.md index 39891cdb..74ca9b4c 100644 --- a/documents/options.md +++ b/documents/options.md @@ -122,15 +122,14 @@ These options allow you to register some of the built-in service that I consider common: true, // These services include: + // - API client // - HTTP // - Responses builder http: true, // These services include: - // - API client - // - Ensure bearer authentication - // - Version validator - api: true, + // - Ensure bearer token + utils: true, } ``` diff --git a/documents/services.md b/documents/services.md index e55dcbf4..be34d2ac 100644 --- a/documents/services.md +++ b/documents/services.md @@ -8,16 +8,15 @@ In the case of the services from the modules `api`, `http` and `common`, you can An implementation of the [wootils API Client](https://github.com/homer0/wootils/blob/master/documents/shared/APIClient.md) but that is connected to the HTTP service, to allow logging and forwarding of the headers. -- Module: `api` +- Module: `http` - Requires: `http` and `appError` ```js const { Jimpex, services: { - api: { apiClient }, common: { appError }, - http: { http }, + http: { apiClient, http }, }, }; @@ -33,15 +32,14 @@ class App extends Jimpex { } ``` -By default, the service is registered with the name `apiClient`, the API entry point is taken from the configuration setting `api.url` and the endpoints from `api.endpoints`, but you can use the _"service generator"_ `apiClientCustom` to modify those options: +By default, the service is registered with the name `apiClient`, the API entry point is taken from the configuration setting `api.url` and the endpoints from `api.endpoints`, but you can use it as a function to modify those options: ```js const { Jimpex, services: { - api: { apiClientCustom }, common: { appError }, - http: { http }, + http: { apiClient, http }, }, }; @@ -52,10 +50,10 @@ class App extends Jimpex { this.register(appError); // Register the client - this.register({ + this.register(apiClient( 'myCustomAPIService', 'myapi' - }); + ); } } ``` @@ -100,21 +98,21 @@ throw appError('Something happened', { This is useful if you are building an app with multiple known exceptions, you can use the context to send useful information. -## Ensure bearer authentication +## Ensure bearer token -A service-middleware that allows you to validate the incoming requests `Authorization` header. +A service-middleware that allows you to validate and retrieve a bearer token from the incoming requests `Authorization` header. -It's a _"service-middleware"_ because when you access the service, it doesn't return a class instance, but a middleware function for you to use on your controller routes. +It's a _"service-middleware"_ because when you access the service, it doesn't return a class/service instance, but a middleware function for you to use on your controller routes. -- Module: `api` +- Module: `utils` - Requires: `appError` ```js const { Jimpex, services: { - api: { ensureBearerAuthentication }, common: { appError }, + utils: { ensureBearerToken }, }, }; @@ -124,21 +122,68 @@ class App extends Jimpex { this.register(appError); // Register the service - this.register(apiClient); + this.register(ensureBearerToken); + } +} +``` + +This service has a few default options: + +```js +{ + // The information for the error generated when no token is found. + error: { + // The error message. + message: 'Unauthorized', + // The HTTP status associated to the error, this is for the error handler. + status: statuses.unauthorized, + // Extra context information for the error handler to add to the response. + response: {}, + }, + // The regular expression used to validate and extract the token. + expression: /bearer (.*?)(?:$|\s)/i, + // The name of the property on `res.locals` where the token will be saved. + local: 'token', +} +``` + +You modify those default values by using the provider as a function when registering: + +```js +const { + Jimpex, + services: { + common: { appError }, + utils: { ensureBearerToken }, + }, +}; + +class App extends Jimpex { + boot() { + // Register the dependencies... + this.register(appError); + + // Register the service + this.register(ensureBearerToken({ + error: { + message: 'You are not authorized to access this route', + }, + local: 'userToken', + })); } } ``` -Now, if the token process a request an detects a valid token, it will set that token on the request `bearerToken` property: +Now, if the token processes a request and detects a valid token, it will save it on `res.locals.token`: ```js const myCtrl = controller((app) => { const router = app.get('router'); - const ensureAuthentication = app.get('ensureBearerAuthentication'); + const ensureBearerToken = app.get('ensureBearerToken'); return [router.get('/something', [ - ensureAuthentication, + ensureBearerToken, (req, res, next) => { - console.log('Token:', req.bearerToken); + console.log('Token:', res.locals.token); next(); }, ])]; @@ -319,14 +364,14 @@ Now, this service has a few default options, so instead of explaining which are, It also supports a custom service with a `getValues` method to obtain the information to inject instead of taking it from the configuration. -To modify the options, you need to use the _"service generator"_ `htmlGeneratorCustom`: +To modify the options, you just need to use provider as a function: ```js const { Jimpex, services: { frontend: { frontendFs }, - html: { htmlGeneratorCustom }, + html: { htmlGenerator }, }, }; @@ -336,7 +381,7 @@ class App extends Jimpex { this.register(frontendFs); // Register the service - this.register(htmlGeneratorCustom({ + this.register(htmlGenerator({ template: 'template.tpl', file: 'my-index.html', ... From 7d1819bc66ed9b01885b94fa3001f3b2150a62ed Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 23:27:24 -0300 Subject: [PATCH 45/78] feat(controllers): add a new statics controller --- src/controllers/common/index.js | 2 + src/controllers/common/statics.js | 292 +++++++++++++ src/typedef.js | 4 + tests/controllers/common/statics.test.js | 499 +++++++++++++++++++++++ 4 files changed, 797 insertions(+) create mode 100644 src/controllers/common/statics.js create mode 100644 tests/controllers/common/statics.test.js diff --git a/src/controllers/common/index.js b/src/controllers/common/index.js index 6d63b4d9..8c22bf3c 100644 --- a/src/controllers/common/index.js +++ b/src/controllers/common/index.js @@ -1,9 +1,11 @@ const { configurationController } = require('./configuration'); const { healthController } = require('./health'); const { rootStaticsController } = require('./rootStatics'); +const { staticsController } = require('./statics'); module.exports = { configurationController, healthController, rootStaticsController, + staticsController, }; diff --git a/src/controllers/common/statics.js b/src/controllers/common/statics.js new file mode 100644 index 00000000..b4af09f7 --- /dev/null +++ b/src/controllers/common/statics.js @@ -0,0 +1,292 @@ +const path = require('path'); +const ObjectUtils = require('wootils/shared/objectUtils'); +const mime = require('mime'); +const { controllerCreator } = require('../../utils/wrappers'); + +/** + * @typdef {Object} StaticsControllerFile + * @description If you wan to customize how, to, and from where files are served, instead of just + * sending a list of strings, you can use an object with these properties. + * @property {string} route The route the controller will use for the file. + * @property {string} path The path for the file, relative to the root of the app. + * @property {Object} headers A dictionary of custom headers to send on the file response. + */ + +/** + * @typedef {Object} StaticsControllerPathsOptions + * @description They are like "master paths" that get prepended to all the file paths and routes + * the controller use. + * @property {string} route A custom route to prefix all the file routes. + * @property {string} source A custom path to prefix all the file paths. + */ + +/** + * @typdef {Object} StaticsControllerOptions + * @description These are the options that allow you to customize the controller, how, to and from + * where the files are served. + * @property {Array} files A list of filenames or + * {@link StaticsControllerFile} definitions. + * @property {Object} methods A dictionary of all the HTTP methods the + * controller will use in order to serve the + * files. If `all` is set to true, all the other + * flags will be ignored. + * @property {StaticsControllerPathsOptions} paths The "master paths" the controller uses to + * prefix all file routes and paths. + */ + +/** + * This controller allows you to serve specific files from any folder to any route without the + * need of mounting directories as "static". + */ +class StaticsController { + /** + * @param {SendFile} sendFile To send the responses for the files. + * @param {StaticsControllerOptions} options The options to customize the controller. + */ + constructor(sendFile, options = {}) { + /** + * A local reference for the `sendFile` service. + * @type {SendFile} + * @access protected + * @ignore + */ + this._sendFile = sendFile; + /** + * The controller configuration options. + * @type {StaticsControllerOptions} + * @access protected + * @ignore + */ + this._options = this._normalizeOptions(ObjectUtils.merge( + { + files: options.files || ['favicon.ico', 'index.html'], + methods: { + all: false, + get: true, + }, + paths: { + route: '', + source: './', + }, + }, + options + )); + /** + * A dictionary of all the formatted files ({@link StaticsControllerFile}). It uses the files + * routes as keys. + * @access protected + * @ignore + */ + this._files = this._createFiles(); + } + /** + * Creates all the needed routes to serve the files. + * @param {ExpressRouter} router To generate the routes. + * @param {Array} [middlewares=[]] A list of custom middlewares that will be added + * before the one that serves a file. + * @return {Array} + */ + getRoutes(router, middlewares = []) { + const { methods } = this._options; + const use = methods.all ? + ['all'] : + Object.keys(methods).reduce((acc, name) => (methods[name] ? [...acc, name] : acc), []); + + return Object.keys(this._files) + .map((route) => { + const file = this._files[route]; + const fileMiddleware = this._getMiddleware(file); + return use.map((method) => this._getRoute( + router, + method, + file, + fileMiddleware, + middlewares + )); + }) + .reduce((allRoutes, routes) => [...allRoutes, ...routes], []); + } + /** + * The controller configuration options. + * @type {StaticsControllerOptions} + */ + get options() { + return Object.freeze(this._options); + } + /** + * Helper method that validates and normalizes the options received by the controller. + * @param {StaticsControllerOptions} options The options to validate. + * @return {StaticsControllerOptions} + * @throws {Error} If no files are specified. + * @throws {Error} If methods is not defined. + * @throws {Error} If no methods are enabled. + * @throws {Error} If there's an invalid HTTP method. + * @access protected + * @ignore + */ + _normalizeOptions(options) { + if (!options.files || !options.files.length) { + throw new Error('You need to specify a list of files'); + } else if (!options.methods) { + throw new Error('You need to specify which HTTP methods are allowed for the files'); + } + + const methods = Object.keys(options.methods); + + const atLeastOne = methods.some((method) => options.methods[method]); + if (!atLeastOne) { + throw new Error('You need to enable at least one HTTP method to serve the files'); + } + + const allowedMethods = [ + 'all', + 'get', + 'head', + 'post', + 'put', + 'delete', + 'connect', + 'options', + 'trace', + ]; + + const invalid = methods.find((method) => !allowedMethods.includes(method.toLowerCase())); + if (invalid) { + throw new Error(`${invalid} is not a valid HTTP method`); + } + + + const newMethods = methods.reduce( + (acc, method) => Object.assign({}, acc, { + [method.toLowerCase()]: options.methods[method], + }), + {} + ); + + return Object.assign({}, options, { + methods: newMethods, + }); + } + /** + * Parses each of the received files in order to create a {@link StaticsControllerFile}. + * @return {Object} A dictionary with the definitions as values and the routes as keys. + * @access protected + * @ignore + */ + _createFiles() { + const { files, paths } = this._options; + const routePath = this._removeTrailingSlash(paths.route); + return files.reduce( + (formatted, file) => { + let source; + let route; + let headers; + if (typeof file === 'object') { + ({ route, source, headers } = file); + } else { + source = file; + route = file; + } + + source = path.join(paths.source, source); + route = this._removeLeadingSlash(route); + route = `${routePath}/${route}`; + + return Object.assign({}, formatted, { + [route]: { + source, + route, + headers: headers || {}, + }, + }); + }, + {} + ); + } + /** + * Generates a route for an specific file. + * @param {ExpressRouter} router To create the actual route. + * @param {string} method The HTTP method for the route. + * @param {StaticsControllerFile} file The file information. + * @param {ExpressMiddleware} fileMiddleware The middleware that serves the file. + * @param {Array} middlewares A list of custom middlewares to add before the + * one that serves the file. + * @return {Object} The Express route + * @access protected + * @ignore + */ + _getRoute(router, method, file, fileMiddleware, middlewares) { + return router[method](file.route, [...middlewares, fileMiddleware]); + } + /** + * Generates the middleware to serve a specific file. + * @param {StaticsControllerFile} file The file information. + * @return {ExpressMiddleware} + * @access protected + * @ignore + */ + _getMiddleware(file) { + return (req, res, next) => { + const extension = path.parse(file.source).ext.substr(1); + const headers = ObjectUtils.merge( + { 'Content-Type': mime.getType(extension) }, + file.headers + ); + + Object.keys(headers).forEach((headerName) => { + res.setHeader(headerName, headers[headerName]); + }); + + this._sendFile(res, file.source, next); + }; + } + /** + * Helper method to remove the leading slash from a URL. + * @param {string} url The URL to format. + * @return {string} + * @access protected + * @ignore + */ + _removeLeadingSlash(url) { + return url.replace(/^\/+/, ''); + } + /** + * Helper method to remove the trailing slash from a URL. + * @param {string} url The URL to format. + * @return {string} + * @access protected + * @ignore + */ + _removeTrailingSlash(url) { + return url.replace(/\/+$/, ''); + } +} +/** + * This controller allows you to serve specific files from any folder to any route without the + * need of mounting directories as "static". + * @type {ControllerCreator} + * @param {StaticsControllerOptions} [options] The options to customize the controller. + * @param {Function():Array} [middlewares] This function can be used to add custom + * middlewares on the file routes. If implemented, + * it must return a list of middlewares when + * executed. + */ +const staticsController = controllerCreator((options, middlewares) => (app) => { + const router = app.get('router'); + const ctrl = new StaticsController(app.get('sendFile'), options); + let useMiddlewares; + if (middlewares) { + useMiddlewares = middlewares(app).map((middleware) => ( + middleware.connect ? + middleware.connect(app) : + middleware + )); + } + + return ctrl.getRoutes(router, useMiddlewares); +}); + +module.exports = { + StaticsController, + staticsController, +}; diff --git a/src/typedef.js b/src/typedef.js index 5adb7345..8bc85966 100644 --- a/src/typedef.js +++ b/src/typedef.js @@ -54,6 +54,10 @@ * @external {ExpressResponse} https://expressjs.com/en/4x/api.html#res */ +/** + * @external {ExpressRouter} https://expressjs.com/en/4x/api.html#router + */ + /** * @typdef {function} ExpressNext * @description A function to call the next middleware. If an argument is specified, it will be diff --git a/tests/controllers/common/statics.test.js b/tests/controllers/common/statics.test.js new file mode 100644 index 00000000..e5cd0bb9 --- /dev/null +++ b/tests/controllers/common/statics.test.js @@ -0,0 +1,499 @@ +jest.unmock('/src/utils/wrappers'); +jest.unmock('/src/controllers/common/statics'); + +const path = require('path'); +require('jasmine-expect'); +const { + StaticsController, + staticsController, +} = require('/src/controllers/common/statics'); + +describe('controllers/common:statics', () => { + it('should be instantiated with its default options', () => { + // Given + const sendFile = 'sendFile'; + let sut = null; + // When + sut = new StaticsController(sendFile); + // Then + expect(sut).toBeInstanceOf(StaticsController); + expect(sut.getRoutes).toBeFunction(); + expect(sut.options).toEqual({ + files: ['favicon.ico', 'index.html'], + methods: { + all: false, + get: true, + }, + paths: { + source: './', + route: '', + }, + }); + }); + + it('should be instantiated with custom options', () => { + // Given + const sendFile = 'sendFile'; + const options = { + files: ['myfile.html'], + methods: { + all: true, + get: false, + post: false, + }, + paths: { + source: '../', + route: 'statics/', + }, + }; + let sut = null; + // When + sut = new StaticsController(sendFile, options); + // Then + expect(sut).toBeInstanceOf(StaticsController); + expect(sut.getRoutes).toBeFunction(); + expect(sut.options).toEqual(options); + }); + + it('should throw an error when instantiated without files', () => { + // Given + const sendFile = 'sendFile'; + const options = { + files: [], + }; + // When/Then + expect(() => new StaticsController(sendFile, options)) + .toThrow(/You need to specify a list of files/i); + }); + + it('should throw an error when instantiated without HTTP methods', () => { + // Given + const sendFile = 'sendFile'; + const options = { + files: ['charito.html'], + methods: null, + }; + // When/Then + expect(() => new StaticsController(sendFile, options)) + .toThrow(/You need to specify which HTTP methods are allowed for the files/i); + }); + + it('should throw an error when instantiated without an enabled HTTP method', () => { + // Given + const sendFile = 'sendFile'; + const options = { + files: ['charito.html'], + methods: { + get: false, + }, + }; + // When/Then + expect(() => new StaticsController(sendFile, options)) + .toThrow(/You need to enable at least one HTTP method to serve the files/i); + }); + + it('should throw an error when instantiated with an invalid HTTP method', () => { + // Given + const sendFile = 'sendFile'; + const options = { + files: ['charito.html'], + methods: { + get: true, + magic: true, + }, + }; + // When/Then + expect(() => new StaticsController(sendFile, options)) + .toThrow(/is not a valid HTTP method/i); + }); + + it('should generate `get` routes for all the files', () => { + // Given + const sendFile = 'sendFile'; + const options = { + files: ['charito.html'], + }; + const route = 'route'; + const router = { + get: jest.fn(() => route), + }; + let sut = null; + let result = null; + // When + sut = new StaticsController(sendFile, options); + result = sut.getRoutes(router); + // Then + expect(result).toEqual(options.files.map(() => route)); + expect(router.get).toHaveBeenCalledTimes(options.files.length); + options.files.forEach((file) => { + expect(router.get).toHaveBeenCalledWith(`/${file}`, [expect.any(Function)]); + }); + }); + + it('should generate routes using `all`', () => { + // Given + const sendFile = 'sendFile'; + const options = { + files: ['charito.html'], + methods: { + all: true, + }, + }; + const route = 'route'; + const router = { + all: jest.fn(() => route), + }; + let sut = null; + let result = null; + // When + sut = new StaticsController(sendFile, options); + result = sut.getRoutes(router); + // Then + expect(result).toEqual(options.files.map(() => route)); + expect(router.all).toHaveBeenCalledTimes(options.files.length); + options.files.forEach((file) => { + expect(router.all).toHaveBeenCalledWith(`/${file}`, [expect.any(Function)]); + }); + }); + + it('should generate routes using custom methods', () => { + // Given + const sendFile = 'sendFile'; + const options = { + files: ['charito.html'], + methods: { + get: false, + post: true, + put: true, + }, + }; + const postRoute = 'post-route'; + const putRoute = 'put-route'; + const router = { + post: jest.fn(() => postRoute), + put: jest.fn(() => putRoute), + }; + let sut = null; + let result = null; + // When + sut = new StaticsController(sendFile, options); + result = sut.getRoutes(router); + // Then + expect(result).toEqual( + options.files + .map(() => [postRoute, putRoute]) + .reduce((acc, routes) => [...acc, ...routes], []) + ); + expect(router.post).toHaveBeenCalledTimes(options.files.length); + expect(router.put).toHaveBeenCalledTimes(options.files.length); + options.files.forEach((file) => { + expect(router.post).toHaveBeenCalledWith(`/${file}`, [expect.any(Function)]); + expect(router.put).toHaveBeenCalledWith(`/${file}`, [expect.any(Function)]); + }); + }); + + it('should add custom middlewares to the routes', () => { + // Given + const sendFile = 'sendFile'; + const options = { + files: ['charito.html'], + }; + const middlewares = ['middlewareOne', 'middlewareTwo']; + const route = 'route'; + const router = { + get: jest.fn(() => route), + }; + let sut = null; + let result = null; + // When + sut = new StaticsController(sendFile, options); + result = sut.getRoutes(router, middlewares); + // Then + expect(result).toEqual(options.files.map(() => route)); + expect(router.get).toHaveBeenCalledTimes(options.files.length); + options.files.forEach((file) => { + expect(router.get).toHaveBeenCalledWith(`/${file}`, [ + ...middlewares, + expect.any(Function), + ]); + }); + }); + + it('should generate a middleware to serve a file', () => { + // Given + const sendFile = jest.fn(); + const file = 'rosario.html'; + const options = { + files: [file], + }; + const route = 'route'; + const router = { + get: jest.fn(() => route), + }; + const request = 'request'; + const response = { + setHeader: jest.fn(), + }; + const next = 'next'; + let sut = null; + let middleware = null; + // When + sut = new StaticsController(sendFile, options); + sut.getRoutes(router); + [[, [middleware]]] = router.get.mock.calls; + middleware(request, response, next); + // Then + expect(response.setHeader).toHaveBeenCalledTimes(1); + expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html'); + expect(sendFile).toHaveBeenCalledTimes(1); + expect(sendFile).toHaveBeenCalledWith(response, file, next); + }); + + it('should generate a middleware to serve a file from a custom directory', () => { + // Given + const sendFile = jest.fn(); + const file = 'rosario.html'; + const options = { + files: [file], + paths: { + source: '../', + }, + }; + const route = 'route'; + const router = { + get: jest.fn(() => route), + }; + const request = 'request'; + const response = { + setHeader: jest.fn(), + }; + const next = 'next'; + let sut = null; + let middleware = null; + // When + sut = new StaticsController(sendFile, options); + sut.getRoutes(router); + [[, [middleware]]] = router.get.mock.calls; + middleware(request, response, next); + // Then + expect(response.setHeader).toHaveBeenCalledTimes(1); + expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html'); + expect(sendFile).toHaveBeenCalledTimes(1); + expect(sendFile).toHaveBeenCalledWith( + response, + path.join(options.paths.source, file), + next + ); + }); + + it('should generate a middleware to serve a file to a different route', () => { + // Given + const sendFile = jest.fn(); + const file = 'rosario.html'; + const options = { + files: [file], + paths: { + source: '../', + route: '/statics', + }, + }; + const route = 'route'; + const router = { + get: jest.fn(() => route), + }; + const request = 'request'; + const response = { + setHeader: jest.fn(), + }; + const next = 'next'; + let sut = null; + let middleware = null; + // When + sut = new StaticsController(sendFile, options); + sut.getRoutes(router); + [[, [middleware]]] = router.get.mock.calls; + middleware(request, response, next); + // Then + expect(router.get).toHaveBeenCalledTimes(1); + expect(router.get).toHaveBeenCalledWith( + `${options.paths.route}/${file}`, + [expect.any(Function)] + ); + expect(response.setHeader).toHaveBeenCalledTimes(1); + expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html'); + expect(sendFile).toHaveBeenCalledTimes(1); + expect(sendFile).toHaveBeenCalledWith( + response, + path.join(options.paths.source, file), + next + ); + }); + + it('should generate a middleware to serve a file with custom route and path', () => { + // Given + const sendFile = jest.fn(); + const file = { + source: '../rosario.png', + route: 'photos/charito.png', + }; + const options = { + files: [file], + paths: { + source: '../', + route: '/statics', + }, + }; + const route = 'route'; + const router = { + get: jest.fn(() => route), + }; + const request = 'request'; + const response = { + setHeader: jest.fn(), + }; + const next = 'next'; + let sut = null; + let middleware = null; + // When + sut = new StaticsController(sendFile, options); + sut.getRoutes(router); + [[, [middleware]]] = router.get.mock.calls; + middleware(request, response, next); + // Then + expect(router.get).toHaveBeenCalledTimes(1); + expect(router.get).toHaveBeenCalledWith( + `${options.paths.route}/${file.route}`, + [expect.any(Function)] + ); + expect(response.setHeader).toHaveBeenCalledTimes(1); + expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'image/png'); + expect(sendFile).toHaveBeenCalledTimes(1); + expect(sendFile).toHaveBeenCalledWith( + response, + path.join(options.paths.source, file.source), + next + ); + }); + + it('should generate a middleware to serve a file with custom headers', () => { + // Given + const sendFile = jest.fn(); + const customHeaderName = 'x-custom-header'; + const customHeaderValue = 'magic!'; + const file = { + source: '../rosario.png', + route: 'photos/charito.png', + headers: { + [customHeaderName]: customHeaderValue, + }, + }; + const options = { + files: [file], + paths: { + source: '../', + route: '/statics', + }, + }; + const route = 'route'; + const router = { + get: jest.fn(() => route), + }; + const request = 'request'; + const response = { + setHeader: jest.fn(), + }; + const next = 'next'; + let sut = null; + let middleware = null; + const expectedHeaders = [ + [customHeaderName, customHeaderValue], + ['Content-Type', 'image/png'], + ]; + // When + sut = new StaticsController(sendFile, options); + sut.getRoutes(router); + [[, [middleware]]] = router.get.mock.calls; + middleware(request, response, next); + // Then + expect(router.get).toHaveBeenCalledTimes(1); + expect(router.get).toHaveBeenCalledWith( + `${options.paths.route}/${file.route}`, + [expect.any(Function)] + ); + expect(response.setHeader).toHaveBeenCalledTimes(expectedHeaders.length); + expectedHeaders.forEach((setHeaderCall) => { + expect(response.setHeader).toHaveBeenCalledWith(...setHeaderCall); + }); + expect(sendFile).toHaveBeenCalledTimes(1); + expect(sendFile).toHaveBeenCalledWith( + response, + path.join(options.paths.source, file.source), + next + ); + }); + + it('should include a controller shorthand to return its routes', () => { + // Given + const services = { + router: { + get: jest.fn((route, middlewaresList) => [`get:${route}`, middlewaresList]), + }, + }; + const app = { + get: jest.fn((service) => (services[service] || service)), + }; + let routes = null; + const expectedGets = ['router', 'sendFile']; + const expectedFiles = ['favicon.ico', 'index.html']; + // When + routes = staticsController.connect(app); + // Then + expect(routes).toEqual(expectedFiles.map((file) => [`get:/${file}`, [expect.any(Function)]])); + expect(app.get).toHaveBeenCalledTimes(expectedGets.length); + expectedGets.forEach((service) => { + expect(app.get).toHaveBeenCalledWith(service); + }); + }); + + it('should include a controller creator to send custom options and middlewares', () => { + // Given + const options = { + files: ['rosario.html'], + }; + const normalMiddleware = 'middlewareOne'; + const jimpexMiddlewareName = 'middlewareTwo'; + const jimpexMiddleware = { + connect: jest.fn(() => jimpexMiddlewareName), + }; + const middlewares = [normalMiddleware, jimpexMiddleware]; + const middlewareGenerator = jest.fn(() => middlewares); + const services = { + router: { + get: jest.fn((route, middlewaresList) => [`get:${route}`, middlewaresList]), + }, + }; + const app = { + get: jest.fn((service) => (services[service] || service)), + }; + let routes = null; + const expectedGets = ['router', 'sendFile']; + // When + routes = staticsController(options, middlewareGenerator).connect(app); + // Then + expect(routes).toEqual(options.files.map((file) => [ + `get:/${file}`, + [ + ...[normalMiddleware, jimpexMiddlewareName], + expect.any(Function), + ], + ])); + expect(middlewareGenerator).toHaveBeenCalledTimes(1); + expect(middlewareGenerator).toHaveBeenCalledWith(app); + expect(jimpexMiddleware.connect).toHaveBeenCalledTimes(1); + expect(jimpexMiddleware.connect).toHaveBeenCalledWith(app); + expect(app.get).toHaveBeenCalledTimes(expectedGets.length); + expectedGets.forEach((service) => { + expect(app.get).toHaveBeenCalledWith(service); + }); + }); +}); From e98144f4ea80a4bef82a104daffeb1de67edc1c7 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 23:28:49 -0300 Subject: [PATCH 46/78] refactor(controllers): remove old root statics controller --- src/controllers/common/index.js | 2 - src/controllers/common/rootStatics.js | 122 ------------- tests/controllers/common/rootStatics.test.js | 183 ------------------- 3 files changed, 307 deletions(-) delete mode 100644 src/controllers/common/rootStatics.js delete mode 100644 tests/controllers/common/rootStatics.test.js diff --git a/src/controllers/common/index.js b/src/controllers/common/index.js index 8c22bf3c..73aa0a97 100644 --- a/src/controllers/common/index.js +++ b/src/controllers/common/index.js @@ -1,11 +1,9 @@ const { configurationController } = require('./configuration'); const { healthController } = require('./health'); -const { rootStaticsController } = require('./rootStatics'); const { staticsController } = require('./statics'); module.exports = { configurationController, healthController, - rootStaticsController, staticsController, }; diff --git a/src/controllers/common/rootStatics.js b/src/controllers/common/rootStatics.js deleted file mode 100644 index 19154f79..00000000 --- a/src/controllers/common/rootStatics.js +++ /dev/null @@ -1,122 +0,0 @@ -const ObjectUtils = require('wootils/shared/objectUtils'); -const mime = require('mime'); -const { controllerCreator } = require('../../utils/wrappers'); -/** - * Since the static files are inside a folder and we can't have the the `static` middleware pointing - * to the app root directory, this service allows you to serve static files that are on the root - * directory. - */ -class RootStaticsController { - /** - * Class constructor. - * @param {SendFile} sendFile To be able to send the files as - * responses. - * @param {Array} [files=['index.html', 'favicon.ico']] The list of files to serve. Each item - * can be a `string` or an `Object` with - * the keys `origin` for the file route, - * `output` for the file location - * relative to the root, and `headers` - * with the file custom headers for the - * response. - */ - constructor(sendFile, files = ['index.html', 'favicon.ico']) { - /** - * A local reference for the `sendFile` service. - * @type {SendFile} - * @access protected - * @ignore - */ - this._sendFile = sendFile; - /** - * A dictionary with the file names as keys and information about the files as values. - * @type {Object} - * @access protected - * @ignore - */ - this._files = this._parseFiles(files); - } - /** - * Gets the list of files the service will serve. - * @return {Array} - */ - getFileEntries() { - return Object.keys(this._files); - } - /** - * Generates a middleware to serve an specific file. - * @param {string} file The name of the file. - * @return {ExpressMiddleware} - * @throws {Error} If the file wasn't sent on the constructor. - */ - serveFile(file) { - if (!this._files[file]) { - throw new Error(`The required static file doesn't exist (${file})`); - } - - return (req, res, next) => { - const item = this._files[file]; - const extension = item.output.split('.').pop().toLowerCase(); - const baseHeaders = { 'Content-Type': mime.getType(extension) }; - const headers = ObjectUtils.merge(baseHeaders, item.headers); - - Object.keys(headers).forEach((headerName) => { - res.setHeader(headerName, headers[headerName]); - }); - - this._sendFile(res, item.output, next); - }; - } - /** - * Parses and format the list of received files into a dictionary with the names of the files as - * keys and the information about them as values. - * @param {Array} files The list of files. Each item can be a `string` or an `Object` with the - * keys `origin` for the file route, `output` for the file location relative - * to the root, and `headers` with the file custom headers for the response. - * @return {Object} - */ - _parseFiles(files) { - const formattedFiles = {}; - files.forEach((file) => { - const item = { - origin: '', - output: '', - headers: {}, - }; - - if (typeof file === 'object') { - item.origin = file.origin; - item.output = file.output; - item.headers = file.headers || {}; - } else { - item.origin = file; - item.output = file; - } - - formattedFiles[item.origin] = item; - }); - - return formattedFiles; - } -} -/** - * This controller can be used to serve files from the root directory; the idea is to avoid - * declaring the root directory as a static folder. - * The files are sent to {@link RootStaticsController} and for each file, it will declare a route - * and mount a middleware in order to serve them. - * @type {ControllerCreator} - * @param {Array} files The list of files. Each item can be a `string` or an `Object` with the - * keys `origin` for the file route, `output` for the file location relative - * to the root, and `headers` with the file custom headers in case they are - * needed on the response. - */ -const rootStaticsController = controllerCreator((files) => (app) => { - const router = app.get('router'); - const ctrl = new RootStaticsController(app.get('sendFile'), files); - return ctrl.getFileEntries() - .map((file) => router.all(`/${file}`, ctrl.serveFile(file))); -}); - -module.exports = { - RootStaticsController, - rootStaticsController, -}; diff --git a/tests/controllers/common/rootStatics.test.js b/tests/controllers/common/rootStatics.test.js deleted file mode 100644 index 542b95cf..00000000 --- a/tests/controllers/common/rootStatics.test.js +++ /dev/null @@ -1,183 +0,0 @@ -jest.unmock('/src/utils/wrappers'); -jest.unmock('/src/controllers/common/rootStatics'); - -require('jasmine-expect'); -const { - RootStaticsController, - rootStaticsController, -} = require('/src/controllers/common/rootStatics'); - -describe('controllers/common:rootStatics', () => { - it('should be instantiated and have public methods', () => { - // Given - const sendFile = 'sendFile'; - let sut = null; - // When - sut = new RootStaticsController(sendFile); - // Then - expect(sut).toBeInstanceOf(RootStaticsController); - expect(sut.getFileEntries).toBeFunction(); - expect(sut.serveFile).toBeFunction(); - }); - - it('should have a method to return all the files it will serve', () => { - // Given - const sendFile = 'sendFile'; - const files = ['index.png', 'index.html']; - let sut = null; - let result = null; - // When - sut = new RootStaticsController(sendFile, files); - result = sut.getFileEntries(); - // Then - expect(result).toEqual(files); - }); - - it('should have a middleware to serve the files sent on the constructor', () => { - // Given - const sendFile = jest.fn(); - const file = 'index.html'; - const request = 'request'; - const response = { - setHeader: jest.fn(), - }; - const next = 'next'; - let sut = null; - let middleware = null; - // When - sut = new RootStaticsController(sendFile, [file]); - middleware = sut.serveFile(file); - middleware(request, response, next); - // Then - expect(response.setHeader).toHaveBeenCalledTimes(1); - expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html'); - expect(sendFile).toHaveBeenCalledTimes(1); - expect(sendFile).toHaveBeenCalledWith(response, file, next); - }); - - it('should be able to serve a static file on a route different from where it is', () => { - // Given - const sendFile = jest.fn(); - const file = { - origin: 'favicon.png', - output: 'some/folder.png', - }; - const request = 'request'; - const response = { - setHeader: jest.fn(), - }; - const next = 'next'; - let sut = null; - let middleware = null; - // When - sut = new RootStaticsController(sendFile, [file]); - middleware = sut.serveFile(file.origin); - middleware(request, response, next); - // Then - expect(response.setHeader).toHaveBeenCalledTimes(1); - expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'image/png'); - expect(sendFile).toHaveBeenCalledTimes(1); - expect(sendFile).toHaveBeenCalledWith(response, file.output, next); - }); - - it('should be able to serve a static file with custom headers', () => { - // Given - const sendFile = jest.fn(); - const customHeaderName = 'x-custom-header'; - const customHeaderValue = 'magic!'; - const file = { - origin: 'favicon.png', - output: 'some/folder.png', - headers: { - [customHeaderName]: customHeaderValue, - }, - }; - const request = 'request'; - const response = { - setHeader: jest.fn(), - }; - const next = 'next'; - let sut = null; - let middleware = null; - const expectedHeaders = [ - [customHeaderName, customHeaderValue], - ['Content-Type', 'image/png'], - ]; - // When - sut = new RootStaticsController(sendFile, [file]); - middleware = sut.serveFile(file.origin); - middleware(request, response, next); - // Then - expect(response.setHeader).toHaveBeenCalledTimes(expectedHeaders.length); - expectedHeaders.forEach((setHeaderCall) => { - expect(response.setHeader).toHaveBeenCalledWith(...setHeaderCall); - }); - expect(sendFile).toHaveBeenCalledTimes(1); - expect(sendFile).toHaveBeenCalledWith(response, file.output, next); - }); - - it('should throw an error when trying to serve an unknown file', () => { - // Given - const sendFile = 'sendFile'; - const files = ['fileA', 'fileC']; - let sut = null; - // When - sut = new RootStaticsController(sendFile, files); - // Then - expect(() => sut.serveFile('fileB')).toThrow(/The required static file doesn't exist/i); - }); - - it('should include a controller shorthand to return its routes', () => { - // Given - const files = ['index.html', 'favicon.ico']; - const services = { - router: { - all: jest.fn((route, middleware) => [`all:${route}`, middleware.toString()]), - }, - }; - const app = { - get: jest.fn((service) => (services[service] || service)), - }; - let routes = null; - let toCompare = null; - const expectedGets = ['router', 'sendFile']; - // When - routes = rootStaticsController.connect(app); - toCompare = new RootStaticsController('sendFile'); - // Then - expect(routes).toEqual(files.map((file) => ( - [`all:/${file}`, toCompare.serveFile(file).toString()] - ))); - expect(app.get).toHaveBeenCalledTimes(expectedGets.length); - expectedGets.forEach((service) => { - expect(app.get).toHaveBeenCalledWith(service); - }); - }); - - it('should include a controller creator shorthand to configure the files', () => { - // Given - const file = 'index.html'; - const services = { - router: { - all: jest.fn((route, middleware) => [`all:${route}`, middleware.toString()]), - }, - }; - const app = { - get: jest.fn((service) => (services[service] || service)), - }; - let routes = null; - let toCompare = null; - const expectedGets = ['router', 'sendFile']; - // When - routes = rootStaticsController([file]).connect(app); - toCompare = new RootStaticsController('sendFile', [file]); - // Then - expect(routes).toEqual([ - [`all:/${file}`, toCompare.serveFile(file).toString()], - ]); - expect(app.get).toHaveBeenCalledTimes(expectedGets.length); - expectedGets.forEach((service) => { - expect(app.get).toHaveBeenCalledWith(service); - }); - }); -}); From f57935881de5e30469d832e0aa439252158caa78 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 23:29:23 -0300 Subject: [PATCH 47/78] docs(project): document the new statics controller --- README-esdoc.md | 2 +- README.md | 2 +- documents/controllers.md | 90 ++++++++++++++++++++++++++++++++++------ 3 files changed, 79 insertions(+), 15 deletions(-) diff --git a/README-esdoc.md b/README-esdoc.md index 1da0a2bd..53ee5628 100644 --- a/README-esdoc.md +++ b/README-esdoc.md @@ -369,7 +369,7 @@ Jimpex comes with a few services, middlewares and controllers that you can impor - **Configuration:** Allows you to see and switch the current configuration. It can be enabled or disabled by using a setting on the configuration. - **Health:** Shows the version and name of the configuration, just to check the app is running. -- **Root statics:** It allows your app to server static files from the root directory, without having to use the `static` middleware on that directory. +- **Statics:** It allows your app to server specific files from any directory, without having to use the `static` middleware. [Read more about the built-in controllers](manual/controllers.html) diff --git a/README.md b/README.md index 3544c4d1..ae9885c8 100644 --- a/README.md +++ b/README.md @@ -369,7 +369,7 @@ Jimpex comes with a few services, middlewares and controllers that you can impor - **Configuration:** Allows you to see and switch the current configuration. It can be enabled or disabled by using a setting on the configuration. - **Health:** Shows the version and name of the configuration, just to check the app is running. -- **Root statics:** It allows your app to server static files from the root directory, without having to use the `static` middleware on that directory. +- **Statics:** It allows your app to server specific files from any directory, without having to use the `static` middleware. [Read more about the built-in controllers](./documents/controllers.md) diff --git a/documents/controllers.md b/documents/controllers.md index 60141f0f..1965f739 100644 --- a/documents/controllers.md +++ b/documents/controllers.md @@ -76,9 +76,9 @@ That's all there is, the controller mounts only one route: - `GET /`: Shows the information. -## Root Statics +## Statics -It allows your app to server static files from the root directory, without having to use the `static` middleware on that directory. +It allows your app to server specific files from any directory, without having to use the `static` middleware. - Module: `common` - Requires: `sendFile` @@ -90,7 +90,7 @@ const { common: { sendFile }, }, controllers: { - common: { rootStaticsController }, + common: { staticsController }, }, }; @@ -100,12 +100,33 @@ class App extends Jimpex { this.register(sendFile); // Add the controller. - this.mount('/', rootStaticsController); + this.mount('/', staticsController); } } ``` -By default, it serves an `index.html` and a `favicon.ico`, but you can use it as a function to modify those values: +The controller comes with a lot of default options: + +```js +{ + // The list of files it will serve. + files: ['favicon.ico', 'index.html'], + // The HTTP methods for which it will mount routes. + methods: { + // If `all` is `true`, then all the others are ignored. + all: false, + get: true, + }, + // The "master" paths to prepend to all file routes and files. + paths: { + // The base route from where the files are going to be served. + route: '', + // The base path from where the files are located. + source: './', + }, +} +``` +All of those values can be customized by calling the controller as a function: ```js const { @@ -114,7 +135,7 @@ const { common: { sendFile }, }, controllers: { - common: { rootStaticsController }, + common: { staticsController }, }, }; @@ -124,14 +145,57 @@ class App extends Jimpex { this.register(sendFile); // Add the controller. - this.mount('/', rootStaticsController([ - 'my-file-one.html', - 'favicon.icon', - 'index.html', - 'some-other.html', - ])); + this.mount('/', staticsController({ + paths: { + route: 'public', + source: 'secret-folder', + } + files: [ + 'my-file-one.html', + 'favicon.icon', + 'index.html', + 'some-other.html', + ], + })); } } ``` -The controller mounts a `GET` route for each one of those files. +You can also specify custom information to each individual file: + +```js +this.mount('/', staticsController({ + files: [ + 'my-file-one.html', + { + route: 'favicon.ico', + source: 'icons/fav/icon.ico', + headers: { + 'X-Custom-Icon-Header': 'Something!', + }, + }, + 'index.html', + ], +})); +``` + +Finally, you can also add a custom middleware or middlewares to the routes created by the controller, you just need to send a function that returns the middlewares when called. + +```js +/** + * In this case, we'll use Jimpex's `ensureBearerToken` to protect the + * file routes. + */ +const filesProtection = (app) => [app.get('ensureBearerToken')]; + +this.mount('/', staticsController( + { + files: [ + 'index.html', + ], + }, + [filesProtection] +)); +``` + +And that's all, the middleware will be added to the route, just before serving the file. From b7130a82443baf8ba039d9ea76fb967aa59b9482 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 23:43:35 -0300 Subject: [PATCH 48/78] feat(app): add method to define extra static folders --- src/app/index.js | 30 ++++++++++++++++++---- tests/app/index.test.js | 55 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/src/app/index.js b/src/app/index.js index 33d0f6d1..4d615389 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -267,11 +267,11 @@ class Jimpex extends Jimple { } if (statics.enabled) { - const { onHome, route, folder } = statics; - const joinFrom = onHome ? 'home' : 'app'; - const staticsRoute = route.startsWith('/') ? route.substr(1) : route; - const staticsFolderPath = this.get('pathUtils').joinFrom(joinFrom, folder || staticsRoute); - this._express.use(`/${staticsRoute}`, express.static(staticsFolderPath)); + this._addStaticsFolder( + statics.route, + statics.folder, + statics.onHome + ); } if (expressOptions.bodyParser) { @@ -290,6 +290,26 @@ class Jimpex extends Jimple { this.set('router', this.factory(() => express.Router())); } + /** + * Helper method to add static folders to the app. + * @param {string} route The route for the static folder. + * @param {string} [folder=''] The path to the folder. If not defined, it will use the + * value from `route`. + * @param {Boolean} [onHome=false] If `true`, the path to the folder will be relative to where + * the app is being executed (`process.cwd()`), otherwise, it + * will be relative to where the executable file is located. + * @access protected + * @ignore + */ + _addStaticsFolder(route, folder = '', onHome = false) { + const joinFrom = onHome ? 'home' : 'app'; + const staticRoute = route.replace(/^\/+/, ''); + const staticFolder = this.get('pathUtils').joinFrom( + joinFrom, + folder || staticRoute + ); + this._express.use(`/${staticRoute}`, express.static(staticFolder)); + } /** * Based on the constructor received options, register or not the default services. * @ignore diff --git a/tests/app/index.test.js b/tests/app/index.test.js index 768d045e..ebcb0163 100644 --- a/tests/app/index.test.js +++ b/tests/app/index.test.js @@ -445,6 +445,61 @@ describe('app:Jimpex', () => { }); }); + it('should be able to add an extra statics folder', () => { + /** + * App directory: Where the executable file is located. + * Home directory: Where the app is executed from (`process.cwd`). + */ + // Given + const customStatic = 'my-static'; + class Sut extends Jimpex { + boot() { + this._addStaticsFolder(customStatic); + } + } + const pathUtils = { + joinFrom: jest.fn((from, rest) => path.join(from, rest)), + }; + JimpleMock.service('pathUtils', pathUtils); + const defaultConfig = {}; + const rootRequire = jest.fn(() => defaultConfig); + JimpleMock.service('rootRequire', rootRequire); + const appConfiguration = { + loadFromEnvironment: jest.fn(), + get: jest.fn(), + }; + JimpleMock.service('appConfiguration', appConfiguration); + const staticsRoute = '/some/statics'; + const staticsFolder = '../statics'; + let sut = null; + const expectedMiddlewares = [ + ['compression-middleware'], + [staticsRoute, path.join('home', staticsFolder)], + ['body-parser-json'], + ['body-parser-urlencoded'], + ['multer-any'], + [`/${customStatic}`, path.join('app', customStatic)], + ]; + // When + sut = new Sut(true, { + statics: { + onHome: true, + route: staticsRoute, + folder: staticsFolder, + }, + }); + // Then + expect(sut.options.statics.folder).toBe(staticsFolder); + expect(expressMock.static).toHaveBeenCalledTimes(2); + expect(pathUtils.joinFrom).toHaveBeenCalledTimes(2); + expect(pathUtils.joinFrom).toHaveBeenCalledWith('home', staticsFolder); + expect(pathUtils.joinFrom).toHaveBeenCalledWith('app', customStatic); + expect(expressMock.mocks.use).toHaveBeenCalledTimes(expectedMiddlewares.length); + expectedMiddlewares.forEach((useCall) => { + expect(expressMock.mocks.use).toHaveBeenCalledWith(...useCall); + }); + }); + it('shouldn\'t add the default services if their flags are `false`', () => { // Given class Sut extends Jimpex { From 3317fc84686f0d9e46d584f97951c08785528550 Mon Sep 17 00:00:00 2001 From: homer0 Date: Fri, 21 Jun 2019 23:48:56 -0300 Subject: [PATCH 49/78] feat(app): add support to 'use' Express middlewares without having to wrap them --- src/app/index.js | 30 +++++++++++++---------- tests/app/index.test.js | 53 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/app/index.js b/src/app/index.js index 4d615389..d54a78dd 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -139,7 +139,7 @@ class Jimpex extends Jimple { throw new Error('This method must to be overwritten'); } /** - * Mount a controller on a route point. + * Mounts a controller on a route point. * @param {string} point The route for the controller. * @param {Controller|ControllerCreator} controller The route controller. */ @@ -151,19 +151,23 @@ class Jimpex extends Jimple { ); } /** - * Add a middleware. - * @param {Middleware|MiddlewareCreator} middleware The middleware to use. + * Adds a middleware. + * @param {Middleware|MiddlewareCreator|ExpressMiddleware} middleware The middleware to use. */ use(middleware) { this._mountQueue.push((server) => { - const middlewareHandler = middleware.connect(this); - if (middlewareHandler) { - server.use(middlewareHandler); + if (typeof middleware.connect === 'function') { + const middlewareHandler = middleware.connect(this); + if (middlewareHandler) { + server.use(middlewareHandler); + } + } else { + server.use(middleware); } }); } /** - * Start the app server. + * Starts the app server. * @param {function(config:AppConfiguration)} [fn] A callback function to be called when the * server starts. * @return {Object} The server instance @@ -201,14 +205,14 @@ class Jimpex extends Jimple { return this.start(fn, port); } /** - * Emit an app event with a reference to this class instance. + * Emits an app event with a reference to this class instance. * @param {string} name The name of the event. */ emitEvent(name) { this.get('events').emit(name, this); } /** - * Disable the server TLS validation. + * Disables the server TLS validation. */ disableTLSValidation() { // eslint-disable-next-line no-process-env @@ -227,7 +231,7 @@ class Jimpex extends Jimple { } } /** - * Register the _'core services'_. + * Registers the _'core services'_. * @ignore * @access protected */ @@ -244,7 +248,7 @@ class Jimpex extends Jimple { this.register(rootRequire); } /** - * Create and configure the Express instance. + * Creates and configure the Express instance. * @ignore * @access protected */ @@ -333,7 +337,7 @@ class Jimpex extends Jimple { this.set('events', () => new EventsHub()); } /** - * Create the configuration service. + * Creates the configuration service. * @ignore * @access protected */ @@ -383,7 +387,7 @@ class Jimpex extends Jimple { } } /** - * Process and mount all the resources on the `mountQueue`. + * Processes and mount all the resources on the `mountQueue`. * @ignore * @access protected */ diff --git a/tests/app/index.test.js b/tests/app/index.test.js index ebcb0163..82112821 100644 --- a/tests/app/index.test.js +++ b/tests/app/index.test.js @@ -1076,6 +1076,59 @@ describe('app:Jimpex', () => { }); }); + it('should mount an Express middleware', () => { + // Given + class Sut extends Jimpex { + boot() {} + } + const pathUtils = { + joinFrom: jest.fn((from, rest) => path.join(from, rest)), + }; + JimpleMock.service('pathUtils', pathUtils); + const defaultConfig = {}; + const rootRequire = jest.fn(() => defaultConfig); + JimpleMock.service('rootRequire', rootRequire); + const configuration = { + port: 2509, + }; + const appConfiguration = { + loadFromEnvironment: jest.fn(), + get: jest.fn((prop) => configuration[prop]), + }; + JimpleMock.service('appConfiguration', appConfiguration); + const events = { + emit: jest.fn(), + }; + JimpleMock.service('events', events); + const appLogger = { + success: jest.fn(), + }; + JimpleMock.service('appLogger', appLogger); + const middleware = 'express-middleware'; + let sut = null; + const expectedStaticsFolder = 'app/statics'; + const expectedMiddlewares = [ + ['compression-middleware'], + ['/statics', expectedStaticsFolder], + ['body-parser-json'], + ['body-parser-urlencoded'], + ['multer-any'], + ]; + const expectedUseCalls = [ + ...expectedMiddlewares, + [middleware], + ]; + // When + sut = new Sut(); + sut.use(middleware); + sut.start(); + // Then + expect(expressMock.mocks.use).toHaveBeenCalledTimes(expectedUseCalls.length); + expectedUseCalls.forEach((useCall) => { + expect(expressMock.mocks.use).toHaveBeenCalledWith(...useCall); + }); + }); + it('shouldn\'t mount a middleware if its `connect` method returned a falsy value', () => { // Given class Sut extends Jimpex { From eb48c8b0f9f8dc85b42bc58e494ed38c48a6bb64 Mon Sep 17 00:00:00 2001 From: homer0 Date: Sat, 22 Jun 2019 00:04:33 -0300 Subject: [PATCH 50/78] feat(app): add a method to get a service and return null if not available --- src/app/index.js | 20 +++++++++++++++++++ tests/app/index.test.js | 40 ++++++++++++++++++++++++++++++++++++++ tests/mocks/jimple.mock.js | 8 +++++++- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/app/index.js b/src/app/index.js index d54a78dd..f36e9afd 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -138,6 +138,26 @@ class Jimpex extends Jimple { boot() { throw new Error('This method must to be overwritten'); } + /** + * Tries to access a service on the container, but if is not present, it won't throw an error, it + * will just return `null`. + * @param {string} name The name of the service. + * @return {*} + */ + try(name) { + let result; + try { + result = this.get(name); + } catch (ignore) { + /** + * The only reason we are ignoring the error is because is expected to throw an error if + * the service is not registered. + */ + result = null; + } + + return result; + } /** * Mounts a controller on a route point. * @param {string} point The route for the controller. diff --git a/tests/app/index.test.js b/tests/app/index.test.js index 82112821..af0880c2 100644 --- a/tests/app/index.test.js +++ b/tests/app/index.test.js @@ -963,6 +963,46 @@ describe('app:Jimpex', () => { expect(expressMock.mocks.closeInstance).toHaveBeenCalledTimes(0); }); + it('should mount a controller', () => { + // Given + class Sut extends Jimpex { + boot() {} + } + const pathUtils = { + joinFrom: jest.fn((from, rest) => path.join(from, rest)), + }; + JimpleMock.service('pathUtils', pathUtils); + const defaultConfig = {}; + const rootRequire = jest.fn(() => defaultConfig); + JimpleMock.service('rootRequire', rootRequire); + const configuration = { + port: 2509, + }; + const appConfiguration = { + loadFromEnvironment: jest.fn(), + get: jest.fn((prop) => configuration[prop]), + }; + JimpleMock.service('appConfiguration', appConfiguration); + const events = { + emit: jest.fn(), + }; + JimpleMock.service('events', events); + const appLogger = { + success: jest.fn(), + }; + JimpleMock.service('appLogger', appLogger); + let sut = null; + let resultAvailable = null; + let resultUnavailable = null; + // When + sut = new Sut(); + resultAvailable = sut.try('events'); + resultUnavailable = sut.try('randomService'); + // Then + expect(resultAvailable).toBe(events); + expect(resultUnavailable).toBeNull(); + }); + it('should mount a controller', () => { // Given class Sut extends Jimpex { diff --git a/tests/mocks/jimple.mock.js b/tests/mocks/jimple.mock.js index f6480d3b..d39a65c9 100644 --- a/tests/mocks/jimple.mock.js +++ b/tests/mocks/jimple.mock.js @@ -2,7 +2,13 @@ const services = {}; const mocks = { set: jest.fn(), - get: jest.fn((name) => services[name]), + get: jest.fn((name) => { + if (!services[name]) { + throw new Error(); + } + + return services[name]; + }), register: jest.fn(), factory: jest.fn((fn) => fn()), }; From f21031f0e8277e07829e871b28aff3b82c5192e2 Mon Sep 17 00:00:00 2001 From: homer0 Date: Sat, 22 Jun 2019 00:12:48 -0300 Subject: [PATCH 51/78] refactor(middlewares/html): use app.try instead of try catch --- src/middlewares/html/fastHTML.js | 21 +++++---------- src/middlewares/html/showHTML.js | 19 ++++---------- tests/middlewares/html/fastHTML.test.js | 34 +++++++++++++------------ tests/middlewares/html/showHTML.test.js | 32 +++++++++++++---------- 4 files changed, 47 insertions(+), 59 deletions(-) diff --git a/src/middlewares/html/fastHTML.js b/src/middlewares/html/fastHTML.js index 8e3d2f8b..bafe988e 100644 --- a/src/middlewares/html/fastHTML.js +++ b/src/middlewares/html/fastHTML.js @@ -161,21 +161,12 @@ const fastHTML = middlewareCreator(( file, ignoredRoutes, htmlGeneratorServiceName = 'htmlGenerator' -) => (app) => { - let htmlGenerator; - try { - htmlGenerator = app.get(htmlGeneratorServiceName); - } catch (ignore) { - htmlGenerator = null; - } - - return new FastHTML( - app.get('sendFile'), - file, - ignoredRoutes, - htmlGenerator - ).middleware(); -}); +) => (app) => new FastHTML( + app.get('sendFile'), + file, + ignoredRoutes, + app.try(htmlGeneratorServiceName) +).middleware()); module.exports = { FastHTML, diff --git a/src/middlewares/html/showHTML.js b/src/middlewares/html/showHTML.js index 25b87444..97d35c80 100644 --- a/src/middlewares/html/showHTML.js +++ b/src/middlewares/html/showHTML.js @@ -125,20 +125,11 @@ class ShowHTML { const showHTML = middlewareCreator(( file, htmlGeneratorServiceName = 'htmlGenerator' -) => (app) => { - let htmlGenerator; - try { - htmlGenerator = app.get(htmlGeneratorServiceName); - } catch (ignore) { - htmlGenerator = null; - } - - return new ShowHTML( - app.get('sendFile'), - file, - htmlGenerator - ).middleware(); -}); +) => (app) => new ShowHTML( + app.get('sendFile'), + file, + app.try(htmlGeneratorServiceName) +).middleware()); module.exports = { ShowHTML, diff --git a/tests/middlewares/html/fastHTML.test.js b/tests/middlewares/html/fastHTML.test.js index c8cf717c..fff325b2 100644 --- a/tests/middlewares/html/fastHTML.test.js +++ b/tests/middlewares/html/fastHTML.test.js @@ -158,20 +158,17 @@ describe('middlewares/html:fastHTML', () => { // Given const services = {}; const app = { - get: jest.fn((service) => { - if (service === 'htmlGenerator') { - throw Error(); - } - - return services[service] || service; - }), + get: jest.fn((service) => services[service] || service), + try: jest.fn(() => null), }; let middleware = null; let toCompare = null; const expectedGets = [ - 'htmlGenerator', 'sendFile', ]; + const expectedTryAttempts = [ + 'htmlGenerator', + ]; // When middleware = fastHTML.connect(app); toCompare = new FastHTML(); @@ -181,26 +178,27 @@ describe('middlewares/html:fastHTML', () => { expectedGets.forEach((service) => { expect(app.get).toHaveBeenCalledWith(service); }); + expect(app.try).toHaveBeenCalledTimes(expectedTryAttempts.length); + expectedTryAttempts.forEach((service) => { + expect(app.try).toHaveBeenCalledWith(service); + }); }); it('should include a middleware creator shorthand to configure its options', () => { // Given const services = {}; const app = { - get: jest.fn((service) => { - if (service === 'htmlGenerator') { - throw Error(); - } - - return services[service] || service; - }), + get: jest.fn((service) => services[service] || service), + try: jest.fn(() => null), }; let middleware = null; let toCompare = null; const expectedGets = [ - 'htmlGenerator', 'sendFile', ]; + const expectedTryAttempts = [ + 'htmlGenerator', + ]; // When middleware = fastHTML().connect(app); toCompare = new FastHTML(); @@ -210,5 +208,9 @@ describe('middlewares/html:fastHTML', () => { expectedGets.forEach((service) => { expect(app.get).toHaveBeenCalledWith(service); }); + expect(app.try).toHaveBeenCalledTimes(expectedTryAttempts.length); + expectedTryAttempts.forEach((service) => { + expect(app.try).toHaveBeenCalledWith(service); + }); }); }); diff --git a/tests/middlewares/html/showHTML.test.js b/tests/middlewares/html/showHTML.test.js index e97437de..875b033c 100644 --- a/tests/middlewares/html/showHTML.test.js +++ b/tests/middlewares/html/showHTML.test.js @@ -126,20 +126,17 @@ describe('middlewares/html:showHTML', () => { // Given const services = {}; const app = { - get: jest.fn((service) => { - if (service === 'htmlGenerator') { - throw Error(); - } - - return services[service] || service; - }), + get: jest.fn((service) => services[service] || service), + try: jest.fn(() => null), }; let middleware = null; let toCompare = null; const expectedGets = [ - 'htmlGenerator', 'sendFile', ]; + const expectedTryAttempts = [ + 'htmlGenerator', + ]; // When middleware = showHTML.connect(app); toCompare = new ShowHTML(); @@ -149,6 +146,10 @@ describe('middlewares/html:showHTML', () => { expectedGets.forEach((service) => { expect(app.get).toHaveBeenCalledWith(service); }); + expect(app.try).toHaveBeenCalledTimes(expectedTryAttempts.length); + expectedTryAttempts.forEach((service) => { + expect(app.try).toHaveBeenCalledWith(service); + }); }); it('should include a middleware creator shorthand to configure its options', () => { @@ -158,18 +159,17 @@ describe('middlewares/html:showHTML', () => { getFile: jest.fn(() => ''), }; const app = { - get: jest.fn((service) => ( - service === htmlGeneratorServiceName ? - htmlGeneratorService : - service - )), + get: jest.fn((service) => service), + try: jest.fn(() => htmlGeneratorService), }; let middleware = null; let toCompare = null; const expectedGets = [ - htmlGeneratorServiceName, 'sendFile', ]; + const expectedTryAttempts = [ + htmlGeneratorServiceName, + ]; // When middleware = showHTML('some-file', htmlGeneratorServiceName).connect(app); toCompare = new ShowHTML(); @@ -179,6 +179,10 @@ describe('middlewares/html:showHTML', () => { expectedGets.forEach((service) => { expect(app.get).toHaveBeenCalledWith(service); }); + expect(app.try).toHaveBeenCalledTimes(expectedTryAttempts.length); + expectedTryAttempts.forEach((service) => { + expect(app.try).toHaveBeenCalledWith(service); + }); expect(htmlGeneratorService.getFile).toHaveBeenCalledTimes(1); }); }); From b9993e559772d918dbf4f50a3612a3816aa50eac Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 00:24:24 -0300 Subject: [PATCH 52/78] feat(app): allow controllers to return a router instance --- src/app/index.js | 20 +++++++++++---- src/utils/wrappers.js | 3 ++- tests/app/index.test.js | 57 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/app/index.js b/src/app/index.js index f36e9afd..9716d4ff 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -164,11 +164,19 @@ class Jimpex extends Jimple { * @param {Controller|ControllerCreator} controller The route controller. */ mount(point, controller) { - this._mountQueue.push( - (server) => controller.connect(this, point).forEach( - (route) => server.use(point, route) - ) - ); + this._mountQueue.push((server) => { + let result; + const routes = controller.connect(this, point); + if (Array.isArray(routes)) { + // If the returned value is a list of routes, mount each single route. + result = routes.forEach((route) => server.use(point, route)); + } else { + // But if the returned value is not a list, it may be a router, so mount it directly. + result = server.use(point, routes); + } + + return result; + }); } /** * Adds a middleware. @@ -177,11 +185,13 @@ class Jimpex extends Jimple { use(middleware) { this._mountQueue.push((server) => { if (typeof middleware.connect === 'function') { + // If the middleware is from Jimpex, connect it and then use it. const middlewareHandler = middleware.connect(this); if (middlewareHandler) { server.use(middlewareHandler); } } else { + // But if the middleware is a regular middleware, just use it directly. server.use(middleware); } }); diff --git a/src/utils/wrappers.js b/src/utils/wrappers.js index 7d49c933..b2a1a257 100644 --- a/src/utils/wrappers.js +++ b/src/utils/wrappers.js @@ -53,7 +53,8 @@ * @description The function called by the app container in order to mount a routes controller. * @param {Jimpex} app The instance of the app container. * @param {string} point The point where the controller will be mounted. - * @return {Array} The list of routes the controller will manage. + * @return {Array|ExpressRouter} The list of routes the controller will manage, or a router + * instance. */ /** diff --git a/tests/app/index.test.js b/tests/app/index.test.js index af0880c2..8b5a1b6d 100644 --- a/tests/app/index.test.js +++ b/tests/app/index.test.js @@ -1060,6 +1060,63 @@ describe('app:Jimpex', () => { }); }); + it('should mount a controller router', () => { + // Given + class Sut extends Jimpex { + boot() {} + } + const pathUtils = { + joinFrom: jest.fn((from, rest) => path.join(from, rest)), + }; + JimpleMock.service('pathUtils', pathUtils); + const defaultConfig = {}; + const rootRequire = jest.fn(() => defaultConfig); + JimpleMock.service('rootRequire', rootRequire); + const configuration = { + port: 2509, + }; + const appConfiguration = { + loadFromEnvironment: jest.fn(), + get: jest.fn((prop) => configuration[prop]), + }; + JimpleMock.service('appConfiguration', appConfiguration); + const events = { + emit: jest.fn(), + }; + JimpleMock.service('events', events); + const appLogger = { + success: jest.fn(), + }; + JimpleMock.service('appLogger', appLogger); + const router = 'my-router'; + const point = '/api'; + const controller = { + connect: jest.fn(() => router), + }; + let sut = null; + const expectedStaticsFolder = 'app/statics'; + const expectedMiddlewares = [ + ['compression-middleware'], + ['/statics', expectedStaticsFolder], + ['body-parser-json'], + ['body-parser-urlencoded'], + ['multer-any'], + ]; + const expectedUseCalls = [ + ...expectedMiddlewares, + ...[[point, router]], + ]; + // When + sut = new Sut(); + sut.mount(point, controller); + sut.start(); + // Then + expect(expressMock.mocks.use).toHaveBeenCalledTimes(expectedUseCalls.length); + expectedUseCalls.forEach((useCall) => { + expect(expressMock.mocks.use).toHaveBeenCalledWith(...useCall); + }); + }); + it('should mount a middleware', () => { // Given class Sut extends Jimpex { From 0938cc1d9bc9233851f4b1f17db75e3d8c9ed092 Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 00:37:06 -0300 Subject: [PATCH 53/78] refactor(app): make emitEvent protected --- src/app/index.js | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/app/index.js b/src/app/index.js index 9716d4ff..817c3b3e 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -205,14 +205,14 @@ class Jimpex extends Jimple { start(fn = () => {}) { const config = this.get('appConfiguration'); const port = config.get('port'); - this.emitEvent('before-start'); + this._emitEvent('before-start'); this._instance = this._express.listen(port, () => { - this.emitEvent('start'); + this._emitEvent('start'); this._mountResources(); this.get('appLogger').success(`Starting on port ${port}`); - this.emitEvent('after-start'); + this._emitEvent('after-start'); const result = fn(config); - this.emitEvent('after-start-callback'); + this._emitEvent('after-start-callback'); return result; }); @@ -234,13 +234,6 @@ class Jimpex extends Jimple { config.set('port', port); return this.start(fn, port); } - /** - * Emits an app event with a reference to this class instance. - * @param {string} name The name of the event. - */ - emitEvent(name) { - this.get('events').emit(name, this); - } /** * Disables the server TLS validation. */ @@ -254,10 +247,10 @@ class Jimpex extends Jimple { */ stop() { if (this._instance) { - this.emitEvent('before-stop'); + this._emitEvent('before-stop'); this._instance.close(); this._instance = null; - this.emitEvent('after-stop'); + this._emitEvent('after-stop'); } } /** @@ -425,6 +418,14 @@ class Jimpex extends Jimple { this._mountQueue.forEach((mountFn) => mountFn(this._express)); this._mountQueue.length = 0; } + /** + * Emits an app event with a reference to this class instance. + * @param {string} name The name of the event. + * @access protected + */ + _emitEvent(name) { + this.get('events').emit(name, this); + } } module.exports = Jimpex; From bf77f9775821646c52f608df2bba6e7843e6af10 Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 00:55:00 -0300 Subject: [PATCH 54/78] feat(app): add reducer events for controllers and middlewares --- src/app/index.js | 31 ++++++++++++++++++++++++++++--- tests/app/index.test.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/app/index.js b/src/app/index.js index 817c3b3e..7fc6894c 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -166,7 +166,12 @@ class Jimpex extends Jimple { mount(point, controller) { this._mountQueue.push((server) => { let result; - const routes = controller.connect(this, point); + const routes = this._reduceWithEvent( + 'controller-mount', + controller.connect(this, point), + point, + controller + ); if (Array.isArray(routes)) { // If the returned value is a list of routes, mount each single route. result = routes.forEach((route) => server.use(point, route)); @@ -188,11 +193,19 @@ class Jimpex extends Jimple { // If the middleware is from Jimpex, connect it and then use it. const middlewareHandler = middleware.connect(this); if (middlewareHandler) { - server.use(middlewareHandler); + server.use(this._reduceWithEvent( + 'middleware-use', + middlewareHandler, + middleware + )); } } else { // But if the middleware is a regular middleware, just use it directly. - server.use(middleware); + server.use(this._reduceWithEvent( + 'middleware-use', + middleware, + null + )); } }); } @@ -426,6 +439,18 @@ class Jimpex extends Jimple { _emitEvent(name) { this.get('events').emit(name, this); } + /** + * Sends a target object to a list of reducer events so they can modify or replace it. This + * method also sends a reference to this class instance as the last parameter of the event. + * @param {string} name The name of the event. + * @param {*} target The targe object to reduce. + * @param {...*} args Extra parameters for the listeners. + * @return {*} An object of the same type as the `target`. + * @access protected + */ + _reduceWithEvent(name, target, ...args) { + return this.get('events').reduce(name, target, ...[...args, this]); + } } module.exports = Jimpex; diff --git a/tests/app/index.test.js b/tests/app/index.test.js index 8b5a1b6d..254251a9 100644 --- a/tests/app/index.test.js +++ b/tests/app/index.test.js @@ -1025,6 +1025,7 @@ describe('app:Jimpex', () => { JimpleMock.service('appConfiguration', appConfiguration); const events = { emit: jest.fn(), + reduce: jest.fn((eventName, router) => router), }; JimpleMock.service('events', events); const appLogger = { @@ -1058,6 +1059,14 @@ describe('app:Jimpex', () => { expectedUseCalls.forEach((useCall) => { expect(expressMock.mocks.use).toHaveBeenCalledWith(...useCall); }); + expect(events.reduce).toHaveBeenCalledTimes(1); + expect(events.reduce).toHaveBeenCalledWith( + 'controller-mount', + routes, + point, + controller, + sut + ); }); it('should mount a controller router', () => { @@ -1082,6 +1091,7 @@ describe('app:Jimpex', () => { JimpleMock.service('appConfiguration', appConfiguration); const events = { emit: jest.fn(), + reduce: jest.fn((eventName, router) => router), }; JimpleMock.service('events', events); const appLogger = { @@ -1115,6 +1125,14 @@ describe('app:Jimpex', () => { expectedUseCalls.forEach((useCall) => { expect(expressMock.mocks.use).toHaveBeenCalledWith(...useCall); }); + expect(events.reduce).toHaveBeenCalledTimes(1); + expect(events.reduce).toHaveBeenCalledWith( + 'controller-mount', + router, + point, + controller, + sut + ); }); it('should mount a middleware', () => { @@ -1139,6 +1157,7 @@ describe('app:Jimpex', () => { JimpleMock.service('appConfiguration', appConfiguration); const events = { emit: jest.fn(), + reduce: jest.fn((eventName, middleware) => middleware), }; JimpleMock.service('events', events); const appLogger = { @@ -1171,6 +1190,13 @@ describe('app:Jimpex', () => { expectedUseCalls.forEach((useCall) => { expect(expressMock.mocks.use).toHaveBeenCalledWith(...useCall); }); + expect(events.reduce).toHaveBeenCalledTimes(1); + expect(events.reduce).toHaveBeenCalledWith( + 'middleware-use', + middlewareFn, + middleware, + sut + ); }); it('should mount an Express middleware', () => { @@ -1195,6 +1221,7 @@ describe('app:Jimpex', () => { JimpleMock.service('appConfiguration', appConfiguration); const events = { emit: jest.fn(), + reduce: jest.fn((eventName, middleware) => middleware), }; JimpleMock.service('events', events); const appLogger = { @@ -1224,6 +1251,13 @@ describe('app:Jimpex', () => { expectedUseCalls.forEach((useCall) => { expect(expressMock.mocks.use).toHaveBeenCalledWith(...useCall); }); + expect(events.reduce).toHaveBeenCalledTimes(1); + expect(events.reduce).toHaveBeenCalledWith( + 'middleware-use', + middleware, + null, + sut + ); }); it('shouldn\'t mount a middleware if its `connect` method returned a falsy value', () => { From 0036abe780fd2bffc548681179bd663c1e7fcdd9 Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 01:00:59 -0300 Subject: [PATCH 55/78] refactor(project): rename controllers 'point' to 'route' --- src/app/index.js | 14 +++++++------- src/middlewares/utils/versionValidator.js | 10 +++++----- src/utils/wrappers.js | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/app/index.js b/src/app/index.js index 7fc6894c..8adccedc 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -159,25 +159,25 @@ class Jimpex extends Jimple { return result; } /** - * Mounts a controller on a route point. - * @param {string} point The route for the controller. + * Mounts a controller on a specific route. + * @param {string} route The route for the controller. * @param {Controller|ControllerCreator} controller The route controller. */ - mount(point, controller) { + mount(route, controller) { this._mountQueue.push((server) => { let result; const routes = this._reduceWithEvent( 'controller-mount', - controller.connect(this, point), - point, + controller.connect(this, route), + route, controller ); if (Array.isArray(routes)) { // If the returned value is a list of routes, mount each single route. - result = routes.forEach((route) => server.use(point, route)); + result = routes.forEach((routeRouter) => server.use(route, routeRouter)); } else { // But if the returned value is not a list, it may be a router, so mount it directly. - result = server.use(point, routes); + result = server.use(route, routes); } return result; diff --git a/src/middlewares/utils/versionValidator.js b/src/middlewares/utils/versionValidator.js index 0f7f4ea2..17c2dde4 100644 --- a/src/middlewares/utils/versionValidator.js +++ b/src/middlewares/utils/versionValidator.js @@ -195,16 +195,16 @@ class VersionValidator { * A middleware that will validate a `version` request parameter against the app version and * generate an error if they don't match. * This is a "middleware/controller" is because the wrappers for both are the same, the - * difference is that, for controllers, Jimpex sends a second parameter with the point where they + * difference is that, for controllers, Jimpex sends a second parameter with the route where they * are mounted. - * By validating the point parameter, the function can know whether the implementation is going + * By validating the route parameter, the function can know whether the implementation is going * to use the middleware by itself or as a route middleware. * If used as middleware, it will just return the result of {@link VersionValidator#middleware}; - * but if used as controller, it will mount it on `[point]/:version/*`. + * but if used as controller, it will mount it on `[route]/:version/*`. * @type {MiddlewareCreator} * @param {VersionValidatorOptions} [options] Custom options to modify the middleware behavior. */ -const versionValidator = middlewareCreator((options) => (app, point) => { +const versionValidator = middlewareCreator((options) => (app, route) => { // Get the middleware function. const middlewareValidator = (new VersionValidator( app.get('appConfiguration').get('version'), @@ -214,7 +214,7 @@ const versionValidator = middlewareCreator((options) => (app, point) => { )).middleware(); // Set the variable to be returned. let result; - if (point) { + if (route) { // If the implementation will use it as a router, get the `router` service and mount it. const router = app.get('router'); // Set the array of "routes" as the return value. diff --git a/src/utils/wrappers.js b/src/utils/wrappers.js index b2a1a257..4361a2ab 100644 --- a/src/utils/wrappers.js +++ b/src/utils/wrappers.js @@ -52,7 +52,7 @@ * @typedef {function} ControllerMountCallback * @description The function called by the app container in order to mount a routes controller. * @param {Jimpex} app The instance of the app container. - * @param {string} point The point where the controller will be mounted. + * @param {string} route The route where the controller will be mounted. * @return {Array|ExpressRouter} The list of routes the controller will manage, or a router * instance. */ From 656a578386d1170abfdcda36438e67f985a9989a Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 01:14:08 -0300 Subject: [PATCH 56/78] feat(app): add a way to keep track of the controller routes --- src/app/index.js | 30 ++++++++++++++++++++++++++-- tests/app/index.test.js | 44 +++++++++++++++++++++++++++++++++-------- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/app/index.js b/src/app/index.js index 8adccedc..a97f8fbc 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -47,6 +47,8 @@ class Jimpex extends Jimple { /** * The app options. * @type {JimpexOptions} + * @access protected + * @ignore */ this._options = ObjectUtils.merge({ version: '0.0.0', @@ -83,11 +85,15 @@ class Jimpex extends Jimple { /** * The Express app Jimpex uses under the hood. * @type {Express} + * @access protected + * @ignore */ this._express = express(); /** * When the app starts, this will be running instance. * @type {?Object} + * @access protected + * @ignore */ this._instance = null; /** @@ -97,8 +103,18 @@ class Jimpex extends Jimple { * services on Jimple use lazy loading, and adding middlewares and controllers as they come * could cause errors if they depend on services that are not yet registered. * @type {Array} + * @access protected + * @ignore */ this._mountQueue = []; + /** + * A list of all the top routes controlled by the app. Every time a controller is mounted, + * its route will be added here. + * @type {Array} + * @access protected + * @ignore + */ + this._controlledRoutes = []; this._setupCoreServices(); this._setupExpress(); @@ -130,6 +146,13 @@ class Jimpex extends Jimple { get instance() { return this._instance; } + /** + * A list of all the top routes controlled by the app. + * @type {Array} + */ + get routes() { + return ObjectUtils.copy(this._controlledRoutes); + } /** * This is where the app would register all its specific services, middlewares and controllers. * @throws {Error} if not overwritten. @@ -180,6 +203,8 @@ class Jimpex extends Jimple { result = server.use(route, routes); } + this._controlledRoutes.push(route); + this._emitEvent('route-added', route); return result; }); } @@ -434,10 +459,11 @@ class Jimpex extends Jimple { /** * Emits an app event with a reference to this class instance. * @param {string} name The name of the event. + * @param {...*} args Extra parameters for the listeners. * @access protected */ - _emitEvent(name) { - this.get('events').emit(name, this); + _emitEvent(name, ...args) { + this.get('events').emit(name, ...[...args, this]); } /** * Sends a target object to a list of reducer events so they can modify or replace it. This diff --git a/tests/app/index.test.js b/tests/app/index.test.js index 254251a9..2daff420 100644 --- a/tests/app/index.test.js +++ b/tests/app/index.test.js @@ -1033,11 +1033,12 @@ describe('app:Jimpex', () => { }; JimpleMock.service('appLogger', appLogger); const routes = ['route-a', 'route-b']; - const point = '/api'; + const route = '/api'; const controller = { connect: jest.fn(() => routes), }; let sut = null; + let routesList = null; const expectedStaticsFolder = 'app/statics'; const expectedMiddlewares = [ ['compression-middleware'], @@ -1048,25 +1049,38 @@ describe('app:Jimpex', () => { ]; const expectedUseCalls = [ ...expectedMiddlewares, - ...routes.map((route) => [point, route]), + ...routes.map((routeRouter) => [route, routeRouter]), + ]; + const expectedEvents = [ + ['before-start'], + ['start'], + ['route-added', route], + ['after-start'], + ['after-start-callback'], ]; // When sut = new Sut(); - sut.mount(point, controller); + sut.mount(route, controller); sut.start(); + routesList = sut.routes; // Then expect(expressMock.mocks.use).toHaveBeenCalledTimes(expectedUseCalls.length); expectedUseCalls.forEach((useCall) => { expect(expressMock.mocks.use).toHaveBeenCalledWith(...useCall); }); + expect(events.emit).toHaveBeenCalledTimes(expectedEvents.length); + expectedEvents.forEach((eventInformation) => { + expect(events.emit).toHaveBeenCalledWith(...[...eventInformation, sut]); + }); expect(events.reduce).toHaveBeenCalledTimes(1); expect(events.reduce).toHaveBeenCalledWith( 'controller-mount', routes, - point, + route, controller, sut ); + expect(routesList).toEqual([route]); }); it('should mount a controller router', () => { @@ -1099,11 +1113,12 @@ describe('app:Jimpex', () => { }; JimpleMock.service('appLogger', appLogger); const router = 'my-router'; - const point = '/api'; + const route = '/api'; const controller = { connect: jest.fn(() => router), }; let sut = null; + let routesList = null; const expectedStaticsFolder = 'app/statics'; const expectedMiddlewares = [ ['compression-middleware'], @@ -1114,25 +1129,38 @@ describe('app:Jimpex', () => { ]; const expectedUseCalls = [ ...expectedMiddlewares, - ...[[point, router]], + ...[[route, router]], + ]; + const expectedEvents = [ + ['before-start'], + ['start'], + ['route-added', route], + ['after-start'], + ['after-start-callback'], ]; // When sut = new Sut(); - sut.mount(point, controller); + sut.mount(route, controller); sut.start(); + routesList = sut.routes; // Then expect(expressMock.mocks.use).toHaveBeenCalledTimes(expectedUseCalls.length); expectedUseCalls.forEach((useCall) => { expect(expressMock.mocks.use).toHaveBeenCalledWith(...useCall); }); + expect(events.emit).toHaveBeenCalledTimes(expectedEvents.length); + expectedEvents.forEach((eventInformation) => { + expect(events.emit).toHaveBeenCalledWith(...[...eventInformation, sut]); + }); expect(events.reduce).toHaveBeenCalledTimes(1); expect(events.reduce).toHaveBeenCalledWith( 'controller-mount', router, - point, + route, controller, sut ); + expect(routesList).toEqual([route]); }); it('should mount a middleware', () => { From 2a91ce2e2287542345ca7c2c0b72eddcbeff86b5 Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 01:17:56 -0300 Subject: [PATCH 57/78] refactor(app): rename the controller and middleware events --- src/app/index.js | 6 +++--- tests/app/index.test.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/index.js b/src/app/index.js index a97f8fbc..87b4af81 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -190,7 +190,7 @@ class Jimpex extends Jimple { this._mountQueue.push((server) => { let result; const routes = this._reduceWithEvent( - 'controller-mount', + 'controller-will-be-mounted', controller.connect(this, route), route, controller @@ -219,7 +219,7 @@ class Jimpex extends Jimple { const middlewareHandler = middleware.connect(this); if (middlewareHandler) { server.use(this._reduceWithEvent( - 'middleware-use', + 'middleware-will-be-used', middlewareHandler, middleware )); @@ -227,7 +227,7 @@ class Jimpex extends Jimple { } else { // But if the middleware is a regular middleware, just use it directly. server.use(this._reduceWithEvent( - 'middleware-use', + 'middleware-will-be-used', middleware, null )); diff --git a/tests/app/index.test.js b/tests/app/index.test.js index 2daff420..3a823114 100644 --- a/tests/app/index.test.js +++ b/tests/app/index.test.js @@ -1074,7 +1074,7 @@ describe('app:Jimpex', () => { }); expect(events.reduce).toHaveBeenCalledTimes(1); expect(events.reduce).toHaveBeenCalledWith( - 'controller-mount', + 'controller-will-be-mounted', routes, route, controller, @@ -1154,7 +1154,7 @@ describe('app:Jimpex', () => { }); expect(events.reduce).toHaveBeenCalledTimes(1); expect(events.reduce).toHaveBeenCalledWith( - 'controller-mount', + 'controller-will-be-mounted', router, route, controller, @@ -1220,7 +1220,7 @@ describe('app:Jimpex', () => { }); expect(events.reduce).toHaveBeenCalledTimes(1); expect(events.reduce).toHaveBeenCalledWith( - 'middleware-use', + 'middleware-will-be-used', middlewareFn, middleware, sut @@ -1281,7 +1281,7 @@ describe('app:Jimpex', () => { }); expect(events.reduce).toHaveBeenCalledTimes(1); expect(events.reduce).toHaveBeenCalledWith( - 'middleware-use', + 'middleware-will-be-used', middleware, null, sut From 5340de344c9dd18e2442468a1634284301958935 Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 01:46:49 -0300 Subject: [PATCH 58/78] refactor(project): save all the event names on a constant --- src/app/index.js | 27 ++++++++++++------------ src/constants/eventNames.js | 37 +++++++++++++++++++++++++++++++++ src/constants/index.js | 12 +++++++++++ src/index.js | 2 ++ tests/app/index.test.js | 41 +++++++++++++++++++------------------ 5 files changed, 86 insertions(+), 33 deletions(-) create mode 100644 src/constants/eventNames.js create mode 100644 src/constants/index.js diff --git a/src/app/index.js b/src/app/index.js index 87b4af81..80084cb9 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -15,6 +15,7 @@ const { } = require('wootils/node/providers'); const { EventsHub } = require('wootils/shared'); +const { eventNames } = require('../constants'); const commonServices = require('../services/common'); const httpServices = require('../services/http'); const utilsServices = require('../services/utils'); @@ -190,7 +191,7 @@ class Jimpex extends Jimple { this._mountQueue.push((server) => { let result; const routes = this._reduceWithEvent( - 'controller-will-be-mounted', + 'controllerWillBeMounted', controller.connect(this, route), route, controller @@ -204,7 +205,7 @@ class Jimpex extends Jimple { } this._controlledRoutes.push(route); - this._emitEvent('route-added', route); + this._emitEvent('routeAdded', route); return result; }); } @@ -219,7 +220,7 @@ class Jimpex extends Jimple { const middlewareHandler = middleware.connect(this); if (middlewareHandler) { server.use(this._reduceWithEvent( - 'middleware-will-be-used', + 'middlewareWillBeUsed', middlewareHandler, middleware )); @@ -227,7 +228,7 @@ class Jimpex extends Jimple { } else { // But if the middleware is a regular middleware, just use it directly. server.use(this._reduceWithEvent( - 'middleware-will-be-used', + 'middlewareWillBeUsed', middleware, null )); @@ -243,14 +244,14 @@ class Jimpex extends Jimple { start(fn = () => {}) { const config = this.get('appConfiguration'); const port = config.get('port'); - this._emitEvent('before-start'); + this._emitEvent('beforeStart'); this._instance = this._express.listen(port, () => { this._emitEvent('start'); this._mountResources(); this.get('appLogger').success(`Starting on port ${port}`); - this._emitEvent('after-start'); + this._emitEvent('afterStart'); const result = fn(config); - this._emitEvent('after-start-callback'); + this._emitEvent('afterStartCallback'); return result; }); @@ -285,10 +286,10 @@ class Jimpex extends Jimple { */ stop() { if (this._instance) { - this._emitEvent('before-stop'); + this._emitEvent('beforeStop'); this._instance.close(); this._instance = null; - this._emitEvent('after-stop'); + this._emitEvent('afterStop'); } } /** @@ -458,24 +459,24 @@ class Jimpex extends Jimple { } /** * Emits an app event with a reference to this class instance. - * @param {string} name The name of the event. + * @param {string} name The name of the event on {@link JimpexEvents}. * @param {...*} args Extra parameters for the listeners. * @access protected */ _emitEvent(name, ...args) { - this.get('events').emit(name, ...[...args, this]); + this.get('events').emit(eventNames[name], ...[...args, this]); } /** * Sends a target object to a list of reducer events so they can modify or replace it. This * method also sends a reference to this class instance as the last parameter of the event. - * @param {string} name The name of the event. + * @param {string} name The name of the event on {@link JimpexEvents}. * @param {*} target The targe object to reduce. * @param {...*} args Extra parameters for the listeners. * @return {*} An object of the same type as the `target`. * @access protected */ _reduceWithEvent(name, target, ...args) { - return this.get('events').reduce(name, target, ...[...args, this]); + return this.get('events').reduce(eventNames[name], target, ...[...args, this]); } } diff --git a/src/constants/eventNames.js b/src/constants/eventNames.js new file mode 100644 index 00000000..fddb0509 --- /dev/null +++ b/src/constants/eventNames.js @@ -0,0 +1,37 @@ +/** + * @typedef {Object} JimpexEvents + * @description The name of all the events {@link Jimpex} can trigger. + * @property {string} beforeStart Called before `listen` is executed on the Express + * app. + * @property {string} start Called from the `listen` callback, when the app is + * ready to be used. + * @property {string} afterStart Called from the `listen` callback, when all + * controllers and middlewares have been mounted. + * @property {string} afterStartCallback Called right after the callback sent to `start` + * gets executed. + * @property {string} beforeStop Called before closing the instance of the app. + * @property {string} afterStop called after the app instance has been closed and + * deleted. + * @property {string} routeAdded Called every time a new route is added to the app. + * @property {string} controllerWillBeMounted This is for a reducer event. It gets called before + * mounting a router or a set of routes to the app in + * order to "reduce it". + * @property {string} middlewareWillBeUsed This is for a reducer event. it gets called before + * using a middleware in order to "reduce it". + */ + +/** + * The name of all the events {@link Jimpex} can trigger. + * @type {JimpexEvents} + */ +module.exports = { + beforeStart: 'before-start', + start: 'start', + afterStart: 'after-start', + afterStartCallback: 'after-start-callback', + beforeStop: 'before-stop', + afterStop: 'after-stop', + controllerWillBeMounted: 'controller-will-be-mounted', + routeAdded: 'route-added', + middlewareWillBeUsed: 'middleware-will-be-used', +}; diff --git a/src/constants/index.js b/src/constants/index.js new file mode 100644 index 00000000..9facc953 --- /dev/null +++ b/src/constants/index.js @@ -0,0 +1,12 @@ +const eventNames = require('./eventNames'); +/** + * A dictionary with all the constants the app needs. + * @type {Object} + * @property {JimpexEvents} eventNames The name of all the events {@link Jimpex} can trigger. + * @ignore + */ +const constants = { + eventNames, +}; + +module.exports = constants; diff --git a/src/index.js b/src/index.js index 44743201..78b0d5ff 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ const Jimpex = require('./app'); const controllers = require('./controllers'); const middlewares = require('./middlewares'); const services = require('./services'); +const { eventNames } = require('./constants'); const { provider, providerCreator, @@ -21,5 +22,6 @@ module.exports = { provider, providerCreator, services, + eventNames, Jimpex, }; diff --git a/tests/app/index.test.js b/tests/app/index.test.js index 3a823114..8539e0fc 100644 --- a/tests/app/index.test.js +++ b/tests/app/index.test.js @@ -20,6 +20,7 @@ const path = require('path'); require('jasmine-expect'); const Jimpex = require('/src/app'); +const { eventNames } = require('/src/constants'); const { EventsHub } = require('wootils/shared'); const originalNodeTLSRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED; @@ -765,12 +766,12 @@ describe('app:Jimpex', () => { let sut = null; let runningInstance = null; const expectedEvents = [ - 'before-start', - 'start', - 'after-start', - 'after-start-callback', - 'before-stop', - 'after-stop', + eventNames.beforeStart, + eventNames.start, + eventNames.afterStart, + eventNames.afterStartCallback, + eventNames.beforeStop, + eventNames.afterStop, ]; // When sut = new Sut(); @@ -1052,11 +1053,11 @@ describe('app:Jimpex', () => { ...routes.map((routeRouter) => [route, routeRouter]), ]; const expectedEvents = [ - ['before-start'], - ['start'], - ['route-added', route], - ['after-start'], - ['after-start-callback'], + [eventNames.beforeStart], + [eventNames.start], + [eventNames.routeAdded, route], + [eventNames.afterStart], + [eventNames.afterStartCallback], ]; // When sut = new Sut(); @@ -1074,7 +1075,7 @@ describe('app:Jimpex', () => { }); expect(events.reduce).toHaveBeenCalledTimes(1); expect(events.reduce).toHaveBeenCalledWith( - 'controller-will-be-mounted', + eventNames.controllerWillBeMounted, routes, route, controller, @@ -1132,11 +1133,11 @@ describe('app:Jimpex', () => { ...[[route, router]], ]; const expectedEvents = [ - ['before-start'], - ['start'], - ['route-added', route], - ['after-start'], - ['after-start-callback'], + [eventNames.beforeStart], + [eventNames.start], + [eventNames.routeAdded, route], + [eventNames.afterStart], + [eventNames.afterStartCallback], ]; // When sut = new Sut(); @@ -1154,7 +1155,7 @@ describe('app:Jimpex', () => { }); expect(events.reduce).toHaveBeenCalledTimes(1); expect(events.reduce).toHaveBeenCalledWith( - 'controller-will-be-mounted', + eventNames.controllerWillBeMounted, router, route, controller, @@ -1220,7 +1221,7 @@ describe('app:Jimpex', () => { }); expect(events.reduce).toHaveBeenCalledTimes(1); expect(events.reduce).toHaveBeenCalledWith( - 'middleware-will-be-used', + eventNames.middlewareWillBeUsed, middlewareFn, middleware, sut @@ -1281,7 +1282,7 @@ describe('app:Jimpex', () => { }); expect(events.reduce).toHaveBeenCalledTimes(1); expect(events.reduce).toHaveBeenCalledWith( - 'middleware-will-be-used', + eventNames.middlewareWillBeUsed, middleware, null, sut From 7dc22fffbfff48ddb151e0589c023c86a00654d5 Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 04:25:42 -0300 Subject: [PATCH 59/78] refactor(middlewares/html/fastHTML): use options instead of parameters and validate the controlled routes --- src/middlewares/html/fastHTML.js | 294 +++++++++++------ tests/middlewares/html/fastHTML.test.js | 401 ++++++++++++++++-------- 2 files changed, 467 insertions(+), 228 deletions(-) diff --git a/src/middlewares/html/fastHTML.js b/src/middlewares/html/fastHTML.js index bafe988e..711fe92e 100644 --- a/src/middlewares/html/fastHTML.js +++ b/src/middlewares/html/fastHTML.js @@ -1,39 +1,65 @@ const mime = require('mime'); +const ObjectUtils = require('wootils/shared/objectUtils'); +const { eventNames } = require('../../constants'); const { middlewareCreator } = require('../../utils/wrappers'); + +/** + * @typedef {Object} FastHTMLOptions + * @description The options to customize the behavior of the middleware. + * @property {string} [file='index.html'] The name of the file the middleware will serve. It can + * get overwritten if {@link FastHTML} receives an + * {@link HTMLGenerator}, in that case, the file will be + * obtained from that service. + * @property {Array} [ignore] A list of regular expressions to match requests paths + * that should be ignored. + * @property {boolean} [useAppRoutes=true] If `true`, {@link FastHTML} will get the list of all + * routes controlled by {@link Jimpex} and will use them + * to validate the incoming requests (in addition to + * `ignore`): If a request URL doesn't match with any of + * the controlled routes, it will show the HTML file. + */ + +/** + * @typedef {Object} FastHTMLMiddlewareOptions + * @extends {FastHTMLOptions} + * @description The only difference with {@link FastHTMLOptions} is that in this options, you can + * specify an {@link HTMLGenerator} service name. + * @property {string} htmlGenerator The name of a {@link HTMLGenerator} service for the middleware + * to use. + */ + /** * It's common for an app to show an HTML view when no route was able to handle a request, so the * idea behind this middleware is to avoid going to every middleware and controller and just * specify that if the request is not for a route handled by a controller, just serve the HTML * and avoid processing unnecessary data. - * A simple example: The app has a route `/backend` that the frontend uses to get information. + * + * A simple example: The app has a route `/backend` that a frontend uses to get information. * This middleware can be used to only allow the execution of middlewares and controllers when - * the request route is for `/backend`, thus avoid extra processing. - * Disclaimer: Managing statics files with Express is not a best practice, but there are scenarios - * where there is not other choice. + * the request route is for `/backend`. + * + * **Disclaimer**: Managing statics files with Express is not a best practice, but there are + * scenarios where there is not other choice. */ class FastHTML { /** - * Class constructor. - * @param {SendFile} sendFile Necessary to serve the HTML file. - * @param {string} [file='index.html'] The name of the file it will - * serve. If `htmlGenerator` is - * specified, this will be - * overwritten with the name of - * the file generated by that - * service. - * @param {Array} [ignoredRoutes=[/^\/api\//, /\.ico$/]] A list of regular expressions - * to match requests paths that - * should be ignored. - * @param {HTMLGenerator} [htmlGenerator=null] If used, the file to serve will - * be the one generated by that - * service. + * @param {EventsHub} events To listen for the {@link Jimpex} event + * triggered after the app starts. The event is + * used to get all the controlled routes, in case + * the `useAppRoutes` option is set to true. + * @param {SendFile} sendFile To send the HTML file response. + * @param {FastHTMLOptions} [options={}] To customize the middleware behavior. + * @param {?HTMLGenerator} [htmlGenerator=null] If used, the file to serve will be the one + * generated by that service. */ - constructor( - sendFile, - file = 'index.html', - ignoredRoutes = [/^\/api\//, /\.ico$/], - htmlGenerator = null - ) { + constructor(events, sendFile, options = {}, htmlGenerator = null) { + /** + * A local reference for the `events` service. + * @type {EventsHub} + * @access protected + * @ignore + */ + this._events = events; /** * A local reference for the `sendFile` service. * @type {SendFile} @@ -42,36 +68,50 @@ class FastHTML { */ this._sendFile = sendFile; /** - * The name of the file to serve. - * @type {string} + * If specified, a reference for a service that generates HTML files. + * @type {?HTMLGenerator} + * @access protected + * @ignore */ - this._file = file; + this._htmlGenerator = htmlGenerator; /** - * A list of regular expressions to match requests paths that should be ignored. - * @type {Array} + * The options that tell the middleware which routes should be ignored and which is the file + * to serve. + * @type {FastHTMLOptions} */ - this._ignoredRoutes = ignoredRoutes; + this._options = this._normalizeOptions(ObjectUtils.merge( + { + file: 'index.html', + ignore: options.ignore || [/\.ico$/i], + useAppRoutes: true, + }, + options + )); /** - * If specified, a reference for a service that generates HTML files. - * @type {HTMLGenerator} + * Whether or not the file is ready to be served, in case there's an + * {@link HTMLGenerator}. If the service is used, the HTML is generated after the app starts, + * so the middleware will have to wait for it to be ready before being able to serve it. + * @type {boolean} + * @access protected + * @ignore */ - this._htmlGenerator = htmlGenerator; + this._ready = !this._htmlGenerator; /** - * Whether or not the file is ready to be served. - * @type {Boolean} + * A list of regular expression that match the routes controlled by the app. This is in case + * the `useAppRoutes` option is set to `true`; when the app gets started, an event listener + * will obtain all the top controlled routes, create regular expressions and save them on + * this property. + * @type {Array} * @access protected * @ignore */ - this._ready = true; - // If an `HTMLGenerator` service was specified... - if (this._htmlGenerator) { - // ...get the name of the file from that service. - this._file = this._htmlGenerator.getFile(); - /** - * Mark the `_ready` flag as `false` as this service needs to wait for the generator to - * create the file. - */ - this._ready = false; + this._routeExpressions = []; + /** + * If the option to use the controlled routes is set to `true`, setup the event listener that + * gets all the routes when the app is started. + */ + if (this._options.useAppRoutes) { + this._setupEvents(); } } /** @@ -80,93 +120,151 @@ class FastHTML { */ middleware() { return (req, res, next) => { - // Validate if the route should be ignored. - const shouldIgnore = this._ignoredRoutes - .some((expression) => expression.test(req.originalUrl)); - // If the route should be ignored... - if (shouldIgnore) { - // ...go to the next middleware. + if (this._shouldIgnore(req.originalUrl)) { + // If the route should be ignored, move to the next middleware. next(); } else if (!this._ready) { - /** - * ...if `_ready` is `false`, it means that it's using the `HTMLGenerator` service, so it - * calls the method that will notify this service when the file has been created and is - * ready to be loaded. - */ + // If there's an HTMLGenerator and is not ready, wait for it... this._htmlGenerator.whenReady() .then(() => { - // The file is ready to use, so mark the `_ready` flag as `true`. + // Change the flag to prevent the next execution to enter here. this._ready = true; // Serve the file. this._sendHTML(res, next); }) .catch((error) => { - // Something happened while generating the file, send the error the next middlware. + // Something went wrong while generating the file, send the error to the error handler. next(error); }); } else { - /** - * If `_ready` is `true` it means that the `HTMLGenerator` has already created the file on - * a previous request or it was never specified, so just serve the file. - */ + // The route is not ignored and the file is ready to be served, so do it. this._sendHTML(res, next); } }; } /** - * The name of the file to serve. - * @type {string} + * The options that tell the middleware which routes should be ignored and which is the file + * to serve. + * @type {FastHTMLOptions} */ - get file() { - return this._file; + get options() { + return Object.freeze(this._options); } /** - * A list of regular expressions to match requests paths that should be ignored. - * @type {Array} + * Normalizes and validates the options recevied on the constructor. + * If the class is using a {@link HTMLGenerator} service, the method will overwrite the `file` + * option with the result of the service's `getFile()` method. + * @param {FastHTMLOptions} options The received options. + * @return {FastHTMLOptions} + * @throws {Error} If no file and no {@link HTMLGenerator} service are specified. + * @throws {Error} If no routes to ignore are specified and `useAppRoutes` is set to `false`. + * @access protected + * @ignore + */ + _normalizeOptions(options) { + if (!options.file && !this._htmlGenerator) { + throw new Error('You need to either define an HTMLGenerator service or a file'); + } else if (!options.ignore.length && !options.useAppRoutes) { + throw new Error( + 'You need to either define a list of routes to ignore or use `useAppRoutes`' + ); + } + + return this._htmlGenerator ? + Object.assign({}, options, { file: this._htmlGenerator.getFile() }) : + options; + } + /** + * Adds the event listener that obtains the controlled routes when `useAppRoutes` is set to + * `true`. + * @access protected + * @ignore */ - get ignoredRoutes() { - return this._ignoredRoutes.slice(); + _setupEvents() { + this._events.once(eventNames.afterStart, ({ routes }) => { + // Re generate the list of expressions... + this._routeExpressions = routes + // Remove leading and trailing slashes. + .map((route) => route.replace(/^\/+/, '').replace(/\/+$/, '').trim()) + // Filter empty routes (in case they were for `/`). + .filter((route) => route !== '') + // Remove repeated routes. + .reduce((unique, route) => (unique.includes(route) ? unique : [...unique, route]), []) + // Generate regular expressions for each route. + .map((route) => this._getRouteExpression(route)); + }); + } + /** + * Generates a regular expression for a given route. + * @param {string} route The route from where the expression will be created. + * @return {RegExp} + * @access protected + * @ignore + */ + _getRouteExpression(route) { + const expression = route + // Separate each component of the route. + .split('/') + /** + * If the component is for a paramter, replace it with a expression to match anything; if not, + * escape it so it can be used on the final expression. + */ + .map((part) => ( + part.startsWith(':') ? + '(?:([^\\/]+?))' : + part.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + )) + // Put everything back together. + .join('\\/'); + // Before returning it, add a leading slash, as `req.originalUrl` always have one. + return new RegExp(`\\/${expression}`); + } + /** + * Checks whether a route should be ignored or not. The method checks first against the `ignore` + * option, and then against the controlled routes (if `useAppRoutes` is `false`, the list + * will be empty). + * @param {string} route The route to validate. + * @return {boolean} + * @access protected + * @ignore + */ + _shouldIgnore(route) { + return this._options.ignore.some((expression) => expression.test(route)) || + this._routeExpressions.some((expression) => expression.test(route)); } /** * Serves the file on the response. * @param {ExpressResponse} res The server response. - * @param {ExpressNext} next The functino to call the next middleware. - * @ignore + * @param {ExpressNext} next The function to call the next middleware. * @access protected + * @ignore */ _sendHTML(res, next) { res.setHeader('Content-Type', mime.getType('html')); - this._sendFile(res, this._file, next); + this._sendFile(res, this._options.file, next); } } /** - * A middleware for filtering routes and serve an HTML file when the requested route doesn't have - * a controller to handle it. + * A middleware for filtering routes so you can serve an HTML before the app gets to evaluate + * whether there's a controller for the requested route or not. For more information about the + * reason of this middleware, please read the description of {@link FastHTML}. * @type {MiddlewareCreator} - * @param {string} [file] The name of the file it will serve. - * If the `HTMLGenerator` service - * specified is avaialable, this will - * be overwritten with the name of the - * file generated by that service. - * @param {Array} [ignoredRoutes] A list of regular expressions to - * match requests paths that should be - * ignored. - * @param {string} [htmlGeneratorServiceName='htmlGenerator'] The name of a `HTMLGenerator` - * service. If the service is not - * registered on the app, it won't throw - * an error, but just send `null` to - * the service constructor. + * @param {FastHTMLMiddlewareOptions} [options={}] The options to customize the middleware + * behavior. */ -const fastHTML = middlewareCreator(( - file, - ignoredRoutes, - htmlGeneratorServiceName = 'htmlGenerator' -) => (app) => new FastHTML( - app.get('sendFile'), - file, - ignoredRoutes, - app.try(htmlGeneratorServiceName) -).middleware()); +const fastHTML = middlewareCreator((options = {}) => (app) => { + const htmlGeneratorServiceName = typeof options.htmlGenerator === 'undefined' ? + 'htmlGenerator' : + options.htmlGenerator; + return ( + new FastHTML( + app.get('events'), + app.get('sendFile'), + options, + htmlGeneratorServiceName ? app.try(htmlGeneratorServiceName) : null + ) + ).middleware(); +}); module.exports = { FastHTML, diff --git a/tests/middlewares/html/fastHTML.test.js b/tests/middlewares/html/fastHTML.test.js index fff325b2..9eb8445a 100644 --- a/tests/middlewares/html/fastHTML.test.js +++ b/tests/middlewares/html/fastHTML.test.js @@ -6,211 +6,352 @@ const { FastHTML, fastHTML, } = require('/src/middlewares/html/fastHTML'); +const { eventNames } = require('/src/constants'); describe('middlewares/html:fastHTML', () => { - it('should be instantiated', () => { + it('should be instantiated with its default options', () => { // Given + const events = { + once: jest.fn(), + }; const sendFile = 'sendFile'; let sut = null; // When - sut = new FastHTML(sendFile); + sut = new FastHTML(events, sendFile); // Then expect(sut).toBeInstanceOf(FastHTML); + expect(sut.options).toEqual({ + file: 'index.html', + ignore: [/\.ico$/i], + useAppRoutes: true, + }); + expect(events.once).toHaveBeenCalledTimes(1); + expect(events.once).toHaveBeenCalledWith(eventNames.afterStart, expect.any(Function)); }); - it('should be instantiated with the optional htmlGenerator service', () => { + it('should be instantiated with custom options', () => { // Given + const events = { + once: jest.fn(), + }; const sendFile = 'sendFile'; - const file = 'random.html'; - const htmlGenerator = { - getFile: jest.fn(() => file), + const options = { + file: 'my-file.html', + ignore: [/\.jpg$/i], + useAppRoutes: false, }; let sut = null; // When - sut = new FastHTML(sendFile, 'index.html', [], htmlGenerator); + sut = new FastHTML(events, sendFile, options); // Then expect(sut).toBeInstanceOf(FastHTML); - expect(sut.file).toBe(file); - expect(htmlGenerator.getFile).toHaveBeenCalledTimes(1); + expect(sut.options).toEqual(options); + expect(events.once).toHaveBeenCalledTimes(0); }); - it('should return a middleware to redirect specific traffic to an html file', () => { + it('should be instantiated and obtain the file from an HTMLGenerator service', () => { // Given - const sendFile = jest.fn(); - const file = 'charito.html'; - const ignoredRoutes = []; - const request = { - originalUrl: '/some/path', + const events = 'events'; + const sendFile = 'sendFile'; + const options = { + file: 'my-file.html', + useAppRoutes: false, }; - const response = { - setHeader: jest.fn(), + const overwriteFile = 'my-other-file.html'; + const htmlGenerator = { + getFile: jest.fn(() => overwriteFile), }; - const next = 'next'; let sut = null; - let middleware = null; // When - sut = new FastHTML(sendFile, file, ignoredRoutes); - middleware = sut.middleware(); - middleware(request, response, next); + sut = new FastHTML(events, sendFile, options, htmlGenerator); // Then - expect(response.setHeader).toHaveBeenCalledTimes(1); - expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html'); - expect(sendFile).toHaveBeenCalledTimes(1); - expect(sendFile).toHaveBeenCalledWith(response, file, next); + expect(sut).toBeInstanceOf(FastHTML); + expect(sut.options.file).toEqual(overwriteFile); }); - it('shouldn\'t redirect if the route is on the ignored list', () => { + it('should throw an error when instantiated without a file and an HTMLGenerator', () => { // Given - const sendFile = jest.fn(); - const file = 'charito.html'; - const ignoredRoutes = [/^\/some\//i]; - const request = { - originalUrl: '/some/path', + const events = 'events'; + const sendFile = 'sendFile'; + const options = { + file: null, + useAppRoutes: false, }; - const response = 'response'; - const next = jest.fn(); - let sut = null; - let middleware = null; - // When - sut = new FastHTML(sendFile, file, ignoredRoutes); - middleware = sut.middleware(); - middleware(request, response, next); - // Then - expect(sut.ignoredRoutes).toEqual(ignoredRoutes); - expect(next).toHaveBeenCalledTimes(1); - expect(sendFile).toHaveBeenCalledTimes(0); + // When/Then + expect(() => new FastHTML(events, sendFile, options)) + .toThrow(/You need to either define an HTMLGenerator service or a file/i); }); - it( - 'should redirect to the file created by the htmlGenerator service', - () => new Promise((resolve) => { + it('should throw an error if there are no ignored routes and `useAppRoutes` is `false`', () => { + // Given + const events = 'events'; + const sendFile = 'sendFile'; + const options = { + ignore: [], + useAppRoutes: false, + }; + // When/Then + expect(() => new FastHTML(events, sendFile, options)) + .toThrow(/You need to either define a list of routes to ignore or use `useAppRoutes`/i); + }); + + describe('middleware', () => { + it('should show the HTML file if the URL is not on the `ignore` list', () => { // Given + const events = 'events'; + const sendFile = jest.fn(); + const options = { + file: 'my-file.html', + useAppRoutes: false, + }; + const request = { + originalUrl: '/some/path', + }; + const response = { + setHeader: jest.fn(), + }; + const next = 'next'; + let sut = null; + // When + sut = new FastHTML(events, sendFile, options); + sut.middleware()(request, response, next); + // Then + expect(response.setHeader).toHaveBeenCalledTimes(1); + expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html'); + expect(sendFile).toHaveBeenCalledTimes(1); + expect(sendFile).toHaveBeenCalledWith(response, options.file, next); + }); + + it('shouldn\'t show the HTML file if the URL is on the `ignore` list', () => { + // Given + const events = 'events'; + const sendFile = jest.fn(); + const options = { + file: 'my-file.html', + ignore: [/\.png$/i], + useAppRoutes: false, + }; + const request = { + originalUrl: '/some/path.png', + }; + const response = 'response'; + const next = jest.fn(); + let sut = null; + // When + sut = new FastHTML(events, sendFile, options); + sut.middleware()(request, response, next); + // Then + expect(next).toHaveBeenCalledTimes(1); + expect(sendFile).toHaveBeenCalledTimes(0); + }); + + it('should validate the requests against the controlled routes', () => { + // Given + const events = { + once: jest.fn(), + }; + const sendFile = jest.fn(); + const options = { + file: 'my-file.html', + }; + const casesForSuccess = [ + { + route: '/api/:version', + url: '/api/abc123', + }, + { + route: '/something', + url: '/something', + }, + { + route: '/service', + url: '/service/health', + }, + { + route: '/service/:name/status', + url: '/service/health/status', + }, + { + route: '/service/:name/status', + url: '/service/health/status/extra', + }, + ]; + const casesForFailure = [ + { + route: '/other-api/:version/resource', + url: '/other-api/abc123/res', + }, + { + route: '/other-services/health/extra', + url: '/other-services/health', + }, + ]; + const cases = [ + ...casesForSuccess, + ...casesForFailure, + ]; + const routes = cases.map(({ route }) => route); + const response = { + setHeader: jest.fn(), + }; + const next = jest.fn(); + let sut = null; + let middleware = null; + let listener = null; + // When + sut = new FastHTML(events, sendFile, options); + [[, listener]] = events.once.mock.calls; + listener({ routes }); + middleware = sut.middleware(); + cases.forEach(({ url }) => { + middleware({ originalUrl: url }, response, next); + }); + // Then + expect(next).toHaveBeenCalledTimes(casesForSuccess.length); + expect(sendFile).toHaveBeenCalledTimes(casesForFailure.length); + }); + + it('should show the HTML file created by an HTMLGenerator', () => new Promise((resolve) => { + // Given + const events = 'events'; const sendFile = jest.fn((res, file, next) => next()); - const file = 'charito.html'; - const ignoredRoutes = []; + const file = 'Pilar.html'; const htmlGenerator = { getFile: jest.fn(() => file), whenReady: jest.fn(() => Promise.resolve()), }; + const options = { + file: 'my-file.html', + ignore: [/\.png$/i], + useAppRoutes: false, + }; const request = { - originalUrl: '/some/path', + originalUrl: '/some/path.jpg', }; const response = { setHeader: jest.fn(), }; let sut = null; - let middleware = null; // When - sut = new FastHTML(sendFile, '', ignoredRoutes, htmlGenerator); - middleware = sut.middleware(); - middleware(request, response, () => { + sut = new FastHTML(events, sendFile, options, htmlGenerator); + sut.middleware()(request, response, () => { // Then expect(htmlGenerator.getFile).toHaveBeenCalledTimes(1); expect(htmlGenerator.whenReady).toHaveBeenCalledTimes(1); + expect(response.setHeader).toHaveBeenCalledTimes(1); + expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html'); expect(sendFile).toHaveBeenCalledTimes(1); expect(sendFile).toHaveBeenCalledWith(response, file, expect.any(Function)); resolve(); }); - }) - .catch(() => { - expect(true).toBeFalse(); - }) - ); + })); - it( - 'should fail redirect to the file created by the htmlGenerator service', - () => new Promise((resolve) => { + it('should fail to show the HTML file from the HTMLGenerator', () => new Promise((resolve) => { // Given + const events = 'events'; const sendFile = jest.fn((res, file, next) => next()); - const file = 'charito.html'; - const ignoredRoutes = []; + const file = 'Pilar.html'; const error = new Error('Unknown error'); const htmlGenerator = { getFile: jest.fn(() => file), whenReady: jest.fn(() => Promise.reject(error)), }; + const options = { + file: 'my-file.html', + ignore: [/\.png$/i], + useAppRoutes: false, + }; const request = { - originalUrl: '/some/path', + originalUrl: '/some/path.jpg', }; const response = { setHeader: jest.fn(), }; let sut = null; - let middleware = null; // When - sut = new FastHTML(sendFile, '', ignoredRoutes, htmlGenerator); - middleware = sut.middleware(); - middleware(request, response, (result) => { + sut = new FastHTML(events, sendFile, options, htmlGenerator); + sut.middleware()(request, response, (result) => { // Then expect(result).toBe(error); expect(htmlGenerator.getFile).toHaveBeenCalledTimes(1); expect(htmlGenerator.whenReady).toHaveBeenCalledTimes(1); - expect(sendFile).toHaveBeenCalledTimes(0); resolve(); }); - }) - .catch(() => { - expect(true).toBeFalse(); - }) - ); - - it('should include a middleware shorthand to return its function', () => { - // Given - const services = {}; - const app = { - get: jest.fn((service) => services[service] || service), - try: jest.fn(() => null), - }; - let middleware = null; - let toCompare = null; - const expectedGets = [ - 'sendFile', - ]; - const expectedTryAttempts = [ - 'htmlGenerator', - ]; - // When - middleware = fastHTML.connect(app); - toCompare = new FastHTML(); - // Then - expect(middleware.toString()).toEqual(toCompare.middleware().toString()); - expect(app.get).toHaveBeenCalledTimes(expectedGets.length); - expectedGets.forEach((service) => { - expect(app.get).toHaveBeenCalledWith(service); - }); - expect(app.try).toHaveBeenCalledTimes(expectedTryAttempts.length); - expectedTryAttempts.forEach((service) => { - expect(app.try).toHaveBeenCalledWith(service); - }); + })); }); - it('should include a middleware creator shorthand to configure its options', () => { - // Given - const services = {}; - const app = { - get: jest.fn((service) => services[service] || service), - try: jest.fn(() => null), - }; - let middleware = null; - let toCompare = null; - const expectedGets = [ - 'sendFile', - ]; - const expectedTryAttempts = [ - 'htmlGenerator', - ]; - // When - middleware = fastHTML().connect(app); - toCompare = new FastHTML(); - // Then - expect(middleware.toString()).toEqual(toCompare.middleware().toString()); - expect(app.get).toHaveBeenCalledTimes(expectedGets.length); - expectedGets.forEach((service) => { - expect(app.get).toHaveBeenCalledWith(service); + describe('shorthand', () => { + it('should return the middleware', () => { + // Given + const events = { + once: jest.fn(), + }; + const services = { + events, + }; + const app = { + get: jest.fn((service) => services[service] || service), + try: jest.fn(() => null), + }; + let middleware = null; + let toCompare = null; + const expectedGets = [ + 'events', + 'sendFile', + ]; + const expectedTryAttempts = [ + 'htmlGenerator', + ]; + // When + middleware = fastHTML.connect(app); + toCompare = new FastHTML('events', 'sendFile', { useAppRoutes: false }); + // Then + expect(middleware.toString()).toEqual(toCompare.middleware().toString()); + expect(app.get).toHaveBeenCalledTimes(expectedGets.length); + expectedGets.forEach((service) => { + expect(app.get).toHaveBeenCalledWith(service); + }); + expect(app.try).toHaveBeenCalledTimes(expectedTryAttempts.length); + expectedTryAttempts.forEach((service) => { + expect(app.try).toHaveBeenCalledWith(service); + }); + expect(events.once).toHaveBeenCalledTimes(1); + expect(events.once).toHaveBeenCalledWith(eventNames.afterStart, expect.any(Function)); }); - expect(app.try).toHaveBeenCalledTimes(expectedTryAttempts.length); - expectedTryAttempts.forEach((service) => { - expect(app.try).toHaveBeenCalledWith(service); + + it('should allow the options to be customized', () => { + // Given + const events = { + once: jest.fn(), + }; + const services = { + events, + }; + const options = { + htmlGenerator: null, + useAppRoutes: false, + }; + const app = { + get: jest.fn((service) => services[service] || service), + try: jest.fn(() => null), + }; + let middleware = null; + let toCompare = null; + const expectedGets = [ + 'events', + 'sendFile', + ]; + // When + middleware = fastHTML(options).connect(app); + toCompare = new FastHTML('events', 'sendFile', { useAppRoutes: false }); + // Then + expect(middleware.toString()).toEqual(toCompare.middleware().toString()); + expect(app.get).toHaveBeenCalledTimes(expectedGets.length); + expectedGets.forEach((service) => { + expect(app.get).toHaveBeenCalledWith(service); + }); + expect(app.try).toHaveBeenCalledTimes(0); + expect(events.once).toHaveBeenCalledTimes(0); }); }); }); From 0f96006c0c6a5dc44e5653762b847db0af386da1 Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 04:26:23 -0300 Subject: [PATCH 60/78] docs(project): update the documents regarding the new version of the fastHTML middleware --- README-esdoc.md | 2 +- README.md | 2 +- documents/middlewares.md | 40 +++++++++++++++++++++++++++++++--------- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/README-esdoc.md b/README-esdoc.md index 53ee5628..57a6176e 100644 --- a/README-esdoc.md +++ b/README-esdoc.md @@ -377,7 +377,7 @@ Jimpex comes with a few services, middlewares and controllers that you can impor - **Error handler:** Allows you to generate responses for errors and potentially hide uncaught exceptions under a generic message, unless it's disabled via configuration settings. - **Force HTTPS:** Redirect all incoming traffic from HTTP to HTTPS. It also allows you to set routes to ignore the redirection. -- **Fast HTML:** Allows you to specify which routes will be handled and in case there are no controllers for a requested route, it sends out and HTML file, thus preventing the request to be unnecessarily processed by the middlewares. +- **Fast HTML:** Allows your app to skip unnecessary processing by showing an specific HTML when a requested route doesn't have a controller for it or is not on a "whitelist". - **Show HTML:** A really simple middleware to serve an HTML file. Its true feature is that it can be hooked up to the **HTML Generator** service. - **Version validator:** If you mount it on a route it will generate a `409` error if the request doesn't have a version parameter with the same version as the one on the configuration. diff --git a/README.md b/README.md index ae9885c8..aee299f8 100644 --- a/README.md +++ b/README.md @@ -377,7 +377,7 @@ Jimpex comes with a few services, middlewares and controllers that you can impor - **Error handler:** Allows you to generate responses for errors and potentially hide uncaught exceptions under a generic message, unless it's disabled via configuration settings. - **Force HTTPS:** Redirect all incoming traffic from HTTP to HTTPS. It also allows you to set routes to ignore the redirection. -- **Fast HTML:** Allows you to specify which routes will be handled and in case there are no controllers for a requested route, it sends out and HTML file, thus preventing the request to be unnecessarily processed by the middlewares. +- **Fast HTML:** Allows your app to skip unnecessary processing by showing an specific HTML when a requested route doesn't have a controller for it or is not on a "whitelist". - **Show HTML:** A really simple middleware to serve an HTML file. Its true feature is that it can be hooked up to the **HTML Generator** service. - **Version validator:** If you mount it on a route it will generate a `409` error if the request doesn't have a version parameter with the same version as the one on the configuration. diff --git a/documents/middlewares.md b/documents/middlewares.md index a25320d0..c3168acb 100644 --- a/documents/middlewares.md +++ b/documents/middlewares.md @@ -132,10 +132,10 @@ class App extends Jimpex { ## Fast HTML -Allows you to specify which routes will be handled and in case there are no controllers for a requested route, it sends out and HTML file, thus preventing the request to be unnecessarily processed by the middlewares. +Allows your app to skip unnecessary processing by showing an specific HTML when a requested route doesn't have a controller for it or is not on a "whitelist" - Module: `html` -- Requires: `sendFile` and, optionally, an `HTMLGenerator` service. +- Requires: `events`, `sendFile` and, optionally, an `HTMLGenerator` service. ```js const { @@ -159,7 +159,22 @@ class App extends Jimpex { } ``` -By default, if the requested URL doesn't match `/^\/api\//` or `/\.ico$/` it serves an `index.html`, but you can use it as a function to modify those options: +The middleware has a few options with default values that can be customized: + +```js +{ + // The name of the file it will serve. + file: 'index.html', + // A list of expressions for routes that should be ignored. + ignore: [/\.ico$/i], + // Whether or not to use the routes controlled by the app to validate the requests. + useAppRoutes: true, + // The name of the HTML Generator service the middleware can use to obtain the HTML. + htmlGenerator: 'htmlGenerator', +} +``` + +You can customize all those options by just calling the middleware as a function: ```js const { @@ -178,17 +193,24 @@ class App extends Jimpex { this.register(sendFile); // Add the middleware on one of the first positions. - this.use(fastHTML( - 'my-custom-index.html', - [`/^\/service\//`] - )); + this.use(fastHTML({ + file: 'my-custom-index.html', + ignore: [`/^\/service\//`], + useAppRoutes: false, + htmlGenerator: null, // To disable it. + })); } } ``` -Now, as mentioned on the requirements, you can optionally use the `htmlGenerator` or an `HTMLGenerator` service to serve a generated file. +Now, as mentioned on the requirements, you can optionally use the `htmlGenerator` service or an `HTMLGenerator`-like service to serve a generated file. + +You use the `htmlGenerator` option to disable it, or modify the name of the service it will look for: + +- If you set it to a _"falsy"_ value, it will be disabled. +- If you change its name, it will try to look for that service when mounted. -The default implementation checks if there's an `htmlGenerator` service registered on the app and uses that file; and in the case of `fastHTML`, you can specify a third parameter with the name of the `HTMLGenerator` service name you want to use. +**Important:** When using the generator, no matter the value you set on the `file` option, it will overwritten with the name of the file from the generator service. ## Show HTML From 87fa7f1910586a904e5bbeabb47c4bc56127b46a Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 04:34:52 -0300 Subject: [PATCH 61/78] fix(project): fix small issues with the documentation generator --- src/constants/eventNames.js | 4 +++- src/middlewares/html/fastHTML.js | 4 ++-- src/utils/wrappers.js | 11 +++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/constants/eventNames.js b/src/constants/eventNames.js index fddb0509..9d9fdf96 100644 --- a/src/constants/eventNames.js +++ b/src/constants/eventNames.js @@ -24,7 +24,7 @@ * The name of all the events {@link Jimpex} can trigger. * @type {JimpexEvents} */ -module.exports = { +const eventNames = { beforeStart: 'before-start', start: 'start', afterStart: 'after-start', @@ -35,3 +35,5 @@ module.exports = { routeAdded: 'route-added', middlewareWillBeUsed: 'middleware-will-be-used', }; + +module.exports = eventNames; diff --git a/src/middlewares/html/fastHTML.js b/src/middlewares/html/fastHTML.js index 711fe92e..eb724892 100644 --- a/src/middlewares/html/fastHTML.js +++ b/src/middlewares/html/fastHTML.js @@ -249,8 +249,8 @@ class FastHTML { * whether there's a controller for the requested route or not. For more information about the * reason of this middleware, please read the description of {@link FastHTML}. * @type {MiddlewareCreator} - * @param {FastHTMLMiddlewareOptions} [options={}] The options to customize the middleware - * behavior. + * @param {FastHTMLOptions|FastHTMLMiddlewareOptions} [options={}] The options to customize the + * middleware behavior. */ const fastHTML = middlewareCreator((options = {}) => (app) => { const htmlGeneratorServiceName = typeof options.htmlGenerator === 'undefined' ? diff --git a/src/utils/wrappers.js b/src/utils/wrappers.js index 4361a2ab..79eff6c8 100644 --- a/src/utils/wrappers.js +++ b/src/utils/wrappers.js @@ -215,9 +215,9 @@ const providers = (items) => { const providerCreator = (creatorFn) => resourceCreator('provider', 'register', creatorFn); /** * Generates a routes controller for the app container to mount. - * @param {ControllerMountCallback} connect A function that will be called the moment the app - * mounts the controller. It should return a list of - * routes. + * @param {ControllerMountCallback} connectFn A function that will be called the moment the app + * mounts the controller. It should return a list of + * routes. * @return {Controller} */ const controller = (connectFn) => resource('controller', 'connect', connectFn); @@ -237,9 +237,8 @@ const controller = (connectFn) => resource('controller', 'connect', connectFn); const controllerCreator = (creatorFn) => resourceCreator('controller', 'connect', creatorFn); /** * Generates a middleware for the app to use. - * @param {function(app:Jimpex):?ExpressMiddleware} connect A function that will be called the - * moment the app mounts the middleware. - * It should return an Express middleware. + * @param {MiddlewareUseCallback} connectFn A function that will be called the moment the app + * mounts the middleware. * @return {Middleware} */ const middleware = (connectFn) => resource('middleware', 'connect', connectFn); From 060b367d95c7dbc5df44063afa1ecef7774427eb Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 04:48:04 -0300 Subject: [PATCH 62/78] refactor(middlewares/html/showHTML): use an options object on the middleware creator --- src/middlewares/html/fastHTML.js | 4 +-- src/middlewares/html/showHTML.js | 43 ++++++++++++++----------- tests/middlewares/html/showHTML.test.js | 33 ++++++++++++++++++- 3 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/middlewares/html/fastHTML.js b/src/middlewares/html/fastHTML.js index eb724892..e0082bc8 100644 --- a/src/middlewares/html/fastHTML.js +++ b/src/middlewares/html/fastHTML.js @@ -24,8 +24,8 @@ const { middlewareCreator } = require('../../utils/wrappers'); * @extends {FastHTMLOptions} * @description The only difference with {@link FastHTMLOptions} is that in this options, you can * specify an {@link HTMLGenerator} service name. - * @property {string} htmlGenerator The name of a {@link HTMLGenerator} service for the middleware - * to use. + * @property {string} [htmlGenerator='htmlGenerator'] The name of a {@link HTMLGenerator} service + * for the middleware to use. */ /** diff --git a/src/middlewares/html/showHTML.js b/src/middlewares/html/showHTML.js index 97d35c80..7f6f9ff6 100644 --- a/src/middlewares/html/showHTML.js +++ b/src/middlewares/html/showHTML.js @@ -1,5 +1,16 @@ const mime = require('mime'); const { middlewareCreator } = require('../../utils/wrappers'); +/** + * @typedef {Object} ShowHTMLMiddlewareOptions + * @description A set options to customize the middleware behavior. + * @param {string} [file='index.html'] The name of the file the middleware will serve. + * @param {string} [htmlGenerator='htmlGenerator'] The name of a {@link HTMLGenerator} service for + * the middleware to use. If the service is + * available, the value of `file` will be + * overwritten for the file generated by the + * service. + */ + /** * A very simple middleware service to send an HTML on a server response. The special _'feature'_ of * this service is that it can be hooked up to an `HTMLGenerator` service and it will automatically @@ -111,25 +122,21 @@ class ShowHTML { /** * A middleware for showing an `index.html` file. * @type {MiddlewareCreator} - * @param {string} [file] The name of the file it will serve. - * If the `HTMLGenerator` service - * specified is avaialable, this will - * be overwritten with the name of the - * file generated by that service. - * @param {string} [htmlGeneratorServiceName='htmlGenerator'] The name of a `HTMLGenerator` - * service. If the service is not - * registered on the app, it won't throw - * an error, but just send `null` to - * the service constructor. + * @param {ShowHTMLMiddlewareOptions} [options] The options to customize the middleware behavior. */ -const showHTML = middlewareCreator(( - file, - htmlGeneratorServiceName = 'htmlGenerator' -) => (app) => new ShowHTML( - app.get('sendFile'), - file, - app.try(htmlGeneratorServiceName) -).middleware()); +const showHTML = middlewareCreator((options = {}) => (app) => { + const htmlGeneratorServiceName = typeof options.htmlGenerator === 'undefined' ? + 'htmlGenerator' : + options.htmlGenerator; + + return ( + new ShowHTML( + app.get('sendFile'), + options.file, + htmlGeneratorServiceName ? app.try(htmlGeneratorServiceName) : null + ) + ).middleware(); +}); module.exports = { ShowHTML, diff --git a/tests/middlewares/html/showHTML.test.js b/tests/middlewares/html/showHTML.test.js index 875b033c..8e1ca346 100644 --- a/tests/middlewares/html/showHTML.test.js +++ b/tests/middlewares/html/showHTML.test.js @@ -158,6 +158,10 @@ describe('middlewares/html:showHTML', () => { const htmlGeneratorService = { getFile: jest.fn(() => ''), }; + const options = { + file: 'some-file', + htmlGenerator: htmlGeneratorServiceName, + }; const app = { get: jest.fn((service) => service), try: jest.fn(() => htmlGeneratorService), @@ -171,7 +175,7 @@ describe('middlewares/html:showHTML', () => { htmlGeneratorServiceName, ]; // When - middleware = showHTML('some-file', htmlGeneratorServiceName).connect(app); + middleware = showHTML(options).connect(app); toCompare = new ShowHTML(); // Then expect(middleware.toString()).toEqual(toCompare.middleware().toString()); @@ -185,4 +189,31 @@ describe('middlewares/html:showHTML', () => { }); expect(htmlGeneratorService.getFile).toHaveBeenCalledTimes(1); }); + + it('should be able to disable the HTMLGenerator from the middleware creator', () => { + // Given + const options = { + file: 'some-file', + htmlGenerator: null, + }; + const app = { + get: jest.fn((service) => service), + try: jest.fn(), + }; + let middleware = null; + let toCompare = null; + const expectedGets = [ + 'sendFile', + ]; + // When + middleware = showHTML(options).connect(app); + toCompare = new ShowHTML(); + // Then + expect(middleware.toString()).toEqual(toCompare.middleware().toString()); + expect(app.get).toHaveBeenCalledTimes(expectedGets.length); + expectedGets.forEach((service) => { + expect(app.get).toHaveBeenCalledWith(service); + }); + expect(app.try).toHaveBeenCalledTimes(0); + }); }); From f4cdb600f97dce5e418d27980d70162644d11fb3 Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 05:17:00 -0300 Subject: [PATCH 63/78] refactor(services/html/htmlGenerator): use an options object on the provider creator --- src/services/html/htmlGenerator.js | 44 ++++++----- tests/services/html/htmlGenerator.test.js | 95 ++++++++++++++++++++--- 2 files changed, 108 insertions(+), 31 deletions(-) diff --git a/src/services/html/htmlGenerator.js b/src/services/html/htmlGenerator.js index 8d2b1750..e88077d6 100644 --- a/src/services/html/htmlGenerator.js +++ b/src/services/html/htmlGenerator.js @@ -1,8 +1,10 @@ const ObjectUtils = require('wootils/shared/objectUtils'); const { deferred } = require('wootils/shared'); +const { eventNames } = require('../../constants'); const { providerCreator } = require('../../utils/wrappers'); /** - * @typedef {Object} HTMLGeneratorOptions The options to customize the an `HTMLGenerator` service. + * @typedef {Object} HTMLGeneratorOptions + * @description The options to customize the an `HTMLGenerator` service. * @property {string} [template='index.tpl.html'] The name of the file it should * use as template. * @property {string} [file='index.html'] The name of the generated file. @@ -23,6 +25,20 @@ const { providerCreator } = require('../../utils/wrappers'); * the file. */ +/** + * @typedef {Object} HTMLGeneratorProviderOptions + * @extends {HTMLGeneratorOptions} + * @description These are the options specific for the service provider that registers + * {@link HTMLGenerator}. It's the same as {@link HTMLGeneratorOptions} but with a + * couple extras settings. + * @property {string} [serviceName='htmlGenerator'] The name that will be used to register the + * service on the app container. This is to allow multiple "instances" of the service to be + * created. + * @property {?string} [valuesService='htmlGeneratorValues'] + * The name of a service that the generator will use in order to read the values that will be + * injected on the template. If the service is available, the values from `configurationKeys` + * will be ignored. + */ /** * @typedef {Object} HTMLGeneratorValuesService A service to provide the information value to an * `HTMLGenerator` service to use on the generated @@ -306,35 +322,23 @@ class HTMLGenerator { * A service that hooks itself to the `after-start` event of the app server in order to trigger * the generation an the html file when the server starts. * @type {ProviderCreator} - * @param {HTMLGeneratorOptions} [options={}] Options to customize the service. - * @param {string} [serviceName='htmlGenerator'] The name of the service that will - * be register into the app. - * @param {?string} [valuesServiceName=null] The name of a service used to read - * the values that will be injected in - * the generated file. + * @param {HTMLGeneratorProviderOptions|HTMLGeneratorOptions} [options] The options to customize + * the service behavior. */ -const htmlGenerator = providerCreator(( - options = {}, - serviceName = 'htmlGenerator', - valuesServiceName = null -) => (app) => { +const htmlGenerator = providerCreator((options = {}) => (app) => { + const { serviceName = 'htmlGenerator' } = options; app.set(serviceName, () => { - let valuesService = null; - if (valuesServiceName) { - valuesService = app.get(valuesServiceName); - } - + const { valuesService = 'htmlGeneratorValues' } = options; return new HTMLGenerator( app.get('appConfiguration'), app.get('appLogger'), app.get('frontendFs'), options, - valuesService + valuesService ? app.try(valuesService) : null ); }); - app.get('events') - .once('after-start', () => app.get(serviceName).generateHTML()); + app.get('events').once(eventNames.afterStart, () => app.get(serviceName).generateHTML()); }); module.exports = { diff --git a/tests/services/html/htmlGenerator.test.js b/tests/services/html/htmlGenerator.test.js index ba78b188..2cd91efb 100644 --- a/tests/services/html/htmlGenerator.test.js +++ b/tests/services/html/htmlGenerator.test.js @@ -457,6 +457,7 @@ describe('services/html:htmlGenerator', () => { const app = { set: jest.fn(), get: jest.fn((service) => (service === name ? sut : services[service])), + try: jest.fn(() => null), }; let serviceName = null; let serviceFn = null; @@ -485,9 +486,8 @@ describe('services/html:htmlGenerator', () => { expect(appLogger.info).toHaveBeenCalledTimes(1); expect(appLogger.info).toHaveBeenCalledWith(expect.any(String)); expect(wootilsMock.mocks.deferredResolve).toHaveBeenCalledTimes(1); - }) - .catch(() => { - expect(true).toBeFalse(); + expect(app.try).toHaveBeenCalledTimes(1); + expect(app.try).toHaveBeenCalledWith('htmlGeneratorValues'); }); }); @@ -505,8 +505,7 @@ describe('services/html:htmlGenerator', () => { write: jest.fn(() => Promise.resolve()), delete: jest.fn(() => Promise.resolve()), }; - const myValuesServiceName = 'myValues'; - const myValuesService = { + const htmlGeneratorValues = { getValues: jest.fn(() => Promise.resolve({})), }; const events = { @@ -520,11 +519,11 @@ describe('services/html:htmlGenerator', () => { frontendFs, events, [name]: 'just-for-the-expect', - [myValuesServiceName]: myValuesService, }; const app = { set: jest.fn(), get: jest.fn((service) => (service === name ? sut : services[service])), + try: jest.fn(() => htmlGeneratorValues), }; let serviceName = null; let serviceFn = null; @@ -533,7 +532,9 @@ describe('services/html:htmlGenerator', () => { const expectedGets = Object.keys(services); const expectedEventName = 'after-start'; // When - htmlGenerator({}, name, myValuesServiceName).register(app); + htmlGenerator({ + serviceName: name, + }).register(app); [[serviceName, serviceFn]] = app.set.mock.calls; [[eventName, eventFn]] = events.once.mock.calls; sut = serviceFn(); @@ -561,10 +562,82 @@ describe('services/html:htmlGenerator', () => { expect(appLogger.info).toHaveBeenCalledTimes(1); expect(appLogger.info).toHaveBeenCalledWith(expect.any(String)); expect(wootilsMock.mocks.deferredResolve).toHaveBeenCalledTimes(1); - expect(myValuesService.getValues).toHaveBeenCalledTimes(1); - }) - .catch(() => { - expect(true).toBeFalse(); + expect(htmlGeneratorValues.getValues).toHaveBeenCalledTimes(1); + expect(app.try).toHaveBeenCalledTimes(1); + expect(app.try).toHaveBeenCalledWith('htmlGeneratorValues'); + }); + }); + + it('should register the generator with the values service disabled', () => { + // Given + const appConfiguration = { + get: jest.fn(() => {}), + }; + const appLogger = { + success: jest.fn(), + info: jest.fn(), + }; + const frontendFs = { + read: jest.fn(() => Promise.resolve('')), + write: jest.fn(() => Promise.resolve()), + delete: jest.fn(() => Promise.resolve()), + }; + const events = { + once: jest.fn(), + }; + let sut = null; + const name = 'myHTMLGenerator'; + const services = { + appConfiguration, + appLogger, + frontendFs, + events, + [name]: 'just-for-the-expect', + }; + const app = { + set: jest.fn(), + get: jest.fn((service) => (service === name ? sut : services[service])), + try: jest.fn(), + }; + let serviceName = null; + let serviceFn = null; + let eventName = null; + let eventFn = null; + const expectedGets = Object.keys(services); + const expectedEventName = 'after-start'; + // When + htmlGenerator({ + serviceName: name, + valuesService: null, + }).register(app); + [[serviceName, serviceFn]] = app.set.mock.calls; + [[eventName, eventFn]] = events.once.mock.calls; + sut = serviceFn(); + return eventFn() + .then(() => { + // Then + expect(serviceName).toBe(name); + expect(eventName).toBe(expectedEventName); + expect(app.get).toHaveBeenCalledTimes(expectedGets.length); + expectedGets.forEach((service) => { + expect(app.get).toHaveBeenCalledWith(service); + }); + expect(app.set).toHaveBeenCalledTimes(1); + expect(app.set).toHaveBeenCalledWith(name, expect.any(Function)); + expect(events.once).toHaveBeenCalledTimes(1); + expect(events.once).toHaveBeenCalledWith(expectedEventName, expect.any(Function)); + expect(frontendFs.read).toHaveBeenCalledTimes(1); + expect(frontendFs.read).toHaveBeenCalledWith(`./${sut.options.template}`); + expect(frontendFs.write).toHaveBeenCalledTimes(1); + expect(frontendFs.write).toHaveBeenCalledWith(sut.options.file, expect.any(String)); + expect(frontendFs.delete).toHaveBeenCalledTimes(1); + expect(frontendFs.delete).toHaveBeenCalledWith(`./${sut.options.template}`); + expect(appLogger.success).toHaveBeenCalledTimes(1); + expect(appLogger.success).toHaveBeenCalledWith(expect.any(String)); + expect(appLogger.info).toHaveBeenCalledTimes(1); + expect(appLogger.info).toHaveBeenCalledWith(expect.any(String)); + expect(wootilsMock.mocks.deferredResolve).toHaveBeenCalledTimes(1); + expect(app.try).toHaveBeenCalledTimes(0); }); }); }); From 121f3966ae7f521c58bc5bc9ea4a8eff5a9e9e73 Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 05:17:32 -0300 Subject: [PATCH 64/78] docs(project): update the documentation for the HTML Generator --- documents/services.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/documents/services.md b/documents/services.md index be34d2ac..36f17888 100644 --- a/documents/services.md +++ b/documents/services.md @@ -332,12 +332,21 @@ class App extends Jimpex { } ``` -The service, after registering, it also hooks itself to the app event that gets fired when it starts so it can create the file automatically. +The service, after registering, it hooks itself to the app event that gets fired when it starts, so it can create the file automatically. Now, this service has a few default options, so instead of explaining which are, we'll see each option on detail: ```js { + // The name the service will have in the container; in case you need more than one. + serviceName: 'htmlGenerator', + + // The name of a service from will it obtain the values for the template. When + // instantiated, it will look for it on the container, and if is not avaiable, + // it will just ignore it and use `configurationKeys`. + // You can completely by setting the value to `null`. + valuesService: 'htmlGeneratorValues', + // The name of the file it should use as template. template: 'index.tpl.html', @@ -362,8 +371,6 @@ Now, this service has a few default options, so instead of explaining which are, } ``` -It also supports a custom service with a `getValues` method to obtain the information to inject instead of taking it from the configuration. - To modify the options, you just need to use provider as a function: ```js @@ -390,8 +397,6 @@ class App extends Jimpex { } ``` -The first parameter is the name of the service and the second the options to customize it. In case you want to use another service to get the values, you can send the name of that service as the third parameter. - ## HTTP A set of utilities to work with HTTP requests and responses. From eab0f6e1759347b77d05c44f6483c309f1c0f03d Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 05:38:15 -0300 Subject: [PATCH 65/78] refactor(services/http/apiClient): use an options object on the provider creator --- src/services/http/apiClient.js | 37 +++++++++++------ tests/services/http/apiClient.test.js | 60 ++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/services/http/apiClient.js b/src/services/http/apiClient.js index 50d0b587..16c53bdd 100644 --- a/src/services/http/apiClient.js +++ b/src/services/http/apiClient.js @@ -1,6 +1,17 @@ const ObjectUtils = require('wootils/shared/objectUtils'); const APIClientBase = require('wootils/shared/apiClient'); const { providerCreator } = require('../../utils/wrappers'); +/** + * @typdef {Object} APIClientProviderOptions + * @description The options to customize how the service gets registered. + * @property {string} [serviceName='apiClient'] The name of the service that will be registered + * into the app. + * @property {string} [configurationSetting='api'] The name of the configuration setting that has + * the API information. + * @property {Class} [clientClass=APIClient] The class the service will instantiate. It has + * to extend from {@link APIClient}. + */ + /** * An API client for the app to use. What makes this service special is that its that it formats * the received errors using the `AppError` service class and as fetch function it uses the @@ -84,19 +95,21 @@ class APIClient extends APIClientBase { * An API Client service to make requests to an API using endpoints defined on the app * configuration. * @type {ProviderCreator} - * @param {string} [name='apiClient'] The name of the service that will be registered into - * the app. - * @param {string} [configurationKey='api'] The name of the app configuration setting that has the - * API information. - * @param {Class} [ClientClass=APIClient] The Class the service will instantiate. + * @param {APIClientProviderOptions} [options] The options to customize how the service gets + * registered. */ -const apiClient = providerCreator(( - name = 'apiClient', - configurationKey = 'api', - ClientClass = APIClient -) => (app) => { - app.set(name, () => new ClientClass( - app.get('appConfiguration').get(configurationKey), +const apiClient = providerCreator((options = {}) => (app) => { + const defaultName = 'apiClient'; + const { + serviceName = defaultName, + clientClass: ClientClass = APIClient, + } = options; + let { configurationSetting } = options; + if (!configurationSetting) { + configurationSetting = serviceName === defaultName ? 'api' : serviceName; + } + app.set(serviceName, () => new ClientClass( + app.get('appConfiguration').get(configurationSetting), app.get('http'), app.get('HTTPError') )); diff --git a/tests/services/http/apiClient.test.js b/tests/services/http/apiClient.test.js index ac6a4f3a..b3ce8596 100644 --- a/tests/services/http/apiClient.test.js +++ b/tests/services/http/apiClient.test.js @@ -95,7 +95,7 @@ describe('services/http:client', () => { let result = null; // When sut = new APIClient(apiConfig, http, 'HTTPError'); - result = sut.getErrorMessageFromResponse({}, fallback) + result = sut.getErrorMessageFromResponse({}, fallback); // Then expect(result).toBe(fallback); }); @@ -164,13 +164,17 @@ describe('services/http:client', () => { get: jest.fn((service) => (services[service] || service)), }; const name = 'myAPI'; - const configurationKey = 'my-api'; - const ClientClass = APIClient; + const configurationSetting = 'my-api'; + const clientClass = APIClient; let sut = null; let serviceName = null; let serviceFn = null; // When - apiClient(name, configurationKey, ClientClass).register(app); + apiClient({ + serviceName: name, + configurationSetting, + clientClass, + }).register(app); [[serviceName, serviceFn]] = app.set.mock.calls; sut = serviceFn(); // Then @@ -182,6 +186,52 @@ describe('services/http:client', () => { expect(sut.fetchClient).toBe(http.fetch); expect(serviceName).toBe(name); expect(appConfiguration.get).toHaveBeenCalledTimes(1); - expect(appConfiguration.get).toHaveBeenCalledWith(configurationKey); + expect(appConfiguration.get).toHaveBeenCalledWith(configurationSetting); + }); + + it('should use the same name as the service when is different from the default', () => { + // Given + const appConfiguration = { + apiConfig: { + url: 'my-api', + endpoints: { + info: 'api-info', + }, + }, + get: jest.fn(() => appConfiguration.apiConfig), + }; + const http = { + fetch: 'fetch', + }; + const services = { + appConfiguration, + http, + }; + const app = { + set: jest.fn(), + get: jest.fn((service) => (services[service] || service)), + }; + const name = 'myAPI'; + const clientClass = APIClient; + let sut = null; + let serviceName = null; + let serviceFn = null; + // When + apiClient({ + serviceName: name, + clientClass, + }).register(app); + [[serviceName, serviceFn]] = app.set.mock.calls; + sut = serviceFn(); + // Then + expect(sut).toBeInstanceOf(APIClientBase); + expect(sut).toBeInstanceOf(APIClient); + expect(sut.apiConfig).toEqual(appConfiguration.apiConfig); + expect(sut.url).toBe(appConfiguration.apiConfig.url); + expect(sut.endpoints).toEqual(appConfiguration.apiConfig.endpoints); + expect(sut.fetchClient).toBe(http.fetch); + expect(serviceName).toBe(name); + expect(appConfiguration.get).toHaveBeenCalledTimes(1); + expect(appConfiguration.get).toHaveBeenCalledWith(name); }); }); From 8f6ff113a63bd64d60d0398ff08d34425fcb6770 Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 05:38:55 -0300 Subject: [PATCH 66/78] docs(project): update the documentation for the API client --- documents/services.md | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/documents/services.md b/documents/services.md index 36f17888..0f50fe2c 100644 --- a/documents/services.md +++ b/documents/services.md @@ -32,7 +32,25 @@ class App extends Jimpex { } ``` -By default, the service is registered with the name `apiClient`, the API entry point is taken from the configuration setting `api.url` and the endpoints from `api.endpoints`, but you can use it as a function to modify those options: +The service has a few options that can be customized: + +```js +{ + // The name the service will have in the container; in case you need more than one. + serviceName: 'apiClient', + + // The name of the configuration setting that will contain the API `url` and `endpoints`. + // If this is not customized, but the `serviceName` is, this value will be set to the + // same as the `serviceName`. + configurationSetting: 'api', + + // The class the service will instantiate. This is in case you end up extending the + // base one in order to add custommethods. + clientClass: APIClient, +} +``` + +You can use the provider as a function to modify the options: ```js const { @@ -50,16 +68,14 @@ class App extends Jimpex { this.register(appError); // Register the client - this.register(apiClient( - 'myCustomAPIService', - 'myapi' - ); + this.register(apiClient({ + serviceName: 'myCustomAPIService', + configurationSetting: 'myapi', + }); } } ``` -The first parameter is the name used to register the server and the second one is the setting key that has a `url` and an `endpoints` dictionary. - ## App Error A very simple subclass of `Error` but with support for context information. It can be used to customize the error handler responses. From 2030722278490858f88813ea3d87b8b2eff925a2 Mon Sep 17 00:00:00 2001 From: homer0 Date: Sun, 23 Jun 2019 06:59:13 -0300 Subject: [PATCH 67/78] fix(project): export the new 'providers' wrapper --- src/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.js b/src/index.js index 44743201..0998654d 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ const services = require('./services'); const { provider, providerCreator, + providers, controller, controllerCreator, middleware, @@ -20,6 +21,7 @@ module.exports = { middlewares, provider, providerCreator, + providers, services, Jimpex, }; From e45907357b9405ca2811c317f8131340d134d688 Mon Sep 17 00:00:00 2001 From: homer0 Date: Mon, 24 Jun 2019 00:59:59 -0300 Subject: [PATCH 68/78] chore(project): update wootils --- package.json | 2 +- yarn.lock | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index fd0777d9..5ada2b8c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "author": "Leonardo Apiwan (@homer0) ", "license": "MIT", "dependencies": { - "wootils": "^2.3.0", + "wootils": "^2.4.0", "jimple": "^1.5.0", "express": "^4.17.1", "body-parser": "^1.19.0", diff --git a/yarn.lock b/yarn.lock index 650e70ec..d987225b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2512,7 +2512,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@~3.0.2: +extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -6161,12 +6161,13 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" -wootils@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/wootils/-/wootils-2.3.0.tgz#b628fd4b0b7644ce116456d2f3c223cfddc5e7c5" - integrity sha512-VtUPHMR+0NM/+rEtGVZjCbY5ajfI0BpBqpCkepFCqcxvMq9YB/coZX5VAdR/aJHup7HvfROLoABtDYRAe2F1jw== +wootils@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/wootils/-/wootils-2.4.0.tgz#03235f121cd2b2d1af235c4495f87f2bc20d6b86" + integrity sha512-QNpDy2QnQyloB+n/gPX/1pJpFYeSNvG0mVsMtL4w0L3Im9OQaRb9v2AepAUN7d+9bB33s7i4NYtklZO6QtXeaw== dependencies: colors "1.3.3" + extend "^3.0.2" fs-extra "8.0.1" jimple "1.5.0" statuses "1.5.0" From df65ad460da107779c9332bf25b2b176149739e5 Mon Sep 17 00:00:00 2001 From: homer0 Date: Mon, 24 Jun 2019 01:10:14 -0300 Subject: [PATCH 69/78] refactor(project): use the paths for checking the debug flags --- src/controllers/common/configuration.js | 3 +-- src/middlewares/common/errorHandler.js | 3 +-- src/services/http/http.js | 3 +-- tests/controllers/common/configuration.test.js | 8 ++------ tests/middlewares/common/errorHandler.test.js | 16 ++++++---------- tests/services/http/http.test.js | 6 ++---- 6 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/controllers/common/configuration.js b/src/controllers/common/configuration.js index 3ba4bc62..746ebca7 100644 --- a/src/controllers/common/configuration.js +++ b/src/controllers/common/configuration.js @@ -72,8 +72,7 @@ class ConfigurationController { const configurationController = controller((app) => { const routes = []; const appConfiguration = app.get('appConfiguration'); - const debugging = appConfiguration.get('debug'); - if (debugging && debugging.configurationController === true) { + if (appConfiguration.get('debug.configurationController') === true) { const router = app.get('router'); const ctrl = new ConfigurationController( appConfiguration, diff --git a/src/middlewares/common/errorHandler.js b/src/middlewares/common/errorHandler.js index 9fb63cfd..ba7b7a36 100644 --- a/src/middlewares/common/errorHandler.js +++ b/src/middlewares/common/errorHandler.js @@ -151,8 +151,7 @@ class ErrorHandler { * @param {ErrorHandlerOptions} [options] Custom options to modify the middleware behavior. */ const errorHandler = middlewareCreator((options) => (app) => { - const debugging = app.get('appConfiguration').get('debug'); - const showErrors = debugging && debugging.showErrors; + const showErrors = app.get('appConfiguration').get('debug.showErrors') === true; return new ErrorHandler( app.get('appLogger'), app.get('responsesBuilder'), diff --git a/src/services/http/http.js b/src/services/http/http.js index a8c1a64d..1faf0d75 100644 --- a/src/services/http/http.js +++ b/src/services/http/http.js @@ -209,8 +209,7 @@ class HTTP { */ const http = provider((app) => { app.set('http', () => { - const debugging = app.get('appConfiguration').get('debug'); - const logRequests = !!(debugging && debugging.logRequests === true); + const logRequests = app.get('appConfiguration').get('debug.logRequests') === true; return new HTTP(logRequests, app.get('appLogger')); }); }); diff --git a/tests/controllers/common/configuration.test.js b/tests/controllers/common/configuration.test.js index a10c62ce..7408b7ff 100644 --- a/tests/controllers/common/configuration.test.js +++ b/tests/controllers/common/configuration.test.js @@ -209,9 +209,7 @@ describe('controllers/common:configuration', () => { it('should return its routes when the debug `configurationController` flag is `true`', () => { // Given const appConfiguration = { - debug: { - configurationController: true, - }, + 'debug.configurationController': true, get: jest.fn((prop) => appConfiguration[prop]), }; const services = { @@ -243,9 +241,7 @@ describe('controllers/common:configuration', () => { it('shouldn\'t return its routes when the debug `configurationController` flag is `false`', () => { // Given const appConfiguration = { - debug: { - configurationController: false, - }, + 'debug.configurationController': false, get: jest.fn((prop) => appConfiguration[prop]), }; const services = { diff --git a/tests/middlewares/common/errorHandler.test.js b/tests/middlewares/common/errorHandler.test.js index 7788dd7b..7eee235d 100644 --- a/tests/middlewares/common/errorHandler.test.js +++ b/tests/middlewares/common/errorHandler.test.js @@ -251,10 +251,8 @@ describe('middlewares/common:errorHandler', () => { it('should include a middleware shorthand to return its function', () => { // Given const appConfiguration = { - debug: { - showErrors: true, - }, - get: jest.fn(() => appConfiguration.debug), + 'debug.showErrors': true, + get: jest.fn((prop) => appConfiguration[prop]), }; const services = { appConfiguration, @@ -285,16 +283,14 @@ describe('middlewares/common:errorHandler', () => { expect(app.get).toHaveBeenCalledWith(service); }); expect(appConfiguration.get).toHaveBeenCalledTimes(1); - expect(appConfiguration.get).toHaveBeenCalledWith('debug'); + expect(appConfiguration.get).toHaveBeenCalledWith('debug.showErrors'); }); it('should include a middleware creator shorthand to modify its options', () => { // Given const appConfiguration = { - debug: { - showErrors: true, - }, - get: jest.fn(() => appConfiguration.debug), + 'debug.showErrors': true, + get: jest.fn((prop) => appConfiguration[prop]), }; const services = { appConfiguration, @@ -325,6 +321,6 @@ describe('middlewares/common:errorHandler', () => { expect(app.get).toHaveBeenCalledWith(service); }); expect(appConfiguration.get).toHaveBeenCalledTimes(1); - expect(appConfiguration.get).toHaveBeenCalledWith('debug'); + expect(appConfiguration.get).toHaveBeenCalledWith('debug.showErrors'); }); }); diff --git a/tests/services/http/http.test.js b/tests/services/http/http.test.js index c576b0e6..de91951b 100644 --- a/tests/services/http/http.test.js +++ b/tests/services/http/http.test.js @@ -368,10 +368,8 @@ describe('services/http:http', () => { it('should turn on the requests log when registered if the configuration flag is `true`', () => { // Given const appConfiguration = { - debug: { - logRequests: true, - }, - get: jest.fn(() => appConfiguration.debug), + 'debug.logRequests': true, + get: jest.fn((prop) => appConfiguration[prop]), }; const services = { appConfiguration, From 75cb46e4d702e98036fc87f3f7c769e4919fc6bb Mon Sep 17 00:00:00 2001 From: homer0 Date: Mon, 24 Jun 2019 12:50:04 -0300 Subject: [PATCH 70/78] feat(services/http/http): normalize headers' names --- src/services/http/http.js | 28 ++++++++++++++++++--- tests/services/http/http.test.js | 42 ++++++++++++++------------------ 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/services/http/http.js b/src/services/http/http.js index 1faf0d75..120bb3c0 100644 --- a/src/services/http/http.js +++ b/src/services/http/http.js @@ -56,8 +56,8 @@ class HTTP { req.connection.socket.remoteAddress; } /** - * Get a dictionary with all the custom headers a request has. By custom header it means all the - * headers which name start with `x-`. + * Creates a dictionary with all the custom headers a request has. By custom header it means all + * the headers which name start with `x-`. * This method doesn't copy `x-forwarded-for` as the `fetch` method generates it by calling * `getIPFromRequest`. * @param {ExpressRequest} req The request from which it will try to get the headers. @@ -73,6 +73,28 @@ class HTTP { return headers; } + /** + * It takes a dictionary of headers and normalize the names so each word will start with an + * upper case character. This is helpful in case you added custom headers and didn't care about + * the casing, or when copying headers from a server request, in which case they are all + * tranformed to lower case. + * @param {Object} headers The dictionary of headers to normalize. + * @return {Object} + */ + normalizeHeaders(headers) { + return Object.keys(headers).reduce( + (newHeaders, name) => { + const newName = name + .split('-') + .map((part) => part.replace(/^(\w)/, (ignore, letter) => letter.toUpperCase())) + .join('-'); + return Object.assign({}, newHeaders, { + [newName]: headers[name], + }); + }, + {} + ); + } /** * Make a request. * @param {string} url The request URL. @@ -122,7 +144,7 @@ class HTTP { * to avoid sending an empty object. */ if (Object.keys(headers).length) { - fetchOptions.headers = headers; + fetchOptions.headers = this.normalizeHeaders(headers); } // If the `logRequests` flag is `true`, call the method to log the request. if (this._logRequests) { diff --git a/tests/services/http/http.test.js b/tests/services/http/http.test.js index de91951b..70bc53a0 100644 --- a/tests/services/http/http.test.js +++ b/tests/services/http/http.test.js @@ -152,9 +152,6 @@ describe('services/http:http', () => { expect(fetch).toHaveBeenCalledWith(url, { method: 'GET', }); - }) - .catch(() => { - expect(true).toBeFalse(); }); }); @@ -175,9 +172,6 @@ describe('services/http:http', () => { expect(result).toBe(response); expect(fetch).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledWith(url, { method }); - }) - .catch(() => { - expect(true).toBeFalse(); }); }); @@ -204,9 +198,6 @@ describe('services/http:http', () => { expect(fetch).toHaveBeenCalledWith(`${url}?${qsVariable}=${qsValue}`, { method: 'GET', }); - }) - .catch(() => { - expect(true).toBeFalse(); }); }); @@ -233,9 +224,6 @@ describe('services/http:http', () => { method, body, }); - }) - .catch(() => { - expect(true).toBeFalse(); }); }); @@ -262,11 +250,11 @@ describe('services/http:http', () => { expect(fetch).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledWith(url, { method: 'GET', - headers: request.headers, + headers: { + 'X-Forwarded-For': request.headers['x-forwarded-for'], + 'X-Custom-Header': request.headers['x-custom-header'], + }, }); - }) - .catch(() => { - expect(true).toBeFalse(); }); }); @@ -304,9 +292,6 @@ describe('services/http:http', () => { `RESPONSE> ${url}`, `RESPONSE> status: ${response.status}`, ]); - }) - .catch(() => { - expect(true).toBeFalse(); }); }); @@ -327,6 +312,10 @@ describe('services/http:http', () => { // The headers object is actually a custom iterable headers: ['custom-header-value', 'another-custom-header-value'], }; + const headersFixedNames = { + 'x-forwarded-for': 'X-Forwarded-For', + 'x-custom-header': 'X-Custom-Header', + }; fetch.mockImplementationOnce(() => Promise.resolve(response)); const logRequests = true; const appLogger = { @@ -343,14 +332,22 @@ describe('services/http:http', () => { expect(fetch).toHaveBeenCalledWith(url, { method, body, - headers: request.headers, + headers: Object.keys(request.headers).reduce( + (newHeaders, name) => Object.assign({}, newHeaders, { + [headersFixedNames[name]]: request.headers[name], + }), + {} + ), }); expect(appLogger.info).toHaveBeenCalledTimes(['request', 'response'].length); expect(appLogger.info).toHaveBeenCalledWith([ '--->>', `REQUEST> ${method} ${url}`, ...Object.keys(request.headers) - .map((headerName) => `REQUEST> ${headerName}: ${request.headers[headerName]}`), + .map((headerName) => ( + `REQUEST> ${headersFixedNames[headerName]}: ` + + `${request.headers[headerName]}` + )), `REQUEST> body: "${body}"`, ]); expect(appLogger.info).toHaveBeenCalledWith([ @@ -359,9 +356,6 @@ describe('services/http:http', () => { `RESPONSE> status: ${response.status}`, ...response.headers.map((value, index) => `RESPONSE> ${index}: ${value}`), ]); - }) - .catch(() => { - expect(true).toBeFalse(); }); }); From 1239738134736c8e9c8591025b7d03ab862327d5 Mon Sep 17 00:00:00 2001 From: homer0 Date: Mon, 24 Jun 2019 14:47:32 -0300 Subject: [PATCH 71/78] feat(utils): add a set of utility functions --- src/index.js | 2 + src/utils/functions.js | 64 ++++++++++++++ tests/utils/functions.test.js | 161 ++++++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 src/utils/functions.js create mode 100644 tests/utils/functions.test.js diff --git a/src/index.js b/src/index.js index 0713f0b9..eed989ed 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ const { middleware, middlewareCreator, } = require('./utils/wrappers'); +const utils = require('./utils/functions'); module.exports = { controller, @@ -25,5 +26,6 @@ module.exports = { providers, services, eventNames, + utils, Jimpex, }; diff --git a/src/utils/functions.js b/src/utils/functions.js new file mode 100644 index 00000000..4f9e0a9a --- /dev/null +++ b/src/utils/functions.js @@ -0,0 +1,64 @@ +/** + * Removes any leading slash from a URL. + * @param {string} url The URL to format. + * @return {string} + */ +const removeLeadingSlash = (url) => url.replace(/^\/+/, ''); +/** + * Removes any trailing slash from a URL. + * @param {string} url The URL to format. + * @return {string} + */ +const removeTrailingSlash = (url) => url.replace(/\/+$/, ''); +/** + * Remove any leading and trailing slash from a URL. + * @param {string} url The URL to format. + * @param {boolean} [leading=true] Whether or not to remove any leading slash. + * @param {boolean} [trailing=true] Whether or not to remove the trailing slash. + * @return {string} + */ +const removeSlashes = (url, leading = true, trailing = true) => { + const newUrl = leading ? removeLeadingSlash(url) : url; + return trailing ? removeTrailingSlash(newUrl) : newUrl; +}; +/** + * Escapes a string to be used on `new RegExp(...)`. + * @param {string} text The text to escape. + * @return {string} + */ +const escapeForRegExp = (text) => text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); +/** + * Given a server route definition, this function creates a regular expression to match + * it: The expression replaces the routes parameters with placeholders so it can be compared + * with real routes. + * @param {string} route The route from which the expression will be created. + * @param {boolean} [leadingSlash=true] Whether or not the expression should match a leading + * slash. + * @param {boolean} [trailingSlash=false] Whether or not the expression should match a trailing + * slash. The reason this is `false` by default is because + * these expressions are often used to match against + * incoming requests, and they don't have a trailing slash. + * @return {RegExp} + */ +const createRouteExpression = (route, leadingSlash = true, trailingSlash = false) => { + let expression = removeSlashes(route) + .split('/') + .map((part) => (part.startsWith(':') ? '(?:([^\\/]+?))' : escapeForRegExp(part))) + .join('\\/'); + if (leadingSlash) { + expression = `\\/${expression}`; + } + if (trailingSlash) { + expression = `${expression}\\/`; + } + + return new RegExp(expression); +}; + +module.exports = { + createRouteExpression, + escapeForRegExp, + removeLeadingSlash, + removeSlashes, + removeTrailingSlash, +}; diff --git a/tests/utils/functions.test.js b/tests/utils/functions.test.js new file mode 100644 index 00000000..bad0ec62 --- /dev/null +++ b/tests/utils/functions.test.js @@ -0,0 +1,161 @@ +jest.unmock('/src/utils/functions'); + +require('jasmine-expect'); +const { + createRouteExpression, + escapeForRegExp, + removeLeadingSlash, + removeSlashes, + removeTrailingSlash, +} = require('/src/utils/functions'); + +describe('utils/functions', () => { + describe('removeLeadingSlash', () => { + it('should remove the leading slash from a URL', () => { + // Given + const url = '/my/url'; + let result = null; + // When + result = removeLeadingSlash(url); + // Then + expect(result).toBe(url.substr(1)); + }); + + it('should remove multiple leading slashes from a URL', () => { + // Given + const url = '///my/url'; + let result = null; + // When + result = removeLeadingSlash(url); + // Then + expect(result).toBe(url.substr(3)); + }); + + it('shouldn\'t modify a URL that doesn\'t start with a slash', () => { + // Given + const url = 'my/url'; + let result = null; + // When + result = removeLeadingSlash(url); + // Then + expect(result).toBe(url); + }); + }); + + describe('removeTrailingSlash', () => { + it('should remove the leading slash from a URL', () => { + // Given + const url = 'my/url/'; + let result = null; + // When + result = removeTrailingSlash(url); + // Then + expect(result).toBe(url.substr(0, url.length - 1)); + }); + + it('should remove multiple trailing slashes from a URL', () => { + // Given + const url = 'my/url///'; + let result = null; + // When + result = removeTrailingSlash(url); + // Then + expect(result).toBe(url.substr(0, url.length - 3)); + }); + + it('shouldn\'t modify a URL that doesn\'t start with a slash', () => { + // Given + const url = 'my/url'; + let result = null; + // When + result = removeTrailingSlash(url); + // Then + expect(result).toBe(url); + }); + }); + + describe('removeSlashes', () => { + it('should remove both leading and trailing slashes from a URL', () => { + // Given + const url = '/my/url/'; + let result = null; + // When + result = removeSlashes(url); + // Then + expect(result).toBe(url.substr(1, url.length - 2)); + }); + + it('should remove the trailing slash from a URL', () => { + // Given + const url = '/my/url/'; + let result = null; + // When + result = removeSlashes(url, false); + // Then + expect(result).toBe(url.substr(0, url.length - 1)); + }); + + it('shouldn\'t remove slashes from a URL', () => { + // Given + const url = '/my/url/'; + let result = null; + // When + result = removeSlashes(url, false, false); + // Then + expect(result).toBe(url); + }); + }); + + describe('escapeForRegExp', () => { + it('should escape a text to be used inside a RegExp', () => { + // Given + const text = 'hello {(world)}'; + let result = null; + // When + result = escapeForRegExp(text); + // Then + expect(result).toBe('hello\\ \\{\\(world\\)\\}'); + }); + }); + + describe('createRouteExpression', () => { + it('should create a expression that matches a route', () => { + // Given + const definition = '/my-route/:my-param/something/:else/end'; + const route = '/my-route/something/something/something/end'; + let expression = null; + let result = null; + // When + expression = createRouteExpression(definition); + result = expression.test(route); + // Then + expect(result).toBeTrue(); + }); + + it('should create a expression that matches the route with a trailing slash', () => { + // Given + const definition = '/my-route/:my-param/something/:else/end'; + const route = 'my-route/something/something/something/end/'; + let expression = null; + let result = null; + // When + expression = createRouteExpression(definition, false, true); + result = expression.test(route); + // Then + expect(result).toBeTrue(); + }); + + it('should create a expression that doesn\'t matches a route', () => { + // Given + const definition = '/my-route/:my-param/something/:else/end'; + const route = '/my-route/something/something/something'; + let expression = null; + let result = null; + // When + expression = createRouteExpression(definition); + result = expression.test(route); + // Then + expect(result).toBeFalse(); + }); + }); +}); From 1971dce9dbc12c807d8992831d1a158d6ff81b25 Mon Sep 17 00:00:00 2001 From: homer0 Date: Mon, 24 Jun 2019 14:56:15 -0300 Subject: [PATCH 72/78] refactor(controllers/common/statics): use the new util functions --- src/controllers/common/statics.js | 25 +++--------------------- tests/controllers/common/statics.test.js | 1 + 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/src/controllers/common/statics.js b/src/controllers/common/statics.js index b4af09f7..7c4367f5 100644 --- a/src/controllers/common/statics.js +++ b/src/controllers/common/statics.js @@ -2,6 +2,7 @@ const path = require('path'); const ObjectUtils = require('wootils/shared/objectUtils'); const mime = require('mime'); const { controllerCreator } = require('../../utils/wrappers'); +const { removeSlashes } = require('../../utils/functions'); /** * @typdef {Object} StaticsControllerFile @@ -175,7 +176,7 @@ class StaticsController { */ _createFiles() { const { files, paths } = this._options; - const routePath = this._removeTrailingSlash(paths.route); + const routePath = removeSlashes(paths.route, false, true); return files.reduce( (formatted, file) => { let source; @@ -189,7 +190,7 @@ class StaticsController { } source = path.join(paths.source, source); - route = this._removeLeadingSlash(route); + route = removeSlashes(route, true, false); route = `${routePath}/${route}`; return Object.assign({}, formatted, { @@ -240,26 +241,6 @@ class StaticsController { this._sendFile(res, file.source, next); }; } - /** - * Helper method to remove the leading slash from a URL. - * @param {string} url The URL to format. - * @return {string} - * @access protected - * @ignore - */ - _removeLeadingSlash(url) { - return url.replace(/^\/+/, ''); - } - /** - * Helper method to remove the trailing slash from a URL. - * @param {string} url The URL to format. - * @return {string} - * @access protected - * @ignore - */ - _removeTrailingSlash(url) { - return url.replace(/\/+$/, ''); - } } /** * This controller allows you to serve specific files from any folder to any route without the diff --git a/tests/controllers/common/statics.test.js b/tests/controllers/common/statics.test.js index e5cd0bb9..74409a37 100644 --- a/tests/controllers/common/statics.test.js +++ b/tests/controllers/common/statics.test.js @@ -1,3 +1,4 @@ +jest.unmock('/src/utils/functions'); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/controllers/common/statics'); From 94c74cfaeb1a972a709e03e4a82a3f6c51f3322a Mon Sep 17 00:00:00 2001 From: homer0 Date: Mon, 24 Jun 2019 15:16:03 -0300 Subject: [PATCH 73/78] refactor(middlewares/html/fastHTML): use the new util functions --- src/middlewares/html/fastHTML.js | 30 +++---------------------- tests/middlewares/html/fastHTML.test.js | 1 + 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/src/middlewares/html/fastHTML.js b/src/middlewares/html/fastHTML.js index e0082bc8..50d6232b 100644 --- a/src/middlewares/html/fastHTML.js +++ b/src/middlewares/html/fastHTML.js @@ -2,6 +2,7 @@ const mime = require('mime'); const ObjectUtils = require('wootils/shared/objectUtils'); const { eventNames } = require('../../constants'); const { middlewareCreator } = require('../../utils/wrappers'); +const { createRouteExpression, removeSlashes } = require('../../utils/functions'); /** * @typedef {Object} FastHTMLOptions @@ -185,40 +186,15 @@ class FastHTML { // Re generate the list of expressions... this._routeExpressions = routes // Remove leading and trailing slashes. - .map((route) => route.replace(/^\/+/, '').replace(/\/+$/, '').trim()) + .map((route) => removeSlashes(route).trim()) // Filter empty routes (in case they were for `/`). .filter((route) => route !== '') // Remove repeated routes. .reduce((unique, route) => (unique.includes(route) ? unique : [...unique, route]), []) // Generate regular expressions for each route. - .map((route) => this._getRouteExpression(route)); + .map((route) => createRouteExpression(route)); }); } - /** - * Generates a regular expression for a given route. - * @param {string} route The route from where the expression will be created. - * @return {RegExp} - * @access protected - * @ignore - */ - _getRouteExpression(route) { - const expression = route - // Separate each component of the route. - .split('/') - /** - * If the component is for a paramter, replace it with a expression to match anything; if not, - * escape it so it can be used on the final expression. - */ - .map((part) => ( - part.startsWith(':') ? - '(?:([^\\/]+?))' : - part.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') - )) - // Put everything back together. - .join('\\/'); - // Before returning it, add a leading slash, as `req.originalUrl` always have one. - return new RegExp(`\\/${expression}`); - } /** * Checks whether a route should be ignored or not. The method checks first against the `ignore` * option, and then against the controlled routes (if `useAppRoutes` is `false`, the list diff --git a/tests/middlewares/html/fastHTML.test.js b/tests/middlewares/html/fastHTML.test.js index 9eb8445a..d84208c5 100644 --- a/tests/middlewares/html/fastHTML.test.js +++ b/tests/middlewares/html/fastHTML.test.js @@ -1,3 +1,4 @@ +jest.unmock('/src/utils/functions'); jest.unmock('/src/utils/wrappers'); jest.unmock('/src/middlewares/html/fastHTML'); From fc5a6bbdbdfc2850e6cd46d36b6c5b53fb0c9eb3 Mon Sep 17 00:00:00 2001 From: homer0 Date: Tue, 25 Jun 2019 14:52:13 -0300 Subject: [PATCH 74/78] feat(controllers): add the gateway controller --- src/controllers/index.js | 2 + src/controllers/utils/gateway.js | 900 ++++++++++++++++ src/controllers/utils/index.js | 5 + src/services/http/apiClient.js | 27 +- tests/controllers/index.test.js | 1 + tests/controllers/utils/gateway.test.js | 1269 +++++++++++++++++++++++ 6 files changed, 2191 insertions(+), 13 deletions(-) create mode 100644 src/controllers/utils/gateway.js create mode 100644 src/controllers/utils/index.js create mode 100644 tests/controllers/utils/gateway.test.js diff --git a/src/controllers/index.js b/src/controllers/index.js index 8fbd164c..9acb0898 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -1,5 +1,7 @@ const common = require('./common'); +const utils = require('./utils'); module.exports = { common, + utils, }; diff --git a/src/controllers/utils/gateway.js b/src/controllers/utils/gateway.js new file mode 100644 index 00000000..bdccdade --- /dev/null +++ b/src/controllers/utils/gateway.js @@ -0,0 +1,900 @@ +const ObjectUtils = require('wootils/shared/objectUtils'); +const { removeSlashes, createRouteExpression } = require('../../utils/functions'); +const { controllerCreator } = require('../../utils/wrappers'); + +/** + * @typedef {Object} GatewayControllerRouteMethod + * @description This object represets an HTTP method for a route the controller will mount. + * @property {string} method The name of the HTTP method. + * @property {GatewayControllerEndpointInformation} endpoint The information for the endpoint + * responsible from creating the route. + * @ignore + */ + +/** + * @typedef {Object} GatewayControllerRoute + * @description This object contains the information for an specific route the controller will + * mount. + * @property {string} path The path the route will have. + * @property {Array} methods A list with all the methods the + * controller will use to mount the route. + * @ignore + */ + +/** + * @typedef {Object} GatewayConfigurationEndpoint + * @description Normally, you would define an endpoint with just a string path, but you can use + * this type of object to add extra settings. + * @property {string} path The path to the endpoint relative to the entry point. It can include + * placeholders for parameters like `/:parameter/`. + * @property {string} method The HTTP method for the endpoint. This will tell the gateway the + * type of route it should mount. If is not specified, it will use + * `all`. + */ + +/** + * @typedef {Object} GatewayConfigurationEndpoints + * @description A dictionary of endpoints or sub endpoints the gateway will use in order to mount + * routes. + * @property {string|GatewayConfigurationEndpoints|GatewayConfigurationEndpoint} [endpointName] + * It can be the path to an actual endpoint, a dictionary of sub endpoints, or a definition of + * an endpoint with settings ({@link GatewayConfigurationEndpoint}). + */ + +/** + * @typedef {Object} GatewayConfiguration + * @description This is a configuration object very similar to the one {@link APIClient} uses in + * order to configure the endpoints; the controller uses it to create the routes and + * to validate the HTTP methods. + * @property {string} url The entry point to the API the controller + * will make the requests to. + * @property {GatewayConfigurationEndpoints} gateway A dictionary with the endpoints the gateway + * will make available. + */ + +/** + * @typedef {Object} GatewayControllerHeadersOptions + * @description The options for how the gateway will handle the headers from the requests and the + * responses. + * @property {boolean} [useXForwardedFor=true] + * Whether or not to include the header with the request's IP address. + * @property {boolean} [copyCustomHeaders=true] + * Whether or not to copy all custom headers from the request. By custom header, it means all the + * headers which names start with `x-`. + * @property {Array} [copy=['authorization','content-type', 'referer', 'user-agent']] + * A list of "known" headers the gateway will try to copy from the incoming request. + * @property {Array} [remove=['server', 'x-powered-by']] + * A list of "known" headers the gateway will try to remove the response. + */ + +/** + * @typedef {Object} GatewayControllerOptions + * @description The options to configure how the gateway will manage the requests and the + * responses. + * @property {string} [root=''] + * This is really a helper for when the gateway is used with an API client. The idea is that, + * by default, the routes are mounted on the controller route, but with this option, you can + * specify another sub path. For example: The controller is mounted on `/routes`, if you set + * `root` to `gateway`, all the routes will be on `/routes/gateway`. + * This become important (and useful) when you get the API client configuration (with + * `endpointsForAPIClient`): The `url` will be the controller route, but all the endpoints will + * be modified and prefixed with the `root`. + * @property {string} [configurationSetting='api'] + * This is another option for when the gateway is used with an API client. When calling + * `endpointsForAPIClient`, all the endpoints will be wrapped inside an object named after this + * option. For example: `{ url: '...', endpoints: { api: { ... } } }` + * @property {GatewayControllerHeadersOptions} [headers] + * The options for how the gateway will handle the headers from the requests and the responses. + */ + +/** + * @typedef {Object} GatewayControllerCreatorOptions + * @description This are the options sent to the controller creator that instantiates + * {@link GatewayController}. They're basically the same as + * {@link GatewayControllerOptions} but with a couple of extra ones. + * @param {string} [serviceName='apiGeteway'] The name of the creator will use to + * register the controller in the container. + * No, this is not a typo. The creator will + * register the controller so other + * services can access the + * `endpointsForAPIClient` getter. The + * service will be available after the app + * routes are mounted. + * If this is overwritten, the creator will + * ensure that the name ends with `Gateway`; + * and if overwritten, but it doesn't + * include `Gateway` at the end, and no + * `configurationSetting` was defined, the + * creator will use the custom name + * (without `Gatway`) for + * `configurationSetting`. + * @param {string} [helperServiceName='apiGatewayHelper'] The name of the helper service the + * creator will try to obtain from the + * container. If `serviceName` is + * overwritten, the default for this will + * be `${serviceName}Helper`. + * @param {string} [configurationSetting='api'] The name of the configuration setting + * where the gateway configuration is + * stored. If not overwritten, check the + * description of `serviceName` to + * understand which will be its default + * value. + * @param {Class} [gatewayClass=GatewayController] The class the creator will instantiate. + * Similar to {@link APIClient}, this + * allows for extra customization in cases + * you may need multiple gateways. + */ + +/** + * @typedef {Object} GatewayControllerRequest + * @description This is the information for a request the controller will make. + * @property {string} url The URL for the request. + * @property {HTTPFetchOptions} options The request options. + */ + +/** + * @typedef {Object} GatewayControllerEndpointInformation + * @description This is the information for an specific endpoint that the gateway may use to + * send to a helper method in order to give it context. + * @property {string} name The name of the endpoint, which is + * actually the path inside the gateway + * configuration's `gateway` property. + * @property {string|GatewayConfigurationEndpoint} settings The path for the endpoint, or the + * dictionary of settings. + */ + +/** + * @typedef {function} GatewayHelperServiceRequestReducer + * @description This is called in order to allow the helper to modify the information of a + * request that is about the fired. + * @param {GatewayControllerRequest} request The information for a request the + * controller will make. + * @param {GatewayControllerEndpointInformation} endpoint The information for the endpoint + * responsible of creating the route. + * @param {ExpressRequest} req The server's incoming request + * information. + * @param {ExpressResponse} res The server's response information. + * @param {ExpressNext} next The function to call the next + * middleware. + * @return {GatewayControllerRequest} + */ + +/** + * @typedef {function} GatewayHelperServiceResponseReducer + * @description This is called in order to allow the helper to modify the information of a + * response the gateway made. + * @param {Object} response The response generated by the fetch + * request. + * @param {GatewayControllerEndpointInformation} endpoint The information for the endpoint + * responsible of creating the route. + * @param {ExpressRequest} req The server's incoming request + * information. + * @param {ExpressResponse} res The server's response information. + * @param {ExpressNext} next The function to call the next + * middleware. + * @return {Object} + */ + +/** + * @typedef {function} GatewayHelperServiceStreamVerification + * @description This is called in order to allow the helper to decide whether a fetch request + * response should be added to the server's response stream. This will only be + * called if the helper also implements `handleEndpointResponse`. + * @param {Object} response The response generated by the fetch + * request. + * @param {GatewayControllerEndpointInformation} endpoint The information for the endpoint + * responsible of creating the route. + * @param {ExpressRequest} req The server's incoming request + * information. + * @param {ExpressResponse} res The server's response information. + * @param {ExpressNext} next The function to call the next + * middleware. + * @return {boolean} + */ + +/** + * @typedef {function} GatewayHelperServiceResponseHandler + * @description This is called in order for the helper to handle a response. This is only + * called if `shouldStreamEndpointResponse` returned `false`. + * @param {Object} response The response generated by the fetch + * request. + * @param {GatewayControllerEndpointInformation} endpoint The information for the endpoint + * responsible of creating the route. + * @param {ExpressRequest} req The server's incoming request + * information. + * @param {ExpressResponse} res The server's response information. + * @param {ExpressNext} next The function to call the next + * middleware. + */ + +/** + * @typedef {function} GatewayHelperServiceErrorHandler + * @description This is called in order for the helper to handle a fetch request error. + * @param {Error} response The fetch request error. + * @param {GatewayControllerEndpointInformation} endpoint The information for the endpoint + * responsible of creating the route. + * @param {ExpressRequest} req The server's incoming request + * information. + * @param {ExpressResponse} res The server's response information. + * @param {ExpressNext} next The function to call the next + * middleware. + */ + +/** + * @typedef {Object} GatewayHelperService + * @description A service that can have specific methods the gateway will call in order to + * modify requests, responses, handle errors, etc. + * @property {?GatewayHelperServiceRequestReducer} reduceEndpointRequest + * This is called in order to allow the helper to modify the information of a request that is + * about the fired. + * @property {?GatewayHelperServiceResponseReducer} reduceEndpointResponse + * This is called in order to allow the helper to modify the information of a response the + * gateway made. + * @property {?GatewayHelperServiceStreamVerification} shouldStreamEndpointResponse + * This is called in order to allow the helper to decide whether a fetch request response should + * be added to the server's response stream. This will only be called if the helper also + * implements `handleEndpointResponse`. + * @property {?GatewayHelperServiceResponseHandler} handleEndpointResponse + * This is called in order for the helper to handle a response. This is only called if + * `shouldStreamEndpointResponse` returned `false`. + * @property {?GatewayHelperServiceErrorHandler} handleEndpointError + * This is called in order for the helper to handle a fetch request error. + */ + +/** + * Geneters routes that will act as a gateway to an specific set of endpoints. + * @param {GatewayConfiguration} gatewayConfig This is a configuration object very similar to + * the one {@link APIClient} uses in order to + * configure the endpoints; the controller uses it + * to create the routes and to validate the HTTP + * methods. + * @param {string} route The route where the controller will be mounted. + * @param {HTTP} http To make the fetch requests on the routes. + * @param {GatewayControllerOptions} [options={}] The options to configure how the gateway will + * manage the requests and the responses. + * @param {?GatewayHelperService} [helper=null] A service that can have specific methods the + * gateway will call in order to modify requests, + * responses, handle errors, etc. + */ +class GatewayController { + constructor(gatewayConfig, route, http, options = {}, helperService = null) { + /** + * The options to configure how the gateway will manage the requests and the responses. + * @type {GatewayControllerOptions} + * @access protected + * @ignore + */ + this._options = this._normalizeOptions(ObjectUtils.merge( + { + root: '', + configurationSetting: 'api', + headers: { + useXForwardedFor: true, + copyCustomHeaders: true, + copy: options.headers && options.headers.copy ? options.headers.copy : [ + 'authorization', + 'content-type', + 'referer', + 'user-agent', + ], + remove: options.headers && options.headers.remove ? options.headers.remove : [ + 'server', + 'x-powered-by', + ], + }, + }, + options + )); + /** + * The configuration for the API the controller will make requests to. + * @type {GatewayConfiguration} + * @access protected + * @ignore + */ + this._gatewayConfig = Object.assign({}, gatewayConfig, { + url: removeSlashes(gatewayConfig.url, false, true), + }); + /** + * A local reference for the `http` service. + * @type {HTTP} + * @access protected + * @ignore + */ + this._http = http; + /** + * A list of the allowed HTTP methods an endpoint can have. + * @type {Array} + * @access protected + * @ignore + */ + this._allowedHTTPMethods = [ + 'get', + 'head', + 'post', + 'put', + 'delete', + 'connect', + 'options', + 'trace', + ]; + /** + * A flat dictionary of the gateway endpoints. The key is the path on the original + * dictionary (`this._gatewayConfig.gateway`) and the value is either the path (`string`) + * or the endpoint settings ({@link GatewayConfigurationEndpoint}). + * @type {Object} + * @access protected + * @ignore + */ + this._endpoints = this._getNormalizedEndpoints(); + /** + * The route where the controller is mounted. + * @type {string} + * @access protected + * @ignore + */ + this._route = removeSlashes(route); + /** + * A regular expression that will be used to remove the controller route from a request + * path. This will allow the main middleware to extract the path to where the request should + * be made. + * @type {RegExp} + * @access protected + * @ignore + */ + this._routeExpression = this._createRouteExpression(); + /** + * This is the list of routes the controller will define. + * @type {Array} + * @access protected + * @ignore + */ + this._routes = this._createEndpointRoutes(); + /** + * An {@link APIClient} configuration based on the controller routes. + * @type {APIClientConfiguration} + * @access protected + * @Ignore + */ + this._apiClientConfiguration = this._createAPIClientConfiguration(); + /** + * A service that can have specific methods the gateway will call in order to modify + * requests, responses, handle errors, etc. + * @type {?GatewayHelperService} + * @access protected + */ + this._helperService = helperService; + /** + * A dictionary of boolean flags that specify if a helper service has method. This is to + * avoid checking if the helper is defined and if "x method" is a function. If no helper + * was specified, the object will have all the flags set to `false`. + * @type {Object} + * @access protected + * @ignore + */ + this._helperServiceInfo = this._createHelperServiceInfo(); + } + /** + * Defines all the routes on a given router. + * @param {ExpressRouter} router The router where all the routes will be added. + * @param {Array} [middlewares=[]] A list of custom middlewares that will be added before + * the one that makes the request. + * @return {ExpressRouter} + */ + addRoutes(router, middlewares = []) { + this._routes.forEach((route) => route.methods.forEach((info) => this._addRoute( + router, + info.method, + route.path, + this._getMiddleware(info.endpoint), + middlewares + ))); + + return router; + } + /** + * An {@link APIClient} configuration based on the controller routes. + * @type {APIClientConfiguration} + */ + get endpointsForAPIClient() { + return this._apiClientConfiguration; + } + /** + * The configuration for the API the controller will make requests to. + * @type {GatewayConfiguration} + */ + get gatewayConfig() { + return this._gatewayConfig; + } + /** + * The options to configure how the gateway will manage the requests and the responses. + * @type {GatewayControllerOptions} + */ + get options() { + return this._options; + } + /** + * Normalizes the options recevied by the controller: + * - Removes any trailing and leading slashes from the `root` path, if defined. + * @param {GatewayControllerOptions} options The options to normalize. + * @return {GatewayControllerOptions} + * @access protected + * @ignore + */ + _normalizeOptions(options) { + let newOptions; + if (options.root) { + const root = removeSlashes(options.root).trim(); + newOptions = Object.assign({}, options, { root }); + } else { + newOptions = options; + } + + return newOptions; + } + /** + * Flattens all the endpoints from gateway configuration into a one level dictionary, where the + * key are the paths they used to have on the original configuration, and the values are the + * endpoints definitions. + * @return {Object} + * @access protected + * @ignore + */ + _getNormalizedEndpoints() { + return ObjectUtils.flat( + this._gatewayConfig.gateway, + '.', + '', + (ignore, value) => typeof value.path === 'undefined' + ); + } + /** + * Creates a regular expression the main middleware will later use in order to remove the + * controller route from the request url. That's needed in order to build the URL where the + * request will be made. + * @return {RegExp} + * @access protected + * @ignore + */ + _createRouteExpression() { + return createRouteExpression( + this._options.root ? `${this._route}/${this._options.root}` : this._route, + true, + true + ); + } + /** + * This is a helper method used in order to validate if an HTTP method can be used in order to + * define a route in the router. If the given method is not on the list of allowed methods, + * it will be "normalized" to `all`. It also transforms the method into lower case. + * @param {string} method The method to validate. + * @return {string} + * @access protected + * @ignore + */ + _normalizeHTTPMethod(method) { + const newMethod = method.toLowerCase(); + return this._allowedHTTPMethods.includes(newMethod) ? newMethod : 'all'; + } + /** + * Based on the information from the endpoints, this method will create the routes the + * controller will later add on a router. + * @return {Array} + * @throws {Error} If there's more than one endpoint using the same path with the same HTTP + * method. + * @access protected + * @ignore + */ + _createEndpointRoutes() { + const routes = {}; + Object.keys(this._endpoints).forEach((name) => { + const endpoint = this._endpoints[name]; + let endpointPath; + let endpointMethod; + if (typeof endpoint === 'string') { + endpointPath = endpoint; + endpointMethod = 'all'; + } else { + endpointPath = endpoint.path; + endpointMethod = endpoint.method ? + this._normalizeHTTPMethod(endpoint.method) : + 'all'; + } + + endpointPath = removeSlashes(endpointPath); + if (!routes[endpointPath]) { + routes[endpointPath] = { + path: endpointPath, + methods: {}, + }; + } + + if (routes[endpointPath].methods[endpointMethod]) { + const repeatedEndpoint = routes[endpointPath].methods[endpointMethod]; + throw new Error( + 'You can\'t have two gateway endpoints to the same path and with the same ' + + `HTTP method: '${repeatedEndpoint}' and '${name}'` + ); + } + + routes[endpointPath].methods[endpointMethod] = name; + }); + + return Object.keys(routes) + .map((endpointPath) => ({ + path: routes[endpointPath].path, + methods: Object.keys(routes[endpointPath].methods).map((methodName) => ({ + method: methodName, + endpoint: { + name: routes[endpointPath].methods[methodName], + settings: this._endpoints[routes[endpointPath].methods[methodName]], + }, + })), + })); + } + /** + * Based on the controller options and the gateway endpoints, this method will create an API + * client configuration that can be used to make requests to this controller. + * @return {APIClientConfiguration} + * @access protected + * @ignore + */ + _createAPIClientConfiguration() { + let endpoints; + const { root } = this._options; + if (root) { + endpoints = Object.keys(this._endpoints).reduce( + (acc, name) => { + const endpoint = this._endpoints[name]; + let newEndpoint; + if (typeof endpoint === 'string') { + newEndpoint = removeSlashes(endpoint); + newEndpoint = `${root}/${newEndpoint}`; + } else { + const endpointPath = removeSlashes(endpoint.path); + newEndpoint = Object.assign({}, endpoint, { + path: `${root}/${endpointPath}`, + }); + } + + return Object.assign({}, acc, { + [name]: newEndpoint, + }); + }, + {} + ); + } else { + endpoints = this._endpoints; + } + return { + url: `/${this._route}`, + endpoints: { + [this._options.configurationSetting]: ObjectUtils.unflat(endpoints), + }, + }; + } + /** + * Validates if a server helper exists and creates a dictionary with flags for all the methods + * a helper can have; this will allow other methods to check if the "helper method X" is + * available without having to check if the helper is defined and if "method X" is a function. + * @return {Object} + * @access protected + * @ignore + */ + _createHelperServiceInfo() { + const methods = [ + 'reduceEndpointRequest', + 'reduceEndpointResponse', + 'shouldStreamEndpointResponse', + 'handleEndpointResponse', + 'handleEndpointError', + ]; + let result; + if (this._helperService) { + result = methods.reduce( + (methodsDict, name) => Object.assign({}, methodsDict, { + [name]: typeof this._helperService[name] === 'function', + }), + {} + ); + } else { + result = methods.reduce( + (methodsDict, name) => Object.assign({}, methodsDict, { [name]: false }), + {} + ); + } + + return result; + } + /** + * Adds a route on a given router. + * @param {ExpressRouter} router The router where the route will be added. + * @param {string} method The HTTP method for the route. + * @param {string} route The path for the route. + * @param {ExpressMiddleware} endpointMiddleware The middleware that makes the request. + * @param {Array} middlewares Extra middlewares to add before the main one. + * + * @return {ExpressRouter} + * @access protected + * @ignore + */ + _addRoute(router, method, route, endpointMiddleware, middlewares) { + return router[method](route, [...middlewares, endpointMiddleware]); + } + /** + * Generates a middleware that will make a request and stream back the response. + * @param {GatewayControllerEndpointInformation} endpoint The information for the enpdoint for + * which the middleware is being created. + * @return {ExpressMiddleware} + * @access protected + * @ignore + */ + _getMiddleware(endpoint) { + return (req, res, next) => { + // Remove the controller route from the requested URL. + const reqPath = req.originalUrl.replace(this._routeExpression, ''); + // Define the request options. + const options = { + method: req.method.toUpperCase(), + headers: {}, + }; + // Copy the specified headers from the incoming request. + this._options.headers.copy.forEach((name) => { + if (req.headers[name]) { + options.headers[name] = req.headers[name]; + } + }); + // If enabled, copy the custom headers. + if (this._options.headers.copyCustomHeaders) { + options.headers = ObjectUtils.merge( + options.headers, + this._http.getCustomHeadersFromRequest(req) + ); + } + // If enabled, add the header with the request's IP. + if (this._options.headers.useXForwardedFor) { + options.headers['x-forwarded-for'] = this._http.getIPFromRequest(req); + } + /** + * If the request has a body and the method is not `GET`, stringify it and addit to + * the options. + */ + if (options.method !== 'GET' && typeof req.body === 'object') { + options.body = JSON.stringify(req.body); + // If there's no `content-type`, let's assume it's JSON. + if (!options.headers['content-type']) { + options.headers['content-type'] = 'application/json'; + } + } + // Reduce the request information. + const request = this._reduceEndpointRequest( + { + url: `${this._gatewayConfig.url}/${reqPath}`, + options, + }, + endpoint, + req, + res, + next + ); + // Make the fetch request. + return this._http.fetch(request.url, request.options) + .then((response) => { + // Reduce the response. + const newResponse = this._reduceEndpointResponse(response, endpoint, req, res, next); + // If the response should be sent down on the stream... + if (this._shouldStreamEndpointResponse(newResponse, endpoint, req, res, next)) { + // Update the server's response status. + res.status(newResponse.status); + // Copy the headers. + newResponse.headers.forEach((value, name) => { + if (!this._options.headers.remove.includes(name)) { + res.setHeader(name, value); + } + }); + // Pipe the server's response into the fetch response stream. + newResponse.body + .pipe(res) + .on('error', (error) => { + next(error); + }); + } else { + // Otherwise, let the helper handle the response. + this._handleEndpointResponse(newResponse, endpoint, req, res, next); + } + }) + .catch((error) => this._handleEndpointError(error, endpoint, req, res, next)); + }; + } + /** + * This method is called in order to reduce a fetch request information. It will check if a + * helper is defined and allow it to do it, or fallback and return the given information. + * @param {GatewayControllerRequest} request The information for a request the + * controller will make. + * @param {GatewayControllerEndpointInformation} endpoint The information for the endpoint + * responsible of creating the route. + * @param {ExpressRequest} req The server's incoming request + * information. + * @param {ExpressResponse} res The server's response information. + * @param {ExpressNext} next The function to call the next + * middleware. + * @return {GatewayControllerRequest} + * @access protected + * @ignore + */ + _reduceEndpointRequest(request, endpoint, req, res, next) { + return this._helperServiceInfo.reduceEndpointRequest ? + this._helperService.reduceEndpointRequest(request, endpoint, req, res, next) : + request; + } + /** + * This method is called in order to reduce a fetch response information. It will check if a + * helper is defined and allow it to do it, or fallback and return the given information. + * @param {Object} response The response generated by the fetch + * request. + * @param {GatewayControllerEndpointInformation} endpoint The information for the endpoint + * responsible of creating the route. + * @param {ExpressRequest} req The server's incoming request + * information. + * @param {ExpressResponse} res The server's response information. + * @param {ExpressNext} next The function to call the next + * middleware. + * @return {Object} + * @access protected + * @ignore + */ + + _reduceEndpointResponse(response, endpoint, req, res, next) { + return this._helperServiceInfo.reduceEndpointResponse ? + this._helperService.reduceEndpointResponse(response, endpoint, req, res, next) : + response; + } + /** + * This method is called in order to validate if the main middleware should pipe the fetch + * response stream into the server's response or if the helper will handle the response. + * This method will only call the helper if it implements both `shouldStreamEndpointResponse` + * and `handleEndpointResponse` + * @param {Object} response The response generated by the fetch + * request. + * @param {GatewayControllerEndpointInformation} endpoint The information for the endpoint + * responsible of creating the route. + * @param {ExpressRequest} req The server's incoming request + * information. + * @param {ExpressResponse} res The server's response information. + * @param {ExpressNext} next The function to call the next + * middleware. + * @return {boolean} + * @access protected + * @ignore + */ + _shouldStreamEndpointResponse(response, endpoint, req, res, next) { + return ( + this._helperServiceInfo.shouldStreamEndpointResponse && + this._helperServiceInfo.handleEndpointResponse + ) ? + this._helperService.shouldStreamEndpointResponse(response, endpoint, req, res, next) : + true; + } + /** + * This is called when the helper say that a fetch response shouldn't be sent, so the controller + * will allow it to handle the response by itself. + * @param {Object} response The response generated by the fetch + * request. + * @param {GatewayControllerEndpointInformation} endpoint The information for the endpoint + * responsible of creating the route. + * @param {ExpressRequest} req The server's incoming request + * information. + * @param {ExpressResponse} res The server's response information. + * @param {ExpressNext} next The function to call the next + * middleware. + * @return {*} + * @access protected + * @ignore + */ + _handleEndpointResponse(response, endpoint, req, res, next) { + return this._helperService.handleEndpointResponse(response, endpoint, req, res, next); + } + /** + * This method is called in order to handle a fetch request error. It will check if a + * helper is defined and allow it to do it, or fallback and call the next middleware. + * @param {Error} response The fetch request error. + * @param {GatewayControllerEndpointInformation} endpoint The information for the endpoint + * responsible of creating the route. + * @param {ExpressRequest} req The server's incoming request + * information. + * @param {ExpressResponse} res The server's response information. + * @param {ExpressNext} next The function to call the next + * middleware. + * @return {*} + * @access protected + * @ignore + */ + _handleEndpointError(error, endpoint, req, res, next) { + return this._helperServiceInfo.handleEndpointError ? + this._helperService.handleEndpointError(error, endpoint, req, res, next) : + next(error); + } +} +/** + * This controller allows you to have gateway routes that actually make requests and respond with + * the contents from an specified API. + * @type {ControllerCreator} + * @param {GatewayControllerCreatorOptions} [options] The options to customize the controller. + * @param {Function():Array} [middlewares] This function can be used to add custom + * middlewares on the gateway routes. If + * implemented, it must return a list of + * middlewares when executed. + */ +const gatewayController = controllerCreator(( + options = {}, + middlewares = null +) => (app, route) => { + /** + * Formats the name in order to keep consistency with the helper service and the configuration + * setting: If the `serviceName` is different from the default, make sure it ends with + * `Gateway`, set the default helper service name to `${serviceName}Helper` and the default + * configuration setting to the same as the service name (without the `Gateway`). + * This way, if you just use `myApi`, the service name will be `myApiGateway`, the helper name + * will be `myApiGatewayHelper` and the configuration setting `myApi`. + */ + const defaultServiceName = 'apiGateway'; + let defaultHelperServiceName = 'apiGatewayHelper'; + let defaultConfigurationSetting = 'api'; + let { serviceName = defaultServiceName } = options; + if (serviceName !== defaultServiceName) { + defaultConfigurationSetting = serviceName; + if (!serviceName.match(/gateway$/i)) { + serviceName = `${serviceName}Gateway`; + } + defaultHelperServiceName = `${serviceName}Helper`; + } + /** + * Get the settings the controller needs in order to use with the container before creating + * the instance. + */ + const { + helperServiceName = defaultHelperServiceName, + configurationSetting = defaultConfigurationSetting, + GatewayClass = GatewayController, + } = options; + /** + * Update the options with the resolved configuration setting name, because the class will + * needed when generating API Client endpoints. + */ + const newOptions = Object.assign({}, options, { + configurationSetting, + }); + // Get the gateway configuration. + const gatewayConfig = app.get('appConfiguration').get(configurationSetting); + // Generate the controller + const ctrl = new GatewayClass( + gatewayConfig, + app.get('http'), + route, + newOptions, + helperServiceName ? app.try(helperServiceName) : null + ); + /** + * Register a service for the controller so other services can ask for the endpoints formatted + * for an API Client. + */ + app.set(serviceName, () => ctrl); + /** + * Check if there are actual middlewares to be included, and in case there are Jimpex + * middlewares, connect them + */ + let useMiddlewares; + if (middlewares) { + useMiddlewares = middlewares(app).map((middleware) => ( + middleware.connect ? + middleware.connect(app) : + middleware + )); + } + // Add the routes to the router and return it. + return ctrl.addRoutes(app.get('router'), useMiddlewares); +}); + +module.exports = { + GatewayController, + gatewayController, +}; diff --git a/src/controllers/utils/index.js b/src/controllers/utils/index.js new file mode 100644 index 00000000..3a2dd877 --- /dev/null +++ b/src/controllers/utils/index.js @@ -0,0 +1,5 @@ +const { gatewayController } = require('./gateway'); + +module.exports = { + gatewayController, +}; diff --git a/src/services/http/apiClient.js b/src/services/http/apiClient.js index 16c53bdd..3a933d9e 100644 --- a/src/services/http/apiClient.js +++ b/src/services/http/apiClient.js @@ -12,6 +12,14 @@ const { providerCreator } = require('../../utils/wrappers'); * to extend from {@link APIClient}. */ +/** + * @typedef {Object} APIClientConfiguration + * @description The configuration for the API the client will make requests to. + * @property {string} url The API entry point. + * @property {APIClientEndpoints} endpoints A dictionary of named endpoints relative to the API + * entry point. + */ + /** * An API client for the app to use. What makes this service special is that its that it formats * the received errors using the `AppError` service class and as fetch function it uses the @@ -20,24 +28,17 @@ const { providerCreator } = require('../../utils/wrappers'); */ class APIClient extends APIClientBase { /** - * Class constructor. - * @param {Object} apiConfig The configuration for the API the client will - * make requests to. - * @param {string} apiConfig.url The API entry point. - * @param {APIClientEndpoints} apiConfig.endpoints A dictionary of named endpoints relative to - * the API entry point. - * @param {HTTP} http To get the `fetch` function for this service - * to use on all the requests. - * @param {Class} HTTPError To format the received errors. + * @param {APIClientConfiguration} apiConfig The configuration for the API the client will + * make requests to. + * @param {HTTP} http To get the `fetch` function for this service + * to use on all the requests. + * @param {Class} HTTPError To format the received errors. */ constructor(apiConfig, http, HTTPError) { super(apiConfig.url, apiConfig.endpoints, http.fetch); /** * The configuration for the API the client will make requests to. - * @type {Object} - * @property {string} url The API entry point. - * @property {Object} endpoints A dictionary of named endpoints relative to the API - * entry point. + * @type {APIClientConfiguration} * @access protected * @ignore */ diff --git a/tests/controllers/index.test.js b/tests/controllers/index.test.js index a03bb6ca..78f8e469 100644 --- a/tests/controllers/index.test.js +++ b/tests/controllers/index.test.js @@ -9,6 +9,7 @@ describe('controllers', () => { // Given const knownControllers = [ 'common', + 'utils', ]; // When/Then expect(Object.keys(controllers).length).toBe(knownControllers.length); diff --git a/tests/controllers/utils/gateway.test.js b/tests/controllers/utils/gateway.test.js new file mode 100644 index 00000000..96f66a0a --- /dev/null +++ b/tests/controllers/utils/gateway.test.js @@ -0,0 +1,1269 @@ +jest.unmock('/src/utils/functions'); +jest.unmock('/src/utils/wrappers'); +jest.unmock('/src/controllers/utils/gateway'); + +const statuses = require('statuses'); +require('jasmine-expect'); +const { + GatewayController, + gatewayController, +} = require('/src/controllers/utils/gateway'); + +describe('controllers/utils:gateway', () => { + describe('instance', () => { + it('should be instantiated with its default options', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: {}, + }; + const route = '/my-gateway'; + const http = 'http'; + let sut = null; + // When + sut = new GatewayController(gatewayConfig, route, http); + // Then + expect(sut).toBeInstanceOf(GatewayController); + expect(sut.addRoutes).toBeFunction(); + expect(sut.gatewayConfig).toEqual(gatewayConfig); + expect(sut.options).toEqual({ + root: '', + configurationSetting: 'api', + headers: { + useXForwardedFor: true, + copyCustomHeaders: true, + copy: [ + 'authorization', + 'content-type', + 'referer', + 'user-agent', + ], + remove: [ + 'server', + 'x-powered-by', + ], + }, + }); + }); + + it('should be instantiated with custom options', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: {}, + }; + const route = '/my-gateway'; + const http = 'http'; + const options = { + root: 'my-root', + configurationSetting: 'myApi', + headers: { + useXForwardedFor: false, + copyCustomHeaders: false, + copy: ['authorization'], + remove: ['x-powered-by'], + }, + }; + let sut = null; + // When + sut = new GatewayController(gatewayConfig, route, http, options); + // Then + expect(sut).toBeInstanceOf(GatewayController); + expect(sut.options).toEqual(options); + }); + + it('should throw an error when two endpoints share the same path and method', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: '/my-path', + endpointTwo: '/my-path', + }, + }; + const route = '/my-gateway'; + const http = 'http'; + // When/Then + expect(() => new GatewayController(gatewayConfig, route, http)) + .toThrow(/You can't have two gateway endpoints to the same path/i); + }); + + it('should create a configuration for an API client', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: '/my-path/one', + endpointTwo: { + path: '/my-path/two', + }, + }, + }; + const route = '/my-gateway'; + const http = 'http'; + let sut = null; + let result = null; + // When + sut = new GatewayController(gatewayConfig, route, http); + result = sut.endpointsForAPIClient; + // Then + expect(result).toEqual({ + url: route, + endpoints: { + [sut.options.configurationSetting]: gatewayConfig.gateway, + }, + }); + }); + + it('should create a configuration for an API client with a custom root', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: '/my-path/one', + endpointTwo: { + path: '/my-path/two', + }, + }, + }; + const route = '/my-gateway'; + const http = 'http'; + const root = 'my-root'; + const configurationSetting = 'myRootedAPI'; + const options = { + root, + configurationSetting, + }; + let sut = null; + let result = null; + const expectedEndpoints = { + endpointOne: `${root}${gatewayConfig.gateway.endpointOne}`, + endpointTwo: { + path: `${root}${gatewayConfig.gateway.endpointTwo.path}`, + }, + }; + // When + sut = new GatewayController(gatewayConfig, route, http, options); + result = sut.endpointsForAPIClient; + // Then + expect(result).toEqual({ + url: route, + endpoints: { + [configurationSetting]: expectedEndpoints, + }, + }); + }); + + it('should add the gateway routes to the router', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: '/my-path/one', + endpointTwo: { + path: '/my-path/two', + method: 'post', + }, + }, + }; + const route = '/my-gateway'; + const http = 'http'; + const router = { + all: jest.fn(), + post: jest.fn(), + }; + let sut = null; + // When + sut = new GatewayController(gatewayConfig, route, http); + sut.addRoutes(router); + // Then + expect(router.all).toHaveBeenCalledTimes(1); + expect(router.all).toHaveBeenCalledWith(gatewayConfig.gateway.endpointOne.substr(1), [ + expect.any(Function), + ]); + expect(router.post).toHaveBeenCalledTimes(1); + expect(router.post).toHaveBeenCalledWith( + gatewayConfig.gateway.endpointTwo.path.substr(1), + [expect.any(Function)] + ); + }); + + it('should add the gateway routes to the router with a custom middleware', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: '/my-path/one', + endpointTwo: { + path: '/my-path/two', + method: 'post', + }, + }, + }; + const route = '/my-gateway'; + const http = 'http'; + const middleware = 'my-middleware'; + const router = { + all: jest.fn(), + post: jest.fn(), + }; + let sut = null; + // When + sut = new GatewayController(gatewayConfig, route, http); + sut.addRoutes(router, [middleware]); + // Then + expect(router.all).toHaveBeenCalledTimes(1); + expect(router.all).toHaveBeenCalledWith(gatewayConfig.gateway.endpointOne.substr(1), [ + middleware, + expect.any(Function), + ]); + expect(router.post).toHaveBeenCalledTimes(1); + expect(router.post).toHaveBeenCalledWith( + gatewayConfig.gateway.endpointTwo.path.substr(1), + [ + middleware, + expect.any(Function), + ] + ); + }); + + it('should use `all` when an endpoint doesn\'t have a valid HTTP method', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: { + path: '/my-path/one', + method: 'myCosmicMethod', + }, + }, + }; + const route = '/my-gateway'; + const http = 'http'; + const router = { + all: jest.fn(), + }; + let sut = null; + // When + sut = new GatewayController(gatewayConfig, route, http); + sut.addRoutes(router); + // Then + expect(router.all).toHaveBeenCalledTimes(1); + expect(router.all).toHaveBeenCalledWith( + gatewayConfig.gateway.endpointOne.path.substr(1), + [expect.any(Function)] + ); + }); + }); + + describe('middleware', () => { + it('should stream a gateway request response', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: 'my-path-one', + }, + }; + const route = '/my-gateway'; + const httpResponse = { + status: statuses.ok, + body: { + pipe: jest.fn(() => httpResponse.body), + on: jest.fn(), + }, + headers: [ + 'content-type', + 'server', + ], + }; + const ip = '25.09.2015'; + const customHeaderName = 'x-custom-header'; + const customHeaderValue = 'my-custom-header-value'; + const http = { + fetch: jest.fn(() => Promise.resolve(httpResponse)), + getIPFromRequest: jest.fn(() => ip), + getCustomHeadersFromRequest: jest.fn(() => ({ + [customHeaderName]: customHeaderValue, + })), + }; + const options = { + headers: { + // This is stupid, but the iterator for headers goes `value, name`. + remove: [1], + }, + }; + const headerToCopy = 'authorization'; + const headerToCopyValue = 'bearer abc'; + const request = { + originalUrl: `${route}/${gatewayConfig.gateway.endpointOne}`, + method: 'GET', + headers: { + [headerToCopy]: [headerToCopyValue], + }, + }; + const response = { + status: jest.fn(), + setHeader: jest.fn(), + }; + const next = 'next'; + const router = { + all: jest.fn(), + }; + let sut = null; + let middleware = null; + // When + sut = new GatewayController(gatewayConfig, route, http, options); + sut.addRoutes(router); + [[, [middleware]]] = router.all.mock.calls; + return middleware(request, response, next) + .then(() => { + // Then + expect(http.getCustomHeadersFromRequest).toHaveBeenCalledTimes(1); + expect(http.getCustomHeadersFromRequest).toHaveBeenCalledWith(request); + expect(http.getIPFromRequest).toHaveBeenCalledTimes(1); + expect(http.getIPFromRequest).toHaveBeenCalledWith(request); + expect(http.fetch).toHaveBeenCalledTimes(1); + expect(http.fetch).toHaveBeenCalledWith( + `${gatewayConfig.url}/${gatewayConfig.gateway.endpointOne}`, + { + method: request.method, + headers: { + [headerToCopy]: [headerToCopyValue], + [customHeaderName]: customHeaderValue, + 'x-forwarded-for': ip, + }, + } + ); + expect(response.status).toHaveBeenCalledTimes(1); + expect(response.status).toHaveBeenCalledWith(httpResponse.status); + expect(response.setHeader).toHaveBeenCalledTimes(1); + expect(response.setHeader).toHaveBeenCalledWith(0, httpResponse.headers[0]); + expect(httpResponse.body.pipe).toHaveBeenCalledTimes(1); + expect(httpResponse.body.pipe).toHaveBeenCalledWith(response); + expect(httpResponse.body.on).toHaveBeenCalledTimes(1); + expect(httpResponse.body.on).toHaveBeenCalledWith('error', expect.any(Function)); + }); + }); + + it('shouldn\'t add custom headers to the request', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: 'my-path-one', + }, + }; + const route = '/my-gateway'; + const httpResponse = { + status: statuses.ok, + body: { + pipe: jest.fn(() => httpResponse.body), + on: jest.fn(), + }, + headers: [ + 'content-type', + 'server', + ], + }; + const http = { + fetch: jest.fn(() => Promise.resolve(httpResponse)), + getIPFromRequest: jest.fn(), + getCustomHeadersFromRequest: jest.fn(), + }; + const options = { + headers: { + useXForwardedFor: false, + copyCustomHeaders: false, + }, + }; + const request = { + originalUrl: `${route}/${gatewayConfig.gateway.endpointOne}`, + method: 'GET', + headers: {}, + }; + const response = { + status: jest.fn(), + setHeader: jest.fn(), + }; + const next = 'next'; + const router = { + all: jest.fn(), + }; + let sut = null; + let middleware = null; + // When + sut = new GatewayController(gatewayConfig, route, http, options); + sut.addRoutes(router); + [[, [middleware]]] = router.all.mock.calls; + return middleware(request, response, next) + .then(() => { + // Then + expect(http.getCustomHeadersFromRequest).toHaveBeenCalledTimes(0); + expect(http.getIPFromRequest).toHaveBeenCalledTimes(0); + expect(http.fetch).toHaveBeenCalledTimes(1); + expect(http.fetch).toHaveBeenCalledWith( + `${gatewayConfig.url}/${gatewayConfig.gateway.endpointOne}`, + { + method: request.method, + headers: {}, + } + ); + }); + }); + + it('should stream a gateway request that includes a body', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: 'my-path-one', + }, + }; + const route = '/my-gateway'; + const httpResponse = { + status: statuses.ok, + body: { + pipe: jest.fn(() => httpResponse.body), + on: jest.fn(), + }, + headers: [], + }; + const http = { + fetch: jest.fn(() => Promise.resolve(httpResponse)), + }; + const options = { + headers: { + useXForwardedFor: false, + copyCustomHeaders: false, + }, + }; + const headerToCopy = 'content-type'; + const headerToCopyValue = 'application/json'; + const request = { + originalUrl: `${route}/${gatewayConfig.gateway.endpointOne}`, + method: 'POST', + headers: { + [headerToCopy]: [headerToCopyValue], + }, + body: { + myProp: 'myValue', + }, + }; + const response = { + status: jest.fn(), + }; + const next = 'next'; + const router = { + all: jest.fn(), + }; + let sut = null; + let middleware = null; + // When + sut = new GatewayController(gatewayConfig, route, http, options); + sut.addRoutes(router); + [[, [middleware]]] = router.all.mock.calls; + return middleware(request, response, next) + .then(() => { + // Then + expect(http.fetch).toHaveBeenCalledTimes(1); + expect(http.fetch).toHaveBeenCalledWith( + `${gatewayConfig.url}/${gatewayConfig.gateway.endpointOne}`, + { + method: request.method, + headers: { + [headerToCopy]: [headerToCopyValue], + }, + body: JSON.stringify(request.body), + } + ); + }); + }); + + it('should add the content type for JSON when there\'s a body but no header', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: 'my-path-one', + }, + }; + const route = '/my-gateway'; + const httpResponse = { + status: statuses.ok, + body: { + pipe: jest.fn(() => httpResponse.body), + on: jest.fn(), + }, + headers: [], + }; + const http = { + fetch: jest.fn(() => Promise.resolve(httpResponse)), + }; + const options = { + headers: { + useXForwardedFor: false, + copyCustomHeaders: false, + }, + }; + const request = { + originalUrl: `${route}/${gatewayConfig.gateway.endpointOne}`, + method: 'POST', + headers: {}, + body: { + myProp: 'myValue', + }, + }; + const response = { + status: jest.fn(), + }; + const next = 'next'; + const router = { + all: jest.fn(), + }; + let sut = null; + let middleware = null; + // When + sut = new GatewayController(gatewayConfig, route, http, options); + sut.addRoutes(router); + [[, [middleware]]] = router.all.mock.calls; + return middleware(request, response, next) + .then(() => { + // Then + expect(http.fetch).toHaveBeenCalledTimes(1); + expect(http.fetch).toHaveBeenCalledWith( + `${gatewayConfig.url}/${gatewayConfig.gateway.endpointOne}`, + { + method: request.method, + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify(request.body), + } + ); + }); + }); + + it('should call the next middleware if the streaming fails', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: 'my-path-one', + }, + }; + const route = '/my-gateway'; + const httpResponse = { + status: statuses.ok, + body: { + pipe: jest.fn(() => httpResponse.body), + on: jest.fn(), + }, + headers: [], + }; + const http = { + fetch: jest.fn(() => Promise.resolve(httpResponse)), + }; + const options = { + headers: { + useXForwardedFor: false, + copyCustomHeaders: false, + }, + }; + const request = { + originalUrl: `${route}/${gatewayConfig.gateway.endpointOne}`, + method: 'POST', + headers: {}, + body: { + myProp: 'myValue', + }, + }; + const response = { + status: jest.fn(), + }; + const next = jest.fn(); + const router = { + all: jest.fn(), + }; + const error = new Error('MyError'); + let sut = null; + let middleware = null; + let onError = null; + // When + sut = new GatewayController(gatewayConfig, route, http, options); + sut.addRoutes(router); + [[, [middleware]]] = router.all.mock.calls; + return middleware(request, response, next) + .then(() => { + [[, onError]] = httpResponse.body.on.mock.calls; + onError(error); + // Then + expect(http.fetch).toHaveBeenCalledTimes(1); + expect(http.fetch).toHaveBeenCalledWith( + `${gatewayConfig.url}/${gatewayConfig.gateway.endpointOne}`, + { + method: request.method, + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify(request.body), + } + ); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(error); + }); + }); + + it('should call the next middleware if the request fails', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: 'my-path-one', + }, + }; + const route = '/my-gateway'; + const error = new Error('MyError'); + const http = { + fetch: jest.fn(() => Promise.reject(error)), + }; + const options = { + headers: { + useXForwardedFor: false, + copyCustomHeaders: false, + }, + }; + const request = { + originalUrl: `${route}/${gatewayConfig.gateway.endpointOne}`, + method: 'POST', + headers: {}, + body: { + myProp: 'myValue', + }, + }; + const response = { + status: jest.fn(), + }; + const next = jest.fn(); + const router = { + all: jest.fn(), + }; + let sut = null; + let middleware = null; + // When + sut = new GatewayController(gatewayConfig, route, http, options); + sut.addRoutes(router); + [[, [middleware]]] = router.all.mock.calls; + return middleware(request, response, next) + .then(() => { + // Then + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(error); + }); + }); + }); + + describe('middleware with helper', () => { + it('should be able to modify the request', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: 'my-path-one', + }, + }; + const route = '/my-gateway'; + const httpResponse = { + status: statuses.ok, + body: { + pipe: jest.fn(() => httpResponse.body), + on: jest.fn(), + }, + headers: [ + 'content-type', + 'server', + ], + }; + const ip = '25.09.2015'; + const customHeaderName = 'x-custom-header'; + const customHeaderValue = 'my-custom-header-value'; + const http = { + fetch: jest.fn(() => Promise.resolve(httpResponse)), + getIPFromRequest: jest.fn(() => ip), + getCustomHeadersFromRequest: jest.fn(() => ({ + [customHeaderName]: customHeaderValue, + })), + }; + const options = { + headers: { + // This is stupid, but the iterator for headers goes `value, name`. + remove: [1], + }, + }; + const helper = { + reduceEndpointRequest: jest.fn((request) => Object.assign({}, request, { + url: `${request.url}-by-helper`, + options: request.options, + })), + }; + const headerToCopy = 'authorization'; + const headerToCopyValue = 'bearer abc'; + const request = { + originalUrl: `${route}/${gatewayConfig.gateway.endpointOne}`, + method: 'GET', + headers: { + [headerToCopy]: [headerToCopyValue], + }, + }; + const response = { + status: jest.fn(), + setHeader: jest.fn(), + }; + const next = 'next'; + const router = { + all: jest.fn(), + }; + let sut = null; + let middleware = null; + // When + sut = new GatewayController(gatewayConfig, route, http, options, helper); + sut.addRoutes(router); + [[, [middleware]]] = router.all.mock.calls; + return middleware(request, response, next) + .then(() => { + // Then + expect(http.getCustomHeadersFromRequest).toHaveBeenCalledTimes(1); + expect(http.getCustomHeadersFromRequest).toHaveBeenCalledWith(request); + expect(http.getIPFromRequest).toHaveBeenCalledTimes(1); + expect(http.getIPFromRequest).toHaveBeenCalledWith(request); + expect(helper.reduceEndpointRequest).toHaveBeenCalledTimes(1); + expect(helper.reduceEndpointRequest).toHaveBeenCalledWith( + { + url: `${gatewayConfig.url}/${gatewayConfig.gateway.endpointOne}`, + options: { + method: request.method, + headers: { + [headerToCopy]: [headerToCopyValue], + [customHeaderName]: customHeaderValue, + 'x-forwarded-for': ip, + }, + }, + }, + { + name: 'endpointOne', + settings: gatewayConfig.gateway.endpointOne, + }, + request, + response, + next + ); + expect(http.fetch).toHaveBeenCalledTimes(1); + expect(http.fetch).toHaveBeenCalledWith( + `${gatewayConfig.url}/${gatewayConfig.gateway.endpointOne}-by-helper`, + { + method: request.method, + headers: { + [headerToCopy]: [headerToCopyValue], + [customHeaderName]: customHeaderValue, + 'x-forwarded-for': ip, + }, + } + ); + expect(response.status).toHaveBeenCalledTimes(1); + expect(response.status).toHaveBeenCalledWith(httpResponse.status); + expect(response.setHeader).toHaveBeenCalledTimes(1); + expect(response.setHeader).toHaveBeenCalledWith(0, httpResponse.headers[0]); + expect(httpResponse.body.pipe).toHaveBeenCalledTimes(1); + expect(httpResponse.body.pipe).toHaveBeenCalledWith(response); + expect(httpResponse.body.on).toHaveBeenCalledTimes(1); + expect(httpResponse.body.on).toHaveBeenCalledWith('error', expect.any(Function)); + }); + }); + + it('should be able to modify the response', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: 'my-path-one', + }, + }; + const route = '/my-gateway'; + const httpResponse = { + status: statuses.ok, + body: { + pipe: jest.fn(() => httpResponse.body), + on: jest.fn(), + }, + headers: [ + 'content-type', + 'server', + ], + }; + const http = { + fetch: jest.fn(() => Promise.resolve(httpResponse)), + }; + const options = { + headers: { + useXForwardedFor: false, + copyCustomHeaders: false, + }, + }; + const helper = { + reduceEndpointResponse: jest.fn((response) => Object.assign({}, response, { + status: statuses.conflict, + })), + }; + const request = { + originalUrl: `${route}/${gatewayConfig.gateway.endpointOne}`, + method: 'GET', + headers: {}, + }; + const response = { + status: jest.fn(), + setHeader: jest.fn(), + }; + const next = 'next'; + const router = { + all: jest.fn(), + }; + let sut = null; + let middleware = null; + // When + sut = new GatewayController(gatewayConfig, route, http, options, helper); + sut.addRoutes(router); + [[, [middleware]]] = router.all.mock.calls; + return middleware(request, response, next) + .then(() => { + // Then + expect(helper.reduceEndpointResponse).toHaveBeenCalledTimes(1); + expect(helper.reduceEndpointResponse).toHaveBeenCalledWith( + httpResponse, + { + name: 'endpointOne', + settings: gatewayConfig.gateway.endpointOne, + }, + request, + response, + next + ); + expect(response.status).toHaveBeenCalledTimes(1); + expect(response.status).toHaveBeenCalledWith(statuses.conflict); + }); + }); + + it('should be able to handle the response', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: 'my-path-one', + }, + }; + const route = '/my-gateway'; + const httpResponse = { + status: statuses.ok, + body: { + pipe: jest.fn(() => httpResponse.body), + on: jest.fn(), + }, + headers: [ + 'content-type', + 'server', + ], + }; + const http = { + fetch: jest.fn(() => Promise.resolve(httpResponse)), + }; + const options = { + headers: { + useXForwardedFor: false, + copyCustomHeaders: false, + }, + }; + const helper = { + shouldStreamEndpointResponse: jest.fn(() => false), + handleEndpointResponse: jest.fn(), + }; + const request = { + originalUrl: `${route}/${gatewayConfig.gateway.endpointOne}`, + method: 'GET', + headers: {}, + }; + const response = { + status: jest.fn(), + setHeader: jest.fn(), + }; + const next = 'next'; + const router = { + all: jest.fn(), + }; + let sut = null; + let middleware = null; + // When + sut = new GatewayController(gatewayConfig, route, http, options, helper); + sut.addRoutes(router); + [[, [middleware]]] = router.all.mock.calls; + return middleware(request, response, next) + .then(() => { + // Then + expect(helper.shouldStreamEndpointResponse).toHaveBeenCalledTimes(1); + expect(helper.shouldStreamEndpointResponse).toHaveBeenCalledWith( + httpResponse, + { + name: 'endpointOne', + settings: gatewayConfig.gateway.endpointOne, + }, + request, + response, + next + ); + expect(helper.handleEndpointResponse).toHaveBeenCalledTimes(1); + expect(helper.handleEndpointResponse).toHaveBeenCalledWith( + httpResponse, + { + name: 'endpointOne', + settings: gatewayConfig.gateway.endpointOne, + }, + request, + response, + next + ); + }); + }); + + it('should be able to handle an error', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: 'my-path-one', + }, + }; + const route = '/my-gateway'; + const error = new Error('MyError'); + const http = { + fetch: jest.fn(() => Promise.reject(error)), + }; + const options = { + headers: { + useXForwardedFor: false, + copyCustomHeaders: false, + }, + }; + const helper = { + handleEndpointError: jest.fn(), + }; + const request = { + originalUrl: `${route}/${gatewayConfig.gateway.endpointOne}`, + method: 'POST', + headers: {}, + body: { + myProp: 'myValue', + }, + }; + const response = { + status: jest.fn(), + }; + const next = jest.fn(); + const router = { + all: jest.fn(), + }; + let sut = null; + let middleware = null; + // When + sut = new GatewayController(gatewayConfig, route, http, options, helper); + sut.addRoutes(router); + [[, [middleware]]] = router.all.mock.calls; + return middleware(request, response, next) + .then(() => { + // Then + expect(helper.handleEndpointError).toHaveBeenCalledTimes(1); + expect(helper.handleEndpointError).toHaveBeenCalledWith( + error, + { + name: 'endpointOne', + settings: gatewayConfig.gateway.endpointOne, + }, + request, + response, + next + ); + }); + }); + }); + + describe('shorthand', () => { + it('should register the routes and return the router', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: '/my-path/one', + }, + }; + const appConfiguration = { + get: jest.fn(() => gatewayConfig), + }; + const router = { + all: jest.fn(), + }; + const services = { + appConfiguration, + router, + }; + const app = { + get: jest.fn((name) => services[name] || name), + set: jest.fn(), + try: jest.fn(), + }; + let result = null; + let service = null; + const expectedGetServices = [ + 'appConfiguration', + 'http', + 'router', + ]; + // When + result = gatewayController.connect(app); + [[, service]] = app.set.mock.calls; + // Then + expect(result).toBe(router); + expect(router.all).toHaveBeenCalledTimes(1); + expect(router.all).toHaveBeenCalledWith(gatewayConfig.gateway.endpointOne.substr(1), [ + expect.any(Function), + ]); + expect(appConfiguration.get).toHaveBeenCalledTimes(1); + expect(appConfiguration.get).toHaveBeenCalledWith('api'); + expect(app.get).toHaveBeenCalledTimes(expectedGetServices.length); + expectedGetServices.forEach((name) => { + expect(app.get).toHaveBeenCalledWith(name); + }); + expect(app.try).toHaveBeenCalledTimes(1); + expect(app.try).toHaveBeenCalledWith('apiGatewayHelper'); + expect(app.set).toHaveBeenCalledTimes(1); + expect(app.set).toHaveBeenCalledWith('apiGateway', expect.any(Function)); + expect(service).toBeFunction(); + expect(service()).toBeInstanceOf(GatewayController); + }); + + it('should be created with a custom name', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: '/my-path/one', + }, + }; + const appConfiguration = { + get: jest.fn(() => gatewayConfig), + }; + const router = { + all: jest.fn(), + }; + const services = { + appConfiguration, + router, + }; + const app = { + get: jest.fn((name) => services[name] || name), + set: jest.fn(), + try: jest.fn(), + }; + let result = null; + let service = null; + const expectedGetServices = [ + 'appConfiguration', + 'http', + 'router', + ]; + const options = { + serviceName: 'myService', + }; + // When + result = gatewayController(options).connect(app); + [[, service]] = app.set.mock.calls; + // Then + expect(result).toBe(router); + expect(router.all).toHaveBeenCalledTimes(1); + expect(router.all).toHaveBeenCalledWith(gatewayConfig.gateway.endpointOne.substr(1), [ + expect.any(Function), + ]); + expect(appConfiguration.get).toHaveBeenCalledTimes(1); + expect(appConfiguration.get).toHaveBeenCalledWith(options.serviceName); + expect(app.get).toHaveBeenCalledTimes(expectedGetServices.length); + expectedGetServices.forEach((name) => { + expect(app.get).toHaveBeenCalledWith(name); + }); + expect(app.try).toHaveBeenCalledTimes(1); + expect(app.try).toHaveBeenCalledWith(`${options.serviceName}GatewayHelper`); + expect(app.set).toHaveBeenCalledTimes(1); + expect(app.set).toHaveBeenCalledWith(`${options.serviceName}Gateway`, expect.any(Function)); + expect(service).toBeFunction(); + expect(service()).toBeInstanceOf(GatewayController); + }); + + it('should be created with a custom name, custom setting and custom helper name', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: '/my-path/one', + }, + }; + const appConfiguration = { + get: jest.fn(() => gatewayConfig), + }; + const router = { + all: jest.fn(), + }; + const services = { + appConfiguration, + router, + }; + const app = { + get: jest.fn((name) => services[name] || name), + set: jest.fn(), + try: jest.fn(), + }; + let result = null; + let service = null; + const expectedGetServices = [ + 'appConfiguration', + 'http', + 'router', + ]; + const options = { + serviceName: 'myServiceGateway', + configurationSetting: 'myConfigSetting', + helperServiceName: 'myGatewayHelper', + }; + // When + result = gatewayController(options).connect(app); + [[, service]] = app.set.mock.calls; + // Then + expect(result).toBe(router); + expect(router.all).toHaveBeenCalledTimes(1); + expect(router.all).toHaveBeenCalledWith(gatewayConfig.gateway.endpointOne.substr(1), [ + expect.any(Function), + ]); + expect(appConfiguration.get).toHaveBeenCalledTimes(1); + expect(appConfiguration.get).toHaveBeenCalledWith(options.configurationSetting); + expect(app.get).toHaveBeenCalledTimes(expectedGetServices.length); + expectedGetServices.forEach((name) => { + expect(app.get).toHaveBeenCalledWith(name); + }); + expect(app.try).toHaveBeenCalledTimes(1); + expect(app.try).toHaveBeenCalledWith(options.helperServiceName); + expect(app.set).toHaveBeenCalledTimes(1); + expect(app.set).toHaveBeenCalledWith(options.serviceName, expect.any(Function)); + expect(service).toBeFunction(); + expect(service()).toBeInstanceOf(GatewayController); + }); + + it('should be created without a helper service', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: '/my-path/one', + }, + }; + const appConfiguration = { + get: jest.fn(() => gatewayConfig), + }; + const router = { + all: jest.fn(), + }; + const services = { + appConfiguration, + router, + }; + const app = { + get: jest.fn((name) => services[name] || name), + set: jest.fn(), + try: jest.fn(), + }; + let result = null; + let service = null; + const expectedGetServices = [ + 'appConfiguration', + 'http', + 'router', + ]; + const options = { + serviceName: 'myServiceGateway', + configurationSetting: 'myConfigSetting', + helperServiceName: null, + }; + // When + result = gatewayController(options).connect(app); + [[, service]] = app.set.mock.calls; + // Then + expect(result).toBe(router); + expect(router.all).toHaveBeenCalledTimes(1); + expect(router.all).toHaveBeenCalledWith(gatewayConfig.gateway.endpointOne.substr(1), [ + expect.any(Function), + ]); + expect(appConfiguration.get).toHaveBeenCalledTimes(1); + expect(appConfiguration.get).toHaveBeenCalledWith(options.configurationSetting); + expect(app.get).toHaveBeenCalledTimes(expectedGetServices.length); + expectedGetServices.forEach((name) => { + expect(app.get).toHaveBeenCalledWith(name); + }); + expect(app.try).toHaveBeenCalledTimes(0); + expect(app.set).toHaveBeenCalledTimes(1); + expect(app.set).toHaveBeenCalledWith(options.serviceName, expect.any(Function)); + expect(service).toBeFunction(); + expect(service()).toBeInstanceOf(GatewayController); + }); + + it('should be created with custom middlewares', () => { + // Given + const gatewayConfig = { + url: 'http://my-api.com', + gateway: { + endpointOne: '/my-path/one', + }, + }; + const appConfiguration = { + get: jest.fn(() => gatewayConfig), + }; + const router = { + all: jest.fn(), + }; + const services = { + appConfiguration, + router, + }; + const app = { + get: jest.fn((name) => services[name] || name), + set: jest.fn(), + try: jest.fn(), + }; + let result = null; + const options = { + serviceName: 'myServiceGateway', + configurationSetting: 'myConfigSetting', + helperServiceName: null, + }; + const normalMiddleware = 'middlewareOne'; + const jimpexMiddlewareName = 'middlewareTwo'; + const jimpexMiddleware = { + connect: jest.fn(() => jimpexMiddlewareName), + }; + const middlewares = [normalMiddleware, jimpexMiddleware]; + const middlewareGenerator = jest.fn(() => middlewares); + // When + result = gatewayController(options, middlewareGenerator).connect(app); + // Then + expect(result).toBe(router); + expect(router.all).toHaveBeenCalledTimes(1); + expect(router.all).toHaveBeenCalledWith(gatewayConfig.gateway.endpointOne.substr(1), [ + normalMiddleware, + jimpexMiddlewareName, + expect.any(Function), + ]); + }); + }); +}); From 879e8371b2c731d640ed378a14ae30a676184d98 Mon Sep 17 00:00:00 2001 From: homer0 Date: Tue, 25 Jun 2019 14:52:51 -0300 Subject: [PATCH 75/78] docs(project): add the documentation for the gateway controller --- README-esdoc.md | 1 + README.md | 1 + documents/controllers.md | 128 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) diff --git a/README-esdoc.md b/README-esdoc.md index 57a6176e..fcc7eae9 100644 --- a/README-esdoc.md +++ b/README-esdoc.md @@ -370,6 +370,7 @@ Jimpex comes with a few services, middlewares and controllers that you can impor - **Configuration:** Allows you to see and switch the current configuration. It can be enabled or disabled by using a setting on the configuration. - **Health:** Shows the version and name of the configuration, just to check the app is running. - **Statics:** It allows your app to server specific files from any directory, without having to use the `static` middleware. +- **Gateway:** It allows you to automatically generate a set of routes that will make gateway requests to an specific API. [Read more about the built-in controllers](manual/controllers.html) diff --git a/README.md b/README.md index aee299f8..4bd387c9 100644 --- a/README.md +++ b/README.md @@ -370,6 +370,7 @@ Jimpex comes with a few services, middlewares and controllers that you can impor - **Configuration:** Allows you to see and switch the current configuration. It can be enabled or disabled by using a setting on the configuration. - **Health:** Shows the version and name of the configuration, just to check the app is running. - **Statics:** It allows your app to server specific files from any directory, without having to use the `static` middleware. +- **Gateway:** It allows you to automatically generate a set of routes that will make gateway requests to an specific API. [Read more about the built-in controllers](./documents/controllers.md) diff --git a/documents/controllers.md b/documents/controllers.md index 1965f739..1fe0a832 100644 --- a/documents/controllers.md +++ b/documents/controllers.md @@ -199,3 +199,131 @@ this.mount('/', staticsController( ``` And that's all, the middleware will be added to the route, just before serving the file. + +## Gateway + +It allows you to automatically generate a set of routes that will make gateway requests to an specific API. + +- Module: `utils` +- Requires: `http` + +```js +const { + Jimpex, + services: { + http: { http }, + }, + controllers: { + utils: { gateway }, + }, +}; + +class App extends Jimpex { + boot() { + // Register the dependencies... + this.register(http); + + // Add the controller. + this.mount('/gateway', gateway); + } +} +``` + +The controller will automatically look into your app configuration for a key called `api` with the following format: + +```js +{ + url: 'api-entry-point', + gateway: { + endpointOne: 'endpoint/one/path', + }, +} +``` + +> Yes, the format is almost the same as the API Client. + +Based on the example above and that configuration, the controller would mount a route on `/gateway/endpoint/one/path` that would fire a request to `api-entry-point/endpoint/one/path`. + +The controller has a few options that you can customize: + +```js +{ + + // The name that will be used to register the controller as a sevice (yes!), + // so other services can access the API Client configuration the controller + // generates from its routes. + + serviceName: 'apiGateway', + + // The name of a registered service that will work as a helper, and that the + // controller will call in order to modify requests, responses and even handle + // errors. + helperServiceName: 'apiGatewayHelper', + + // The name of the configuration setting where the gateway configuration is stored. + // This is also used to wrap the endpoints on the generated API Client configuration. + configurationSetting: 'api', + + // This is a helper for when the gateway is used with an API client. The idea is + // that, by default, the routes are mounted on the controller route, but with + // this option, you can specify another sub path. For example: The controller + // is mounted on `/routes`, if you set `root` to `gateway`, all the routes will + // be on `/routes/gateway`. + root: '', + + // How the gateway will handle headers from requests and responses. + headers: { + + // Whether or not to include the header with a request real IP. + useXForwardedFor: true, + + // Whether or not to copy the custom headers (the ones that start with `x-`). + copyCustomHeaders: true, + + // A list of headers that will be copied from the incoming request into the + // fetch request. + copy: [ + 'authorization', + 'content-type', + 'referer', + 'user-agent', + ], + + // A list of headers that will be removed while copying the headers from a + // fetch response into the server's response. + remove: [ + 'server', + 'x-powered-by', + ], + }, +} +``` + +The way you overwrite them is by calling the controller as a function: + +```js +const { + Jimpex, + services: { + http: { http }, + }, + controllers: { + utils: { gateway }, + }, +}; + +class App extends Jimpex { + boot() { + // Register the dependencies... + this.register(http); + + // Add the controller. + this.mount('/gateway', gateway({ + serviceName: 'Batman', + })); + } +} +``` + +I strongly recommend you to read the techinical documentation in order to know all the things you +can do with the helper service and the logic behind the naming convetion the controller creator enforces (the `serviceName` must end with `Gateway`, among other things). From 2afa16cf23711f7b91c9fb3ba9fa7709b3beaa66 Mon Sep 17 00:00:00 2001 From: homer0 Date: Tue, 25 Jun 2019 15:04:08 -0300 Subject: [PATCH 76/78] refactor(controllers/common/statis): make the controller return the router and not the 'routes' --- src/controllers/common/statics.js | 22 ++--- tests/controllers/common/statics.test.js | 105 ++++++++++------------- 2 files changed, 56 insertions(+), 71 deletions(-) diff --git a/src/controllers/common/statics.js b/src/controllers/common/statics.js index 7c4367f5..b9a4cc6c 100644 --- a/src/controllers/common/statics.js +++ b/src/controllers/common/statics.js @@ -81,31 +81,31 @@ class StaticsController { this._files = this._createFiles(); } /** - * Creates all the needed routes to serve the files. + * Defines all the needed routes to serve the files. * @param {ExpressRouter} router To generate the routes. * @param {Array} [middlewares=[]] A list of custom middlewares that will be added * before the one that serves a file. - * @return {Array} + * @return {ExpressRouter} */ - getRoutes(router, middlewares = []) { + addRoutes(router, middlewares = []) { const { methods } = this._options; const use = methods.all ? ['all'] : Object.keys(methods).reduce((acc, name) => (methods[name] ? [...acc, name] : acc), []); - return Object.keys(this._files) - .map((route) => { + Object.keys(this._files).forEach((route) => { const file = this._files[route]; const fileMiddleware = this._getMiddleware(file); - return use.map((method) => this._getRoute( + use.forEach((method) => this._addRoute( router, method, file, fileMiddleware, middlewares )); - }) - .reduce((allRoutes, routes) => [...allRoutes, ...routes], []); + }); + + return router; } /** * The controller configuration options. @@ -212,11 +212,11 @@ class StaticsController { * @param {ExpressMiddleware} fileMiddleware The middleware that serves the file. * @param {Array} middlewares A list of custom middlewares to add before the * one that serves the file. - * @return {Object} The Express route + * @return {ExpressRouter} * @access protected * @ignore */ - _getRoute(router, method, file, fileMiddleware, middlewares) { + _addRoute(router, method, file, fileMiddleware, middlewares) { return router[method](file.route, [...middlewares, fileMiddleware]); } /** @@ -264,7 +264,7 @@ const staticsController = controllerCreator((options, middlewares) => (app) => { )); } - return ctrl.getRoutes(router, useMiddlewares); + return ctrl.addRoutes(router, useMiddlewares); }); module.exports = { diff --git a/tests/controllers/common/statics.test.js b/tests/controllers/common/statics.test.js index 74409a37..07e2f770 100644 --- a/tests/controllers/common/statics.test.js +++ b/tests/controllers/common/statics.test.js @@ -18,7 +18,7 @@ describe('controllers/common:statics', () => { sut = new StaticsController(sendFile); // Then expect(sut).toBeInstanceOf(StaticsController); - expect(sut.getRoutes).toBeFunction(); + expect(sut.addRoutes).toBeFunction(); expect(sut.options).toEqual({ files: ['favicon.ico', 'index.html'], methods: { @@ -52,7 +52,7 @@ describe('controllers/common:statics', () => { sut = new StaticsController(sendFile, options); // Then expect(sut).toBeInstanceOf(StaticsController); - expect(sut.getRoutes).toBeFunction(); + expect(sut.addRoutes).toBeFunction(); expect(sut.options).toEqual(options); }); @@ -108,23 +108,20 @@ describe('controllers/common:statics', () => { .toThrow(/is not a valid HTTP method/i); }); - it('should generate `get` routes for all the files', () => { + it('should register `get` routes for all the files', () => { // Given const sendFile = 'sendFile'; const options = { files: ['charito.html'], }; - const route = 'route'; const router = { - get: jest.fn(() => route), + get: jest.fn(() => router), }; let sut = null; - let result = null; // When sut = new StaticsController(sendFile, options); - result = sut.getRoutes(router); + sut.addRoutes(router); // Then - expect(result).toEqual(options.files.map(() => route)); expect(router.get).toHaveBeenCalledTimes(options.files.length); options.files.forEach((file) => { expect(router.get).toHaveBeenCalledWith(`/${file}`, [expect.any(Function)]); @@ -140,17 +137,14 @@ describe('controllers/common:statics', () => { all: true, }, }; - const route = 'route'; const router = { - all: jest.fn(() => route), + all: jest.fn(() => router), }; let sut = null; - let result = null; // When sut = new StaticsController(sendFile, options); - result = sut.getRoutes(router); + sut.addRoutes(router); // Then - expect(result).toEqual(options.files.map(() => route)); expect(router.all).toHaveBeenCalledTimes(options.files.length); options.files.forEach((file) => { expect(router.all).toHaveBeenCalledWith(`/${file}`, [expect.any(Function)]); @@ -168,23 +162,15 @@ describe('controllers/common:statics', () => { put: true, }, }; - const postRoute = 'post-route'; - const putRoute = 'put-route'; const router = { - post: jest.fn(() => postRoute), - put: jest.fn(() => putRoute), + post: jest.fn(() => router), + put: jest.fn(() => router), }; let sut = null; - let result = null; // When sut = new StaticsController(sendFile, options); - result = sut.getRoutes(router); + sut.addRoutes(router); // Then - expect(result).toEqual( - options.files - .map(() => [postRoute, putRoute]) - .reduce((acc, routes) => [...acc, ...routes], []) - ); expect(router.post).toHaveBeenCalledTimes(options.files.length); expect(router.put).toHaveBeenCalledTimes(options.files.length); options.files.forEach((file) => { @@ -200,17 +186,14 @@ describe('controllers/common:statics', () => { files: ['charito.html'], }; const middlewares = ['middlewareOne', 'middlewareTwo']; - const route = 'route'; const router = { - get: jest.fn(() => route), + get: jest.fn(() => router), }; let sut = null; - let result = null; // When sut = new StaticsController(sendFile, options); - result = sut.getRoutes(router, middlewares); + sut.addRoutes(router, middlewares); // Then - expect(result).toEqual(options.files.map(() => route)); expect(router.get).toHaveBeenCalledTimes(options.files.length); options.files.forEach((file) => { expect(router.get).toHaveBeenCalledWith(`/${file}`, [ @@ -227,9 +210,8 @@ describe('controllers/common:statics', () => { const options = { files: [file], }; - const route = 'route'; const router = { - get: jest.fn(() => route), + get: jest.fn(() => router), }; const request = 'request'; const response = { @@ -240,7 +222,7 @@ describe('controllers/common:statics', () => { let middleware = null; // When sut = new StaticsController(sendFile, options); - sut.getRoutes(router); + sut.addRoutes(router); [[, [middleware]]] = router.get.mock.calls; middleware(request, response, next); // Then @@ -260,9 +242,8 @@ describe('controllers/common:statics', () => { source: '../', }, }; - const route = 'route'; const router = { - get: jest.fn(() => route), + get: jest.fn(() => router), }; const request = 'request'; const response = { @@ -273,7 +254,7 @@ describe('controllers/common:statics', () => { let middleware = null; // When sut = new StaticsController(sendFile, options); - sut.getRoutes(router); + sut.addRoutes(router); [[, [middleware]]] = router.get.mock.calls; middleware(request, response, next); // Then @@ -298,9 +279,8 @@ describe('controllers/common:statics', () => { route: '/statics', }, }; - const route = 'route'; const router = { - get: jest.fn(() => route), + get: jest.fn(() => router), }; const request = 'request'; const response = { @@ -311,7 +291,7 @@ describe('controllers/common:statics', () => { let middleware = null; // When sut = new StaticsController(sendFile, options); - sut.getRoutes(router); + sut.addRoutes(router); [[, [middleware]]] = router.get.mock.calls; middleware(request, response, next); // Then @@ -344,9 +324,8 @@ describe('controllers/common:statics', () => { route: '/statics', }, }; - const route = 'route'; const router = { - get: jest.fn(() => route), + get: jest.fn(() => router), }; const request = 'request'; const response = { @@ -357,7 +336,7 @@ describe('controllers/common:statics', () => { let middleware = null; // When sut = new StaticsController(sendFile, options); - sut.getRoutes(router); + sut.addRoutes(router); [[, [middleware]]] = router.get.mock.calls; middleware(request, response, next); // Then @@ -395,9 +374,8 @@ describe('controllers/common:statics', () => { route: '/statics', }, }; - const route = 'route'; const router = { - get: jest.fn(() => route), + get: jest.fn(() => router), }; const request = 'request'; const response = { @@ -412,7 +390,7 @@ describe('controllers/common:statics', () => { ]; // When sut = new StaticsController(sendFile, options); - sut.getRoutes(router); + sut.addRoutes(router); [[, [middleware]]] = router.get.mock.calls; middleware(request, response, next); // Then @@ -435,21 +413,26 @@ describe('controllers/common:statics', () => { it('should include a controller shorthand to return its routes', () => { // Given + const router = { + get: jest.fn(), + }; const services = { - router: { - get: jest.fn((route, middlewaresList) => [`get:${route}`, middlewaresList]), - }, + router, }; const app = { get: jest.fn((service) => (services[service] || service)), }; - let routes = null; + let result = null; const expectedGets = ['router', 'sendFile']; const expectedFiles = ['favicon.ico', 'index.html']; // When - routes = staticsController.connect(app); + result = staticsController.connect(app); // Then - expect(routes).toEqual(expectedFiles.map((file) => [`get:/${file}`, [expect.any(Function)]])); + expect(result).toBe(router); + expect(router.get).toHaveBeenCalledTimes(expectedFiles.length); + expectedFiles.forEach((file) => { + expect(router.get).toHaveBeenCalledWith(`/${file}`, [expect.any(Function)]); + }); expect(app.get).toHaveBeenCalledTimes(expectedGets.length); expectedGets.forEach((service) => { expect(app.get).toHaveBeenCalledWith(service); @@ -468,26 +451,28 @@ describe('controllers/common:statics', () => { }; const middlewares = [normalMiddleware, jimpexMiddleware]; const middlewareGenerator = jest.fn(() => middlewares); + const router = { + get: jest.fn(), + }; const services = { - router: { - get: jest.fn((route, middlewaresList) => [`get:${route}`, middlewaresList]), - }, + router, }; const app = { get: jest.fn((service) => (services[service] || service)), }; - let routes = null; + let result = null; const expectedGets = ['router', 'sendFile']; // When - routes = staticsController(options, middlewareGenerator).connect(app); + result = staticsController(options, middlewareGenerator).connect(app); // Then - expect(routes).toEqual(options.files.map((file) => [ - `get:/${file}`, - [ + expect(result).toBe(router); + expect(router.get).toHaveBeenCalledTimes(options.files.length); + options.files.forEach((file) => { + expect(router.get).toHaveBeenCalledWith(`/${file}`, [ ...[normalMiddleware, jimpexMiddlewareName], expect.any(Function), - ], - ])); + ]); + }); expect(middlewareGenerator).toHaveBeenCalledTimes(1); expect(middlewareGenerator).toHaveBeenCalledWith(app); expect(jimpexMiddleware.connect).toHaveBeenCalledTimes(1); From 1ab174785990c16fe58818d84de32c5c2f1caf2e Mon Sep 17 00:00:00 2001 From: homer0 Date: Tue, 25 Jun 2019 23:28:30 -0300 Subject: [PATCH 77/78] docs(controllers/utils/gateway): replace wrong name --- src/controllers/utils/gateway.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/utils/gateway.js b/src/controllers/utils/gateway.js index bdccdade..dc54452d 100644 --- a/src/controllers/utils/gateway.js +++ b/src/controllers/utils/gateway.js @@ -210,7 +210,7 @@ const { controllerCreator } = require('../../utils/wrappers'); /** * @typedef {function} GatewayHelperServiceErrorHandler * @description This is called in order for the helper to handle a fetch request error. - * @param {Error} response The fetch request error. + * @param {Error} error The fetch request error. * @param {GatewayControllerEndpointInformation} endpoint The information for the endpoint * responsible of creating the route. * @param {ExpressRequest} req The server's incoming request @@ -796,7 +796,7 @@ class GatewayController { /** * This method is called in order to handle a fetch request error. It will check if a * helper is defined and allow it to do it, or fallback and call the next middleware. - * @param {Error} response The fetch request error. + * @param {Error} error The fetch request error. * @param {GatewayControllerEndpointInformation} endpoint The information for the endpoint * responsible of creating the route. * @param {ExpressRequest} req The server's incoming request From 90fa16a4380771e4b452a58733948a912c37fb45 Mon Sep 17 00:00:00 2001 From: homer0 Date: Tue, 25 Jun 2019 23:30:15 -0300 Subject: [PATCH 78/78] v4.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ada2b8c..1936b7c4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "jimpex", "description": "Express as dependency injection container.", "homepage": "https://homer0.github.io/jimpex/", - "version": "3.0.0", + "version": "4.0.0", "repository": "homer0/jimpex", "author": "Leonardo Apiwan (@homer0) ", "license": "MIT",