From 0053f78f94d3bc34a9c76d0c1a1c560c026bb0b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Fri, 22 Feb 2019 20:27:35 +0000 Subject: [PATCH 01/66] Bump eslint from 5.13.0 to 5.14.1 Bumps [eslint](https://github.com/eslint/eslint) from 5.13.0 to 5.14.1. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v5.13.0...v5.14.1) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 183 +++++++++++++++++++++++++++------------------------ 2 files changed, 99 insertions(+), 86 deletions(-) diff --git a/package.json b/package.json index 957d9fd45..f80496878 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "babel-eslint": "~10.0.1", "babel-jest": "~24.1.0", "chai": "~4.2.0", - "eslint": "~5.13.0", + "eslint": "~5.14.1", "eslint-config-standard": "~12.0.0", "eslint-plugin-import": "~2.16.0", "eslint-plugin-jest": "~22.3.0", diff --git a/yarn.lock b/yarn.lock index 2fe610fee..91e971746 100644 --- a/yarn.lock +++ b/yarn.lock @@ -871,15 +871,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: +acorn@^6.0.1: version "6.0.4" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.4.tgz#77377e7353b72ec5104550aa2d2097a2fd40b754" integrity sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg== -ajv@^6.5.3, ajv@^6.5.5, ajv@^6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.1.tgz#6360f5ed0d80f232cc2b294c362d5dc2e538dd61" - integrity sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww== +acorn@^6.0.7: + version "6.1.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818" + integrity sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw== + +ajv@^6.5.5, ajv@^6.9.1: + version "6.9.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.2.tgz#4927adb83e7f48e5a32b45729744c71ec39c9c7b" + integrity sha512-4UFy0/LgDo7Oa/+wOAlj44tp9K78u38E5/359eSrqEp1Z5PdVfimCcs7SluXMP755RUQu6d2b4AvF0R1C9RZjg== dependencies: fast-deep-equal "^2.0.1" fast-json-stable-stringify "^2.0.0" @@ -898,6 +903,11 @@ ansi-escapes@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw== +ansi-escapes@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -1693,16 +1703,7 @@ chai@~4.2.0: pathval "^1.1.0" type-detect "^4.0.5" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" - integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1767,11 +1768,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" @@ -2225,10 +2221,10 @@ 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" @@ -2327,6 +2323,11 @@ electron-to-chromium@^1.3.86: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.88.tgz#f36ab32634f49ef2b0fdc1e82e2d1cc17feb29e7" integrity sha512-UPV4NuQMKeUh1S0OWRvwg0PI8ASHN9kBC8yDTk1ROXLC85W5GnhTRu/MZu3Teqx3JjlQYuckuHYXSUSgtb3J+A== +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== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -2508,35 +2509,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.14.1: + version "5.14.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.14.1.tgz#490a28906be313685c55ccd43a39e8d22efc04ba" + integrity sha512-CyUMbmsjxedx8B0mr79mNOqetvkbij/zrXnFeK2zc3pGRn3/tibjiNAv/3UxFEyfMDjh+ZqTrJrEGBFiGfD5Og== 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" + doctrine "^3.0.0" eslint-scope "^4.0.0" 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" + inquirer "^6.2.2" js-yaml "^3.12.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" @@ -2547,15 +2548,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" @@ -2726,7 +2727,7 @@ extend@^3.0.0, extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -external-editor@^3.0.0: +external-editor@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA== @@ -2793,13 +2794,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= +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: - flat-cache "^1.2.1" - object-assign "^4.0.1" + flat-cache "^2.0.1" fileset@^2.0.3: version "2.0.3" @@ -2864,15 +2864,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== fn-name@~2.0.1: version "2.0.1" @@ -3509,21 +3513,21 @@ ini@^1.3.4, 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.1" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.1.tgz#9943fc4882161bdb0b0c9276769c75b32dbfcd52" - integrity sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg== +inquirer@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.2.tgz#46941176f65c9eb20804627149b743a218f25406" + integrity sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA== dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.0" + ansi-escapes "^3.2.0" + chalk "^2.4.2" cli-cursor "^2.1.0" cli-width "^2.0.0" - external-editor "^3.0.0" + external-editor "^3.0.3" figures "^2.0.0" - lodash "^4.17.10" + lodash "^4.17.11" mute-stream "0.0.7" run-async "^2.2.0" - rxjs "^6.1.0" + rxjs "^6.4.0" string-width "^2.1.0" strip-ansi "^5.0.0" through "^2.3.6" @@ -5079,7 +5083,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, 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= @@ -5970,7 +5974,7 @@ retry@0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= -rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@~2.6.2: +rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, 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== @@ -5994,10 +5998,10 @@ rx@^4.1.0: resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" integrity sha1-pfE/957zt0D+MKqAP7CfmIBdR4I= -rxjs@^6.1.0: - version "6.3.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55" - integrity sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw== +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== dependencies: tslib "^1.9.0" @@ -6195,10 +6199,10 @@ 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: - version "2.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.0.0.tgz#5373bdb8559b45676e8541c66916cdd6251612e7" - integrity sha512-4j2WTWjp3GsZ+AOagyzVbzp4vWGtZ0hEZ/gDY/uTvm6MTxUfTUIsnMIFb1bn8o0RuXiqUw15H1bue8f22Vw2oQ== +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== dependencies: ansi-styles "^3.2.0" astral-regex "^1.0.0" @@ -6406,6 +6410,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.0.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.0.0.tgz#5a1690a57cc78211fffd9bf24bbe24d090604eb1" + integrity sha512-rr8CUxBbvOZDUvc5lNIJ+OC1nPVpz+Siw9VBtUjB9b6jZehZLFt0JMCZzShFHIsI8cbhm0EsNIfWJMFV3cu3Ew== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.0.0" + string.prototype.padend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0" @@ -6534,15 +6547,15 @@ synchronous-promise@^2.0.5: resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.6.tgz#de76e0ea2b3558c1e673942e47e714a930fa64aa" integrity sha512-TyOuWLwkmtPL49LHCX1caIwHjRzcVd62+GF6h8W/jHOeZUFHpnd2XJDVuUlaTaLPH1nuu2M69mfHr5XbQJnf/g== -table@^5.0.2: - version "5.1.1" - resolved "https://registry.yarnpkg.com/table/-/table-5.1.1.tgz#92030192f1b7b51b6eeab23ed416862e47b70837" - integrity sha512-NUjapYb/qd4PeFW03HnAuOJ7OMcBkJlqeClWxeNlQ0lXGSb52oZXGzkO0/I0ARegQ2eUT1g2VDJH0eUxDRcHmw== +table@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/table/-/table-5.2.3.tgz#cde0cc6eb06751c009efab27e8c820ca5b67b7f2" + integrity sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ== 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" tar@^4: version "4.4.8" @@ -7081,10 +7094,10 @@ write-file-atomic@^2.3.0: 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" From 95dbba336906af3f1bc4fbd6000d49edb45f11ed Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Sun, 24 Feb 2019 15:02:05 +0100 Subject: [PATCH 02/66] Added more badges to readme --- README.md | 11 +++++++++-- humanconnection.png | Bin 0 -> 132723 bytes 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 humanconnection.png diff --git a/README.md b/README.md index 1b12562d2..29f2adee0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ -# Human-Connection - NITRO Backend -[![Build Status](https://travis-ci.com/Human-Connection/Nitro-Backend.svg?branch=master)](https://travis-ci.com/Human-Connection/Nitro-Backend) +

+ Human Connection +

+ +# NITRO Backend +[![Build Status](https://img.shields.io/travis/com/Human-Connection/Nitro-Backend/master.svg)](https://travis-ci.com/Human-Connection/Nitro-Backend) +[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitor-Backend/blob/develop/LICENSE.md) +[![FOSSA Status](https://app.fossa.io/api/projects/git+github.com/Human-Connection/Nitro-Backend.svg?type=shield)](https://app.fossa.io/projects/git+github.com/Human-Connection/Nitro-Backend?ref=badge_shield) +[![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discord.gg/6ub73U3) > This Prototype tries to resolve the biggest hurdle of connecting > our services together. This is not possible in a sane way using diff --git a/humanconnection.png b/humanconnection.png new file mode 100644 index 0000000000000000000000000000000000000000..f0576413ff0663ce0c79b6a0ef8ccd2ffbc77594 GIT binary patch literal 132723 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∈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�=oy+|6Oy_YqK(cGyJQ-6R9JfIGTSwpI5|f=N7bp8>lYn^`GG~oQx`MbiFIR>nG|d?@F{3hwksD zY)4h1NH~~8h|ZAip%(Wv{7UUJ^T5wDzF@h*cJo)tD#~RWJ#j$Wp z8DPIOZ>A>B?6peG0AKAU*9AvYQ_Cq&THnQZ^X3i2r~$9|Lv9}#F!WDe(k$v>*726; zUulw2pv1*tqsH6$TlB;G#a?S@Y5^0i&%zf9_#c1#+1|NKj5p-3+XH4qw#w@;+*iz$ z(LV#owzf;zUH(*OVW@MmPTAUvI;{P(9OpFT$F-nlwAubp(esZv@eZvGC}SoBJWgpP z_y#!Kkk;e!=6;-8$p@I&AxR0vEpC|XZ`!@-yj$DLnRkyXT?+(!6B?G}BfElmxE=m9 z_G89t=@}~(2kTh`vAxqnuRN70GLIa)a<4I(s_R*!8l=5BVHF7+bkN?_#n3{_@Ird(^8$i2}ni}y_3Ij&txzI-k&%FP3YyP?!<3L~t zi(S;)W1(uxhx!$A;wx&H(~X3w`dK$dq$F zVBY4#J!!mCCE=_0vDt1Y^#qro>&ZL;pheFMX2H*MLGA}|i<9Rk z&{DIpDGr3qbfVjDzDguJ42D``W;vE>&GdzuM$sh+@0<|_wLNCWF3z^>t(3MqBBjB+ zEo4AS){ar7O$Q}&qg`w(TT^qH;HR4k0zAKhH9MeFcf3If7wn*7WTrDN$S8E8d&AuR zUB${rQuaD5bihEY@`9^)NsfGv)9dH6x_N9Sm*J_5$^K?&%sIPNlL3>{1@stkvED2( z{rf5Aua%Jb2&Y|s2vK%z@f;bBL>wf38@*yNFBghq=m9JU&P`Q4qe^zZx_!Ngc z11NedJ~;ZC^Y`*1>aSJrLCNMfora}pyjZzM$WPbCax1wE0+21mW2~)i!vO~Y2~c#? z)IRFFnUU90IJ9CQ)`E+{B$odK1^>Yb&}vHLoHO$ZW0#$^pFWE*@dcIPJ>(W#9Z3?%C!_vU&_huY$>IBU%R=Vlg)j7O6FZRru z%D3D$4giM+wR-UY4XF{m2eTJaY9)P!*u=iYsrQguF|SRsucqpt~v-$SXctCF?UBH(`eeKFv?!f0_3x+#>z5FWo=vdLRZl zV_`%YlMcA=QU4SlrIWF{HXzs@oR$5;=y@L$XECw>x)eb@Xnh9)!_MCoBqXu=n~e<* z<80DA7aMh^mJ3q-EA|KN#|i>FRryOaky(I4%8b_on6RV%8-ASh#r&sx={19*#E#wC zCt(@)>*o~1ZI);bfq`E)H$5LjUz4P*DI~mOenltGJpuz2x<{cKWWwsqZ*O@hIljLR z(eX9si1a~dQ;bKsE>IAOKG3QEE?vJK&E83S`AcVMZeHGKS2bKR>Ro&>$xG8lg zfka7L>*8p+J~<^R6?OrlRDBCnfidy)q~irkicDXZDQOT2g-SgUgJ~gKC=b}b%jXvt zqr(Ks3YY+Xl?CWT>MI>D#Q@!c4-Co#D4Fe+jvf*H%SSBdF&qW$BoE@r-~e+bT{IgY z?1_*%>DGh3vf4yh{flcWziV9`YWGEp_B9b^PdFZ0q@4ZRrlaV$NQ);l>|m?&I}c^V zk;f{})09(Cjq4kDrAhQ%rjRV4W2k<}`C3!y=&J$N1Gv_&iUZNo64+DVUg1x}*{rNz z7ipK@uzuAe3o=b_-xUb<({VOSzthlO(?QE^dQX6oXkcpH|8lAl7cKu7y`!Xj>}PD? z{d-J^L#(RI?%HCnX;xkZa}xa1BNaSk^Y->m$r(V@MmQe=!bxhstZ zNj=2+VL#*w_XuV%RMQ%M8s_H1_09dUwg_JII5E7~@9M8C- z=ia6C4JpRNJob)xoNdC)12eJ6a$IK_hah76Cpz*@v5$P8cU!!Mkg_yR?uKcAPgu}a zQ{22-jee)+SOo=C3Xdc6iqbZf`uio3fBJY&C( z3tYC-RSTGF0P)yuR+PVe8+g{UKw7Mbfz5mJpH0QEQJ6Nxmu{Y%hU63BHnT5*<6g5jX#ek*(hig{ zt!qoJT*KNlUR>rfe@JGy|D_|asvepFl=tNL?1*m4Pp9wO(Ehn*a??olj|QHr>UrEd z;Stx3G}JNUOChvWH^0BTz6!rvcPht5QbslgSCzS_?L7PQ3+NPxZnewhv176n)HBlQ zpDYtM<%Z63cQ3CyM;p*e+bgqC1Koj><6v)e9!uTcO4k*TXu3XX zMDIG^(xc&14C}i~E7-GU3b{cBuE@CzU;jnh`oOEy>>Z8{R!3`Y-8xz>J{mb2BraK; z_a+9Cp_&FWzlBk>WNmQX^@)Ejf9HE!_}-=M@nL1X<9A^ zde6|JCfQGxv!VI_iwsO~3vN88Ct^T^JOup8@qzJtvr)0y3 zceBnm*;vB|dJZDc5CE4PR4J63WwObbmu#PwS zLsn;5EpR+e@csdxuH$Oocg)b1Lo^ef?fztr(a~r*cVUbGyWl^p;)Kv&A>YXrMYewK z=w0M`+E`zGr$zobNi=Nt~Si2(;zE~Sy~FB%@yV=U`*I!YNqt_xIehT`hV*5 z-M3);fq*x~7ZPgjDd7DV- zHIdCwESc9>cejjH8!V{2*c)o&ZG;YxF*GzZXBU@53~*&-rLd*b%XfMVnI5=DQHy7^ zXDR2pAnoeHq&28koTudo7Qeq9KY`l(LynOFe(_zWrivfh2eTtq)MaC3z1+is0P^K| zTCePb=pg>yP~x1>=gqXaRu?v%t1Buw={YhkF39wF&|?%SXeq;)zNS7S$MjFCiBU6- zz42nJUwb<%$agw}#*oaTpciRI^UxPX8ULDLBH)Ae!@+lLZKkOt{QmEA+TwWNN4*W9 zS4Eok14Cy=iMN80^!@LBU(936YYcD{9AlEcblMcIxVw+c-VvK$<;icOxhv~pCycwL zQPN!JBEY(A$k)TJ3w%f zRR1HXOjFyp#wwsa?RjQ?w4f}1I3XHq)*rWq2#4%#oE?r5W{EUnQR$ENU9wp=ttrY} zFILIASRLdx23o)2PucEXTVRiQsHpG>&Ckytn1e;!i;98x3hNO5EoKr$5zyDyx1C?) z148Wd$+Gr0FY95dswI8>794QXhnsWne4Vj?I)5qb9>*(T2-h=M%*x7&*_{lYig)tR z@HpO%n!QI;?>JG%JmXPOpmTMZmtP!_Qv6|KmCFdacWzw`n*lB|&JO2{E6?r-3AbA% zo9l-v*Vtvi^?qi+J-hFYB3lf2KcwO}g?KG4w|44lLKXhSPEFkfZTvw2{RJMNp@W)+WR0sJFLX5SIP)lQ*=;*j=2pm6f z*y6bm&b>@i%U&fHA@K)Lc`7%_j_+ggwS5Y|O@X zQp`yf_V(-C|s3gEps2MO*Ht!jBiD6_dl8I@$8mjFIdBno$?B@nW`9UpjH zZ_Nk({9)JY`+@56GlmqL1KeEA$NfGb-N;nxV^^Dm91&@Pua5}5T4^P;=M69|}c|<&LM?Kw^(Z&o?5)f+KbT&JO4@ z**Y5fYu}DX@IfSe5Y@5;?eN?ETb9S=6}BH&P5!Ss3{1M3i}e~$|6t=q zR)6DS_vP3~2WjC&f772kRiZ<7JlyBcJIf3i-SaSaZ&LjG8Qe0P1{p?K^IuI6)P1k1 z_g?^4AAuUI2+80Hw@4Z3!YuLzQTX^Sy&-nZq;^MS=GR_JV<$-{< z@8RKFDbX5TW2+M1Wtg6(f~7Sr@$$+dR-G}CUQ!=VeyzFW+vEu$=pAf_J3;9+^=d0? z61KJXvHsutG-G6b0pG>JKhMY2et1zxU%J$xoy{(*uJG8W`er`ode>=k4KrR66}NzO zuEkdCFr`_n+L;os6{I(wDj3Pyrem8pJuD`%AX>@81&yNaJ4X z3%J}Io0LkA%*>E_(n;F<(}!CiuslPhcb3gZWy>(@u9(bS4NCxT{ zFNb)CcoR&@z?ZS*=n)&qG@J`nO zo2g@Jq`%Edf+^XqSdm}86+B!LCu+rr(-v)#_%-PphLHSNR^_yxi=7;(glqE-s5~1i z-|IZXE?o((#BTpqk@|}~w64oD6fQSX0VBXm8yR~uu8mk-8L{AkV0l+2B=YQyJh1+* z_`|J%3vWg%&x5^c5CX8VRbkgIKidqco`cNpvA7T_bo%3hxjDC(*jED|g zHOvY8qh>h|KOFAGNRqL9jl8 z%1#IBfA3_Y8};VgC?a!utw)!}qs%&dtQVw~HXX8@u15KqJH}W4W$(9R%T-#+=YXH6 z1?RfG`O``rXZgR*Ysn}D&A7hg4 zgCMqJT*!R3vU2qCzLZ2`EvHi)V*J&SPOh7l3kWYhg~=UY|?mn4hg z6hPn^Jx3ZL*6`8a(P7>XeDi7fkWumy864Ox#qi1vVt?9l5QCnc9`_tRyV%b^=71)K zB2Jep!}h&(T?+rp+MQBEH(xRT%=Gz7PKRcKm$?t0Q@nwnY_o)Dj|Xn6o#bI4FMU0! zKXM=&AM^Q)%2MEZeMMnt<9Ot^o&~gm16UWN`EmWxuu~A`AhSr$TE3UBk0h^uT(Wkm zb56kRxAUE86oP+2a9c)V%=!m=-M+E}wJwny%Hlcl#~aA0VRge>+_X%>SKP^#S%+94 z)i0kV%ph7BL-donpU+siL}^@B05O#&ABr+@RUqm49Q_qP(*AY-uf{+AP^aAzA2!aQ zEg@@j44`YlPQE@pS9Qq>63#FQD!3~S(UPbk=L%|oSP}cCuFLz`AonaiBqT%z(Thql z4ISufL43SOEY}wo50d?hcLKtYKklYcv-H~!Ty7miM|Fa!5X29Oo)oxwJ7zg+Hr9mQo1R8#!2^GJ9O>xT=jVUTZX*J?fYI?+ zOLK#2E<8O|uOrIiO0W8o_5#-@;H#28JpjBV6Rf@IJgzCq9vHT^r%D5bjXVX?F+s_# z?d|UcMiwO#7+?UHZ#lLOL&icvFvdacNJ1HXN&JLK@l4^T4*&pFr)(QlXxGZK=(^?c zHY!Vl1}2Y zU-zxI?$F2OeSq(SQBY3$T>g8_op z^%2C;zwuWW5NA8Ce(ANaQK{;AveLhnv0~HQSJnY*filvAUc7IR3 z=!pR0o736Y{6=YDf=Fx)qa#3j+XcKWL>_aWd(}SyP6p;{Wt~Q4DJgT!=f~T=ZLWZ8 z=g|+G8pQ>*9Avk&j-_njQed?xtZ8tSJht*xyL*2^np z`X$7Bo1A%xj;HVo^zUb?W3c#vy0o7$r2ZHqnfZ7!m1sPaN6`qqHoinr2)_ZHk?H2! zSdE+y{MEx#K)BLpW&ebYBIpBk6y|v5vgeA7YjFF{_24)!c;06C-QfqEPxUJ zln$HXL^|s3tN(5Km>|^j^zfa8bC$M+|7m3V>L|k*g2tIuR)KIrT%0V<$)=#%f1?JX zVk0d~p4>uNRltY?etWP)5)ekW>xqQ6IG5!a6Gq^n+jp?$jcEr_m7*rg9#F1N#b6|BUjvN3SNsgSloFNp26RRgxMB_gX`hGwl)0<382N&26@i zaDVwk;_+O9*e0}K?t6r?iZjudFwq8U4eui^I(F%Mo;xnIf}u%$X6)Nz#_ZXI(SOc{ zi)5a!@X95raQ!HE)7kNs)wep?UJf(W)47P?iMyEEsuCb3;sze8 z0w1?LZ16fbH9}#+HyhI&$k+jqWEk)*w)s?*m7P9k9*Fq5KF+)S}`{4 z<%m8Ynxb-}i$oOdg$eHbFv5yZK?sMTq$Tyj9wC?Te1(%2_DCY&ejx+AYT@K`2+A8_ zdp0Gi?yYbib0qVYuxK_mBS6PwzuKxZ1x$xydF$%xgk27!ZeL*EvO*S-|;?JgA6FIXs1F-bHFt*BS5)!x}xO;_OJ}h zY^3-3SlSe#8HIc0fSKW2GoM=}5B2SLjSNVuZ#OEyydL?_`1rU2|7ajs*oiIY${Flv zkW3c(u{jB&ZNXjq&`wn@)-Nn@t$fe1;=GLNUS&ajoEp|Cf&=pd5R~9 z(k@pQcQ4f>vlYJ^D_Q9RNqFwZ!6BU3)*Kf8Nu5Y$9>nylpQU`_NSx$MHZovC8|s{v zLn?y+j>Dby$Oq;NJJ6vtj+L)QnEvH)({&t>yciE3%!0+RoT<#kb%$dTKsOAZ(|)nx zX6n=VRW;)oq2zBeWIUJQ zZCZVnz{nuT&6<$V-V-!5c`;{tx`*j5<9#A-*_vq0U)25Ub?;uXp73$xh3(tI^pZ9n@f1N;#n!)rsQMB6Zf-^jZ+MhL3PT(bhWKPhJ@dxj~P~+nA zQCHKnEDSuEN4yrN)Nf&XOF%LIjHl1pf_Re@<|_~)yetAqgMKqItcRXFam)Es4|~7= zipoLJqe^G~wDqfGn>5TmW{vcue;+hdh)1iDR>HPzdLICj|0Wb-%qTkzCEUOjpAYxUvlV3vr-IMGkM1ef#fF+7#sf z^n`;SBT<*G;|(WD_?>?s>HQ#KyE_};H5KNM+(lqXD<6q2KRG$E`ThGhCH+73`u27& zRYSwvI$+OPcy>S|a7|VrA|kdN93IZx@X-X}P*B9Iub-HsJV6}TYv33a(Pe3?ph~dN z23+RNTk|+akh%j;~c(ve89L&!XKtkJ-1%L`;R#beIt#l$ay!@o7 zbbKYSuA`oQl1D^zn|c?o`^4$Ea(>qMtHcRz0j#%E7;*!qG3jw#`97()@`=x1dAY4L zJhXNRgMU(jJtIr-%cnn}$8sooZiYxc-@Lv2fH5Ik5m?eQp0bLIy5omyWMhK_Y!=bF z&sr&i8($L)9Sw!t^!V7Y7DsVxA7O6Lw4ov8_+TbQ7w1UyM z8p)NKW0ZCHxau zjwn?^a!DB>6_}kv;UXZAdwe<}>MXd--p8fFNM-l&FpmmZoMnrg0D%kxZwGB7)CcXkk$-Y6C{q3ZPfi-Nvhr!owL786#KGWd!GIJw#HUr3m~1R=vL z5ey)fTMJnmHZHx&856291YnCI9q@k^I^U8)))@B|3W?QTlf&U zs9W?71#z`8^GMI?%Et2Ot?^-D!{UIMvk~&w_P`??mmz#YJLiDRIKvE_c{RTW-(wH* zaTJNz)*f)GeS6IEU;TEF+^=9pteaHgf;O*xI`S4)qnQp}gmPqTR7o=+Y3MV;Fpg^z z>|(1+hB2@73T2vwgJE@AnH)fOUn%fS(JWDD9is{x0X$Wh_6cRlU_&L>p^U@#l@^y? z*Lu>x7YQwlR}l&TNm<-k^*_b!`ygc&OF@amF%mrAw^`1#w@xGwjYeB zLS3G49yU2%c%0wl2fbGFdE+TWRCH@Ig(%C%ApLJ?IQx4TnYf$zkaEzdjv8Qtx6Kx| zzJ5_j*^UDxU0|vsVW^NhXtyNvsH%ff$@`4aL<#5S=TF)}dV&Vb5G%{ViyrnFg@}RN z`6gF~8Zl=8W8jZ0|Cxx8y;lKOFj`jV$YiU^s9aUOus@3mb3y7y?e`3IqW$g)yM;t4 z0U>E%jP6MFC3QlHZwL0FKo85d(F*Z5e1>Zj`miZ(cpN-FYdnL5{RmG13*jpH8L}Sl zaCl^f_q&JpDOWAl=pPTMD3KC{AO!FVA99q+$}s~}$Wffg`wye${IdupH3n8M@M)n0 zgEc(k9BeMr&b*X>YWBv*%+@lcf%-rvv9Z%c3!{{M-nbeKz&=5Loc+#q@Nl1`OaoQ< zQDi{zP&CTIJTo1_?RUvvuSE(GJj9&mwq#)j`KD1r=^nG?)#lpTl~3I69U~k17q>Na zWfT!q&iJf>@W@=CjMLrR+|~;H8dq%KCBNkkJ2aS2WQ1@rdK2H8d|$YE+#cl=$VAWw zUXC)%%o5IAI42E-0wKeoz}vyFDVJpn@4|+x&v=cs?JNkR6&E-p5|CQG5WN zLjn`nli?wmKzWXUEqB8L8wZGqiC%_Oa4fzC;)yd@B@CkhTzP2`^2 zup4N7xzjp^EV+22d@X6b=1`fzxl#Q{(s~_-^ly%a`?KwWR(Ktx!H}Qn%lBzocBS|K zUN@k1?DS)Y|4uO7y^9z|iP(l@O8odY*6>{8^J<@h+!|u$@v|WpKmByYO!;^?>nH!y zwlDTPFk${qaGB9s{FY$6sW=o60d{!X8pl^xu;K`asTLWf#oN_e5@}0L2g!K(Sn}l$ zkE#4?!oX@y3D+HZ+4^bI`BN4Vm}^{mQ+FFmVD$0m?&z=`U84GE($ zI}HF*s*RM%`EO_=hT`E3lMdF?Qe#x4QLc#Pzqb-FRz*r?Wxm; zwcl*?YKw-q{=33AleAPiyzPb3VNk6Y^3oy(bg+&+_P>U`Z)g1fGmR`(6S#V;Yh$CN z8s7^qxi0zQFYbg(RxI!Y3Tz-jZ29kaFsjxuUZKA&tXOh#OQa5JbEXjjuMxY92vtXp zm#McXwTvkGTbqmzPML0v&cRIs9e;$n;Q4{Q;?c)Tj^l}|R|3p3_>dHQi%BrraCF|h zSe#*>eBKzK-EZqlrU|tC!sS?VCu0oLyI)}<;jfVwpB)=Jvkr?1xM zW|75on*?&}5^!s{ySX zg4={dNV2}Siolu@TOpxDMeP-vM)Fm|#BugJ^8+DjoAm)0dq~U{s32`2C;O z91y725A>zCd3Fu|H6rviz$yz35K&Z?K>Vczq_UDnZX5m;kR#`F_nHy{9#{~e9@@MloBg24R{u6fg4B_27(t6_#b2&Ub-gZCg2A;ZtC-90TTCp zWamwZ+@1*tzq$$V(2HWiW{6(yW%*`WS>J#9jF0@{E0Onu&D1R7N0b@)GwpI}) z;FLlKlXqP}+_aNegm?avi3on{I+C*i2^N=!Pyv!Mx}XLA1q6zh%60~Y$C`AQ7)fT^ zJbs+a3&4{Wty(g3tTF!tsEfAai|MBsKk*lo6=El8kQ=T%B2G*-`m@t_IuN+3l$FLz zU?PEAN!a?Isz3uLKr~-e0Pg7OcR)@2xy+!>n7&t?eYXOb^UY@3WMcRbntv_O3c^`^ zjPpWv&S%SupPBV2W$ksi38kpU$EsDv)lCZu-Yzb&kVjpOAiy?a`OH$uqR&DpLJIM4 zxm(uOi@g-9+5f>T*|JiB?7AWw1wx|KSSp7U$vXrlFR({St4WiTX$%nBRY$Yy>XVtY zbO;RsGR-_HISdA+asf~U@axIwBFjs)M)V{aq-qYsLzxE zBWw+7deray@t9nwvqrn9xj(UR#`jse+Lx0@1-C&~Av(WgQ|U#|0X~PZ)Z5#LmUK1g z--Xb^&eaWnHzwQ^t3Mg5ZXED`0%~09m;D|5A4j6Srf>(|2^TRttd^-dO^c}tSZa$8 z9W*C|-W;TR(8@Ez+@iKsMiG02`E+ZT3qwre<@H3ut7pLVYjXNdRG4tx~XG$!+A+2=?oG5-*jQe z9QA)x&R5Pmp}5T#zp%@~LsO7KxC&o?jeozC1mz55ALIW5b-xje&^}Tym(t=E{b+@% z;tYc`cdUBXQYJCw$N4tbFOi)>MtxIRVO_7piY#q(9{avBOi{8{oO^ef`NDFlU48CLnp@SR>9VFAyERGz6urlC)kI zb5XC;B(7S2?hG*F9d4=!8pU2^RoToNXr2sGq?VUD;@_1rP!iPx12kfBxO*Y97JSAD zsPn+Av?#L;uSd&@q|R?aXcyEa9|A;w+=Jpj}=^d zhQ9u1G!&c?h$CMDmKGM(RI!Al|J_5OuJ#|mmf6>1L!2X?6 zP)Ty=+A~KNVtmjqfcvU`$1E-L4zshwMoG~u%IY%I`*`wKr%@j>HzVvb7_$A18 z_cQ%__v4mdC6z@t(8y(Nb+^G$-}fy6@`{_lAg+{u;(v%UqOno%+nL@iL~Vb)>J}!3 zP4Wrt7~L-dT>MNooFU_&HO1&*GyhNYAOBN{(7UXK=8j#%-T6Aci;%)r&&Jo&|#*5{~3<Uf?4|=hq}g^x!Lxa2uQn(e z&c9Xm-AdiIDD=Vk`FQfN866B5JMsSsC}WjCO;~jI%|6xOgUx=f(6?>?!q4#f09o9*io>^X5I^Tz9>VPrDu!CTM3dof#ku+N zjOk`hA;wyY+_P#WXaYc0olFfknMv*-)t1De_jDfxP10Xv^+1v?LokU;LM}-n=^Q?S zOci^-k`P$f9WW}9=dBx48ebkEjn@!4nASc16}nv6;+cw z6c?ADB@`m3Gh(#taG?P0Rm@=t^CvSNE*=MKX91m2^PpqO-maoet;`8@B`e4mx#dF} z0HsBw9_;Iht03*+^iN(==Qh_=03fp7?_ zlYwZ^CC%mzpi^-C_YE4vRC0Vr4>1$Q4QdQcY3^gCs$=19rVKt-V)`3_v9HVBk{Y5a zoXf3MvQ{$~-PNNbypW+=U}2lAW%r{~B~t|wS&v&b4=c&Y)w{*PD;|Q}!i;L8^0wMW(whk)f$eV8HBQ%a_bJ@yEXDeu zu#}T1chS)h{q)>hWR#gIs|H!v*iu76e;|U@h)>fAl{4TlCv=Ay%TD^gQ(?%d&yyfZ zcKWycEz6wR=5!>MGf~dk_q(DuF>T3_G8>dvQ^VUj$vT{NB73#TIosMt`SE;hh(;N! z_wXS(M+n&iEh>I7wXx-{Ciy|G*&>mvDJ*uao~~Y9C}vMS7IFRKT}ahf_e_2VUIoAz zduvYbzkedQ_GTX;+FPjbGX6g1%c~8wj0|HMtCh&ep3o4`w4DW`qqK31(e>5Ni^=FH zypztVsHg@xd(HN%2YL0YsGqib^ta|AvF}3xyR*oVJ}nQzXyB^De10xofWPjC7k;cE5t>tQLel0AeUYHmB_wL-r|SUG9uj!Q!{akDIzEC0+Nmyt z)!2|FmZIkSSeJ!Wdo;58JjHUM4EeGIQYPU^e9I>`TDH=ryxOY9CXx)CtW)hmJ8tsO z2_IRWq7gju+RTmILSd-es352sB0jbrW3;GzAh#-nVm>)rOQ3D}>N zBAB~@Rg_BBP+zY@1qV6B3gW6087%hr{hu^0r&JLG_CVq)3PVs|u}sh~OmktG!)Tae`G8rz34xzH7Kggww z4oCq3cQY$^+J$J(L@t48x5|0g2L5Wa8_Tb88I?%CcWvP4Xy&D+N%}c*E9wXM6G&R4 zZbe#CP0sd{22bt0X2H`XyPD*Yi@S+@P@{(Y%~uV*vjVNy{<1veV4MByILD{s#;g5FKOhEaH3D9BI2Q!h|hdd93f* ziH8HM6s#*|`-9M0#SOwAAz$iu6&c16yk{c$b#6jc9{Z0w zHAopFdTNz8%u)# z%0#4qhemZRoZ;mT0(KtA&=ExP+?&xIjsBBxd3J8)=&GQ~mg_fE2rVu_#1F68!*6R0 zV8#E~hUUP=s24*xOb10`xcUn##ZU#=kD&^_=tUvl!1=SgxsUF<^0cp_MDU^sb!D8q z|KMJ-ixtR!7j~p86wK}#zIgIkoS)Vr%08(W`l3a3*6xg7b^u2so?>xQg%xqbJ)ML^ z%WJuZVpVgktKZ z5*RuVMzFRrGpj*-s$V-er^!>5GTiXxuNC>;p)KWsXXY>-i?I8plQEIX?Hs%GR1YdSkw{^-KV6O(Kmf2=y33;3QwPD;6c#t&W5sozg8XvIy%{nZ z{qK^*g1EI;)Umk@utdQ{G8Xu9TFw(d=rNur4uD9rXr(c+h?@PIZAQ0Qk+h=FTi4d9 z^M5$TN+UoPf5-X*kDt)v_{%uty~J>WXQf6uQBpS7Qvh@>i~-)+S5( zZ_*CPizyRxNP2r{>A9}w9@5#>Rl|a?uq#afkdDOnABe*JjOF?Oo}@NF$ZAc{(1joV z`$#*~RuCS%_mN<&DU2trN0{L4>9{%vaGK)LV2=byPz}V%6n@7JQ{x9oHMwv98~Mub zO(d|Vu9Mh^mNF#+QyZjCfYov%7Fv_sc#53mtF;`qmT#yyGC67I-_*nnk48*`Gz)uD z@8eyby{Ymy%ic24*!)|sHu_&DzW0YEV0Qock89Cs)gQN08j$U4;;Pu&et2hhNEeRT z4lQ?bvqyj2`<)`qBE!~F<92viD#EffBP!?lfduase&<)KLb>C=L`$H!^}uJb;46~} z7L3-+{=vWL|NZ?b@Eiysj~n|fAj#-D9)EjmFj}pTR@J&LNb4|1dzLVDTvxa6rc?^! z#$yzvIn$OdnhgGPk9SrV)b0BuX!N?fzRdd}Hn$7UF+!RO2#ckOo1zwVWdHu5NL_lF z9+7m$%y5jpLIlosZq4>6+nj0Nol$5RXw}48yJb`w->qXrB38!AelaOL#55!s_i;KH zH&3l_SVlig(O3K?45fXyQU*4`WG}mO1%3vWvskN^5d-lb(NRTS8==SkX|`y8Fs3AA z|G5Gy5rIrF9GkL4GRoA~!y2-0cx#>kLh9M!Ur8j@27mpMgg{o-cbdL@rNu-@GQ8k- z*;5lq3p8(cHI$Bsu{94hNJ6#B4yT1+PUyir$8fF7V#4MDsV+Ji8g3L}5{#5_GXI7( zBC`hBAHCKjWhs+uHE?Q~*xV4o3|`g3dj|N`F5-zKF7IWkC5tZ?{TSZ^iI=*bpHQZ= z8-j@UBn*siVN3tyO~5iqb;sE=k3ru&8H!GDuwRm3QJO6!)R<$F3u)5RMJsg=mz3Xb zbNcA@W2ldh+K&Pppz@*9#DCRg6(q+46d7F2gEepzwXw2=v%BH{NjXBs%*cx0;vM^4 zwef&UF+rJ^G(WPJcMNIEVfAig`v!)^g$lP!jagIKucJd?sxQ0Yb(SG&T6`-R?jHxl z3Ife^g3rjWybR2T#%y?|9_3t#&*X%oOC#GTZp?e69FVSGs)X-aCz;UNwa)OIzbA>H zd?FGkLLM4%u=p*Yz8gHr39jtX=;EV*S$c}sK3*hAGIVkQo%MMvW^>cU2aTmCnP^(( zYB6msIEcaf8g-935hqF5z`3su<9$nnpi|dIn5VJuU!{N{Uw!oi-0G$c(eq0GRf8Xs z)LktKC$%(1C4CW2h)R_#r5({zqTBMeZ-lh-re3*Jy?D%gl3R~y9==TLY9&p{&}ord zBupxp%R8cFkz7{QF6+bof$p{A7+j94C+W7s8Zt0=6UUzLY$ z#MNKC)dn^ZGK?5*6skQPA6ylYB{eV?>G^kece`i+-Sw;paW`6Al5GqB53lAwx$L2K zX%}o`tNFBYT5C(dy1bMu|Ay-KB@FN98+cL72aheDUmhFSb}lh*-DSE0B2-Cp~VS1z?nl{BSn-|9R!8;s!ABb-Jam!JgS zH(y(-NBnVAV?gmg75huLzFRqcMoX&rYT<=5Q<32s8CuE(kg7t>DIaG();!==!C-LtkADl9 zVQeW=3QW1v2oj*7=DG6ksc)vzpkLF8= zV}o(|yquY!L2E!s;jExeE7+clH%F1&jT+Etv*iqzZToxP4E!h&$J04FiY7D)op_9F ziR<#&kOx0=pr-izyR_z)qz8k4_omyra9ST72Q%{t8+;#M75PHU()ou2o~@B5j4#%A z`ub3FXFp0=&7pU? zn#nH})R{ZKmPlQ$tA&I`htWor8l+sGO#b7Vom#j+F37;oD2x))`ZBFPqjK}-AgUx{ zYFKM$1jXoQu!yWQz#N&*fwR(YQX9CijTF^MOulSJ6TDA`F_{J8LGF#*L21zxwid4kMiVT7bXPCB z2_~D!rKcXoAr0t>+e^G!gg)MQh*4!Qcfz-2guXV&>w4&Ul*%}efA}D#OEt0Lzxsvk zq+;ng(BH4ud_cG*9*Bc9L+MEP@D@uk_7!GUxy+@&RGue88cGFSwIH<4Sks-({KMhv zM2Cg1vp5$c0NX7c*(t#!h6zFl>zLk~j*}$gTAzw(rzLKPol-*URd{5Gl6r zGSboaqd|3H7|zdN6GIT^CIqpCm>PW%H6Rdf)xhBtlAsD~nu6$&>STSG4i!d^&|Ki% z&1}x!5YcrbJ9VBHqYEafcUz83{JAc-82GFW+}Bab$E6nGPaiT`#;V$0?@n?@*jm?@ zKAnw90i>5r7YSSaE8+ZaRr96<)e%LV0>TQ}Ft53Sv{^ zV0czhbe*|-Q8qJ5BNxS==BQ;HmqXpxvVe?Af72-@nP}fAQaT)u@t?bU1i6&_$jnkY zAMV1xetBb_Hhi()%#gSDnZGy~e^yy-x7xDo{CVH>l1p zA%LYA=3Ar9LETjB1m#i<5iUHym9Z-l1R{)SxWY|`V8mg$+dfV;u>zdHqJfXGiAlYn zSA=1~4+9n;k?;8Z&(a>O=vGFx&`?KOx6$KE*m9FKJSwXlOn>JO$nf#8ez~feEuKLlt?%}D*+U$qNCj8a^-FTyd!YRnGf|XlYlEIejDJk% zi*_wvjgET-m6uy!k2T(+T$$bi`UZ=Xd4G`N8rav?&@ocaGjcfLiegH*X7DDtSR_&I;F}&9BGAtz0Y5vzNP_QRlYUaIwOmI1Zl>k+z&e!|$XM3M@ zJ{tr@pOJ+Y{!}BW6ef!yW$XvLu8=#?UjQq0dTMxz37&enVjY4sYTu?U+)gYM51fGB z*pk@Ok4ND|D)X3$iu4(6UyrjZwm}i)bWlJ5@xatOdwzW`KF|{E@Vxm)ax~o>Y5x;A z^kccS9&&UT|E$(ccm=GjC*u@9vC!OQ^sTAUNc@)c*3nP*3*)nolwBdSf3CB^|{aO=J`_X7L zJMkDGdNN}lZt((Bv;#NUdyYXKpKoGMo$ru>)Qh#^M|s@ZNWdct-l(aoTg}OJ7l&G; zQff8}iXSm0w;@{>(zLOR`3f5IZKCk@yoV9ROyzgx1fg=U5@&~I=2LP1`v-(E$+PM9 za7s!FxVDZiHTo%hn0^inZEU}p-f?4{e$W`OT|zV1%&*AKZaF#(!)dNEyq+Zjl8H1S zEJT-ZZnj$C*W5pP{zPNr?jB+i+&6ki{MOF{H4P7|v(%W@M7;+{BapFk7byNUi(J3U zz_h~U;0>szw_CV9Fv-4pu9;fK@EWb`mns_B4mkk%j#kLZ0*}`h{*agNl!gs@dL%$! z81Hw^A5s_DY2l;tLnO-El;LXt62i9`1PE*Rt-nm{1MQ>=d7>m+miFXzAqqq=wCo%5ge zr9TlW?tmc9z)wFNyop$f)>R36-mn|pU0`(?+gg{1V9igC3RiDxPvT$}p|om^VWT@1 z)wpk~)35J;g;p>>ZJ_TWh>arGQ1Gri~mPTf@CctLfqq{-wcKmS^6xZBy!ykGj zqo+1)*Dw!QtbcKa3d(pF(zFP<<~c@GBsu*WpfowLr*@_lU3rKLgww}J{w6x%pw)`->xe&P^ z&W|Lk7|6%(!TDtji4zkCfG2g`6=6|o^&*uD3)wNWI7}19pSWM&i$3Be=W_6t+uEPgndH(iPI&~~*$V+sh$Gc&u)tmRYf}28 zz_FXr|Dwea)dl^rReoMOqN2J&;p<4g&xg4<4fb`JEG8XjI)T(Ug^S}=0sIY>N3;ii_9$YDsL8SSJjOSu_S^eGgJS_)XN5X9OQ>NfmG&)CTJFsC2TgutUd zR9T0FBjacD(P9rLmd^9b?eBXfjZfS&rl)x{`}$VbvYS)z1xXH}Z~HTuFqfbkW2vvrnI066A19-g<}pXq5Kwc}5Kf{Sq$$ne^97{bRJ>R?pj0Ktr8; ziF`=>j!O*-M*Vv}aLZ~k6q|9(sqwZ*rd+|-E(JCJP)QXQkiV5Ye+@GwP7hFZX9?c9 z?WHLZFSIkjZp?6}?%%J1vHihyztySZd@RxHsv+=mHW=Yq(Rs^O_AR9uGza_f$!*{@ zx*yh5<0CaG#EZzCNW0zHf#dNf==VCQHj#YQQ(i5%KoWh9W?JVdK=``dtt#~^+K7?@ zQK$EW>x7Rn5|&K|{DF;rYRBaK1ntv!J0?5fsg!>*u5&^g^?X>)j(78&Am5jN`Mf^K zPM#f0`o+FN2R+RyiE9ISYF~GZmW-%?e0#jAr$5p;s&gK>O@2=mBHrV5b^L}LY4nUg zKeG2{Q!MY;+y|tkz|bWi;UBx`AeHP3l`3?GkB1)>M_#QO&AOn=lz8m^-rM)nQt|J& za{9f$_mP_oV{c)>5~2XdA3_Bi;n>u!tV$Saj33eLgB39_!pI^Clc4ouuBS(qw3JLo z2;W+MXzM(z-@$Z+aQP&QUTt{^0NK`c`8A*x7h?>j@j!u@nWSgLdLsX6UV;33z6qpo z{-sMp+lPy7N<^4Ip9iU(2O#5{On#_oUibN}eaAPNVn_%FXQsy7u43Td`|X?CNVNgC8{T|N)6bnL za=<-3jPzz8Zlz$WC;Hji*;^&!R3C1u9^XvQMHw|$|(DS(aUHJ3$J2G}^)R!#RCM>#Ci!I4bi;+-fvuKzHUek`ZO7LrETVT;ek=pW+^Gt5$jINP*9AljeO-&*sS5Is z@;IG$RpI}73M7OT2EeGz&r_B(;9e@w?W8Ht5a8zKl_5m|(s#KU3A04iNB9}&nhQ6Lf|GzocGwE{iAjoHss8wfuvli?5!A$5=B8+7~pTdJlIdheik z@_nZLKaQ?BD)P6BXJc!#+sv)awON~8lg-(*+1jvivu)e1O`B_*Y}?lR{k`W@XJ$I{ zN6mfibMe7o$8SRg zZ(AHrhM)NPK)u~|Bs7@#y!2n>ygNLoz411WvgRA6{8TXhZ`j8iA|hqc+EZNwxZj+{ zp&F>4>Nd0r)M&oH0Q@1?k`hG@5y_vdaU==u-F55VGu2?O+(jk?Vo8&)^lkRyb)OHwc4Sskpk@LC#tFbr4R!V#A_ zs}0}shUp!C5@j}g>XPVKsc@v1TCZx)e>%n)J)P2-QK2eLHom{~x&H-&;7#3(NQf&I zc+QfIJPnL~m|EDsOXaHlO&kdV=16`RUE=B5lk0Z?mEafl@XO|L^{?lu+n#DMIM$y_(*ME$kkQiOt zhVn`Dw0N`|wO2##WXX-55k=Hj=wpTa$dQgDSt0z1EMWhMwWj4nKB0oRh(TOJhl9hr zjs6lG8X6=47;j}$8MionvciA!Up-ql>U82fxsMF^@8Pn|1mm`&<*^k(72&UCrVw%a zU5A588}`gXaYwHK2j25cr0dF$7w0?13tu+Z6?@ZOQN{yS)@D~Ub-yFf1XiD|=$*9; zdg>+cp6zzo7;o)mCl?XBzsIH#5=nySA6faR5H2H^|cXH`xJemEV&xAjERkb`cQ z@!KDb!K*Fy0&sP!erUfRx`LYeq_~5wFe5~(Y&0&;u7}_pD5D1`@|lk`Q-N90fqV3| zpwSIgCb3_UF~?8er%i{?XUDKo!nmvMLJ(j+a6REKX&9WLBXj~i| z%$rHqBsn?B>>F2hpz!_T5x@km$WJe9lQv2I1w1TRIY=}DKDNVyfuBGR)|-hrgPW%R zsm;<-FPb%P##M&I>0P~?EV!|Gad!iG+dYQt{#gF44P1vbY`cT?a#SE$E&D++74lIXuWi? zVzxdNw7Ifga-y|$e4{SGB2r^twrcnW0y~CAiy2dr0+n?Q_8+*m;x#cL4)lQm1F$5*kDSwUm6>xPpOCYF+$F9Tj zH#Oo2j=-LXvY_OVEIX2E)a=se97OVB3YFpd&RS5B`EV?~U`vh)+&+%>LvUjR?4@!w zkR=s3M=yF!0=pdvN3)i02%S5v8rrN+V`ivr1H$jOqJ!t@xry=j^D$9}IfI|C^?umfD5q{UT-SB6xZ=iILj;3AZPms3h%h;OBSA#!dKtAe*3BGe#^3(cj2r zUwEm5Plv{+H3N$$aL-j10-#y3;PH{Tvp$`X_|9SX)kmX?v6;B4QgG!UT4u~x9w&o( z$Cm`Q=-jn@t>gffVhYVjri5R63BCZVjy*ym*;HoCO_VK2#H4G-WEj&b-HT-5q@Sl_ zniVnfh5kQ+SnW0!dPZ2omucIWI60<^;~+m^AOx}Bng6;##J;6lP-G{jJjF=zl@~e) z(T`)iyKgQb=MIcK|dsop>0hRlRcP5RlhxA4f1eugI1d(WhFnL zJ@04&jY=`K@`C#J?^z=?F*OoiQKfuqaA4p!b@hXQ{zus@-?6W>V$g9?*tLSXfmop~)kl=My{E&8lRra{@zyVxGpQ~GAHg6HQ&{D;O&3S$fMyQ}> zYkKxsSS0|NY-O@djhPXTwwX2uG2PHA zZ@g30)fJStWGA04?tW^EYa>)QXL+(h*)GD7rb-c|;5@@GNZt)6jR$D{DF})Ojz&)5 z>T$9vr{RJ*_Q%tvK7G7*qCB?mZ%-08XY+ZHx99JC2B8(bOO5@7(Q3~p^+HoqDQK3( ze(+u*cCUy%nUvBuq4RkkNy%okW-*FpiI;T4_^D*SZgQ!nov>{hm98b zh>aE~+^ged6yfv``TPzjo-Y0;PfyeTl*^_r{fPp{t2Aj>Xff!)OZS6K8(wLQYs0`d z!3Bg{8gX6Y%JY0Ywt~BlhtcK)F&pdlD*vJoEZH7owD5~JIr2`mae!&>yA9N{;V)TqUHK+{YiZQ6i`RzC}miG&pdnz;Q!Kx zJp47lZjItDmE1Aa(_SrPWn}sv;kb@)711?NE0BCjsjaO4ugnbZe9bB!_k9OZ;Rvth zKR`$XUwC9)c=TLH987{_sk4rGISK)lYxV#@QZz7L>EJM#Xol&H6hgB25GKD&?N|Xj zdj@EW{%1)5J~<>qY|Oa{74*$8*4~PctiBhz1G?%-OSBDWK$tsU^pnqA;Z9)p+Y|g% z1TY-IBxDyW1G@@8;rh-zX)*p+!hWK$Oi-_Z*Mp{5w_9X@IxbuiqH#h{Tv?x}N42v| zLcSTl^C&9V-VER|{_=&TGOuZzG#GTM9oE75Q6<@8Yv31y`wye9B#A@)lIAdhlky+G zvDK`7K?ng$1$NlL$wQ+4td4hYI+mC1wd}i%W%Hr|vf`{3HgU!P{}6(!Xv9uG2{^3n z3YA?ajE%c}gJGj#yu~1|kn|4ZLiPhp`|AG_sAs$mbwNWiD3gq1#aI=C26BPPK-gp^ z|I4R8%7O4UO+Oj;vVEsiFEoLxz|K9&%VX3{tRe2L=Pw@5!G|`L;RHRq$R*MEY&l@x zTtwDRb=-ZvNAV!U-_u%Ei{NA#Jvig>@nT^AbpcMag9529m;y;Hkb<`O*-&O<^rv~B zXfCGLWMENMk51IIB=kZV<}4tNreHo;;#m|_COxlWT0~P&GM5VG(a<#rXt;!#>C;h} znqyT+SX`V05S;+MB&ByYR)b!D&j?V8D}$wTuB-K`IbcOGCj{tq&!W9VOqLN3f8Q?q zvBoHLR&wP>2EgQ!C!SP2dx<&Ara4LK8R$o&BZ8jZMFAiGw0+MHPk9)yyYnN12&FG% zBZAhwUp?kIM0GW|&)yMVZ$8e@B*4hBm~Kv~p?GE{0A^9Z2C}5&MSJRW#Dk+|u!=y} zV?gd5Z{F@Hb#ga5<_E!jQ_+B+sHzRp%$2H&ivB<;(5Kq+R2Ee!=I9N5YTX2k!~_N zMxZ&p0KBh-#ho`h_%ho_p>G|q&x;1mEvkNDw*cHvRr^FS86)L`sxaU@o6H@2t`vCt zM8JwYeDI6b>7QM;Ex%PsOg2wtuTnaM`?bN^c6%}%($Hnl|L8H=7Lv_fx#^IMzE(C> zbKbYOm+ki^+7_zREi~Rd$w)zU!MMP_^TZfFQ@~1tN6&&6SV1QrTicNsRxv@FROxfs1S`PUxY5&TLf#zYw=GZS(jW9HhX{wfyNmArVzj52H zV9Oi1bx&Gn;_)wxPF+qY3KA`3$Epet6oHS6w$wNEUyf#61Ynxy`Ld9aj}i7NzgmI_ zOj)0Mjcnw^M0lVS;vC>cZ#*MU!DWbp%QQ4?%0_mOz5Z;U~l7RW!patxKAP_A2 z+di0}pQ)dPpJg>`KkmzpYI7zlm+u|noSSFpm* z?~&4H)7{4u!0G@Wk8;Wdm+Sc3+h=Q`cG?k%kb$X5Q+E1qlZp}y5y1>)Wu$>iJWhK# z_wR2*UST(V-q!ODA)+F z4|HcCAS))K)(`C|w&c}m8sv2zk}V)@1*3n=#wVqNk~lqM2Z0Yg*8Qq z?}Iv+9VI=qt-ewDi1a{+33u~W-D~;g;f+{;a)wf=uIz}Q^dZFGmL331p`J?!N^lf1 zU4GC?@wcqq_v|1e@uOV%hv7@#F&y3BtHs0i^49lGT@lsR{-M*;MM?YFHq59wXj&Zc z>h1kPTi4aY-2g*VCwnJAYdt@n-E2{LjPQwTr~8y?=8fQ*)Ym{5yK_#rNu?(3Q^vQc z8%ZT#@g1Br7fnb@$vhPG{+Om^prf-4Yd5tNf{?-IR@c(m*5nEKM(I-db*U5mUs^iW zxjdHY+}$`iO-;7nd+Og~NdJCU#}WqQq`e=B3$0(fg5XDKSPEC$#Y+B`Kd*v;ymx4g zRAJXfgf`PN9C(XW4DC_L6|FUe(7AClxK%ZV9-KPS&RYmnGrMby04eu1$?8Q{_Ud?W zu!y#z&0Pg)457-DcPbew--{VdoQ`y#PYLSlB zU-l1`6&~TCTuHS?fSdw~u0k|E(ci6UWxSXfJ^rIBG38|Zz$*#`QOR*N!Xf6!sa^#b z06C@N5qE56j#rr5b3Y0g3Ie!@UJN-Shh5%@6a5&|^tXY-&f^@fO@b^TM=2c!xsowy z#}2%sj&3|;@GjOCI5KfJ^CjnBW5x`@bd^F;{D+FpXj1=%8E0fiV?H)7hWkPPN*2u% zw}sH^An+IiAXU^SJU|j)eZP?@F*utNUxT?01>0?Ge?vg>$yL_yXLA7#Fq~z-o34Au zsUUT)ikj?x$kUzd7bm7g?I7dmK!=bBdjD22fY0=oemLvXBP$uHAd7|%62eEdK0U4O zHQZcYHFaDytqW5nUBAPrC~P*=G~{+Ee=vG#%9;jvs5~p#kJ*j&L6GgCRYkTM*Jh^& zFNg%CTS?iAy;x6@O)DZYAOf(zB1@?}tH-5j^lU7SNOiF(V~`CNikZXhg@diLb<++Q zU?b=^IVi3pur=nF%tBl?$rnse-YfrsJ#{kPorz;N^+5oQ_Y9`08Pg*+#|qpwOy8zrPUPXr8B@|XI(a3}jzjs!%Ew2g5dN0i>S*bS zHe*0|-{<%!gdM?X|LlZwa+ZQbCx}y+o6)7vqE+JN2N-MoaxG38^i{4&GQ3fD_IG0! zM}hzqMme>?@_ee&8gE4>CSHog^5a75%#QafC}yuwlUN$#shbly5k z=(kJ`j5=YtdGedX%0vQz6U4Fj_<^PYQ#QwcO2BI4OAz9=q~|9KqO)y4Y$mESg`P** z=G%62FsxAj>T@5wBXN4yF+h^CI*Xt2)s}htUJh$vkuOOYAsX#k&l{$H{QmP~@|CaC z^LEQ&Xy@3zt2YjpY2)B%sql&vd9iep;d4W+s&B}L8rw%poKMS*nKs!*7wV32${9-= zy>*gg5b4LcdfKisM4i*Uvv$E=)Cz*Xm4W`K!}s=%?RY|3qL)tQ1;R^FpyNy{OdHiU zQL0Cy%EyltF3J>IM`#iZD>=5l;9V5!7}E<#<-nF+7VLbr(stGTz;8-{b%~9WF7_{( zqi|eb>98O+@{K*#cJr=`zg9N6Pg(l4Th?^MCBI~=uR*zvt-3basx#`7gWq#4>{yAD zVx=v1a77aa*dhc}=$i@P1Nz&MZ8}9Xj#-prs;S!DjhJR+86~+xs?qeBp}s#JV*VlX zbJ^>_M1aVqY<*#6!or?aqOKX(*Ou$(nwUluwo>M}A&xGHTVWlVrb$TH+5y83uRI@j z!Z-S@>grfkE_dLFV_%^=hus~MP07l@d>)nCkTHM_ zS>?{%f6$ogzC6{+Hl9M#_I9->5*b@;5%xR6E+Nh0Ady3O!_*z}{k2zQ03)KI{ptcR zdMu`ma?K+P20l?59p4_O2wTEPr?r~22Ex2-ZSiXm@K7_%louX$2$88_nvA?0{^zF+ z4E)>iBc+B950M0(L!l~K85IIwn;dyL?Wutd5U@ue`bMqV0tl0CT53wuHaod+H@F;8 z8se4E5s5b)FcW|8bhUlbw6)!>)T+wRcRff#Ok^ZXq9!|(If zcU}I=KkzG?%)(AZj%(BTlDZ9z3Qbnv>}Aj0zj%@#KPF;G1pS1#H~NzPLM7*_&|s&Z z7l3I{&z8Gr=I>F1=z1Bk{Y5)397>ruBV>F zjQ0k-?L|t-p5q35RrI+zRhSFCPRi$^IbXh4{=8Gu1hgMCcEZVHYJRRu^yx$h05~Mq z5gPfF?zFq!zBHMQ^9$LQ;NN3IVGLo1KcLr>Jw`yME)ShQx%yL*&Tx%?Su2Bt#LLLn zadVRko;H0nT)4wn^5I2J+x1QP(XE$#Ujd8kBw#5(=!-yH^IgOcj<=Bprv}_DWq!*8 znBXvsVTp~VJpK@as!@J^aC)8$mIcrgY5SwKCj@8q5~YS|DP zxwWm4YW-KWN3UV?Enm57HLuXTQ(F#IPuG15ZU+Z9R|+ES+RIsypF8iiH(oi;=0fn&*2h==RI~4=2R$RFIe#Xf*`iU~OT)C_sODU~d}M!Q}5} zUK;7G#j4sso|4lSIqaP~fJaFKm)}~uj+rJzlTcs8X=8TotaHVbn7 zy37dLIcd(nCO2tCo9k^0jsBwWe_;zf;x26)D50WZ!=kw%L)+FH?e~+n0QYP(T3&{&p9qMS^_bc+Bz}6U*t1 zkI{$i$lUw=PM#d@mtGj^goez}wcS@h4jE%U3EBLIV#-_ozu!%kCnCI=X)$^G;V{pQ zUpa|95toUoi}Bx3G^1Kl8YQ7ZY?(w|? zuIGeu0{Hfj8P7gU8^B`bA~}r+2pjhI0~G*6yTLgZMCe04GO3u)$$|rAbg~7*Cn*UC zX_&kYL|AplbRr;Z8L?yo&OE0RnWEW7l~h_*9V0&p@S_r_qwiU2Ugt zi~2tP!xmyh3EFPXkC%ZlbXU2XO@4jgkX_9!yWkR#pHGqOMz5;1Zev&0cpxf6G1Qv2 zN31hk6-(^bP+!*D#ci%^ocg;TS}_Rn;iQdM*>}x%a%LM0^WR0~QP?I(ufPE4(14+UBz~JooY=h@#78nZx3FKxCz;wR3g^h6QRWG;fOU~l z^U;hR(F4MW@{TSVlJ#arg#>$> zBsjuEzLY4gq6T=#JPf()dqIO`1GM0xS<)`cq>WEkPHDtyxGjgZu%T^_OGI1hz}yGw z@U_q5j%&ek=|W&N$We1n>7z7*0FiTw(FW!RQJCGYY28*f_wIW0WPML}XTidLuNYqM zw8qsh=L#p!Y&zoiS9K%9Fq&f~Iox+o9xIXv>Sd*EoZLNd=x0a#n z{mI3`qd}L3x6jW>;I<;~K3x_udiC=$?fZdSeAI;~Fl z`Xc@%x<_~p^2)z??}(95j>m&oyfXjRC|ggH5x1lJEPZXh_<$;S(|4=9@4W4)mB2yi zG}k&%bV&7D$o(lhdrNGCahtNe)2swZ#Hig zb!=SenVwOxy(I68ZDIJ|8$S$^2U92jvCCb4SDmFg|3j6fFr@LsveUsKz)VD!;|{#C z#)F}vN&J&+uM|VTo!xGT`*pc;dxFJxl@ETj{BxY9Oi06BQd_6+QNNcPa^H-PXMkr1 zW;DxbL}$IOypK6~O6EZjwVNo?7_({k*P#c!kH2d0i0-LoWa|f_Rb*wLnFB3+oqGlw zpErrIXq(U?!!8-i{}{Z>0VaT1mQpCmV#sVKSfB`(m0!@7!RfV=&yT3kyt>z;TDla{HPrcf>R+#xt1Rc~ z)6*xlxOuWa3mhDioL#?PcXigD+Y@%!*goQv3!(~X@mvPJWZu1C3<%3S)EVkNjOFIr zgpH3LPtAPH;)uwP+LXG7OO%2IA<9r^UEG!_ijrw22& zs%-#b6G>zJ_UShJD*od%?FuV=7l7VFuqXQ)qB+wq~;fc&<`AEB7xPk*oswxUOY*Z}e$2^AGmK7m{eZ%OE{ z-@iZ0IKQ3;xhuPxjGKMsSU@*i#obLy_Z`|htYbM>AZEs4>vBG@JO=cjA=Lnm*95ra znts^Fl?HVIdTQ^JVj@HMVpbVXm8pKc)vfR+C-r-gMgkOHWtg9)S z293hrJMDUxOv@`DE>Flm31GhPZ4uK?l0cL9`+W0u#Ymj)%hXa0a_0Zpc#_kk16DtO z#WsOREK)!j{__6JV2`8Mr-{PC<0^m*bmi?gx6{Rq%l#YVUH%bE)Rab7yFpwA*EcTt z>^XB(9G(Qom+}^&NyJW=F^64Whx2Bvl-s@V!_^sC`T25blPT*?)i;p&^isG8M%M0g zuTKbp00P3bKfzuY^B|zah#-eIye~MrsO1AJYVQmw0#9Kem+dKdQ-gAaEuSB^^>ZrKTSx76TrRBWhd)9B?EwO}4Tcx?_85&$mMeFQi|Eu{>qsU8 z+4`n3gfYn7Cbx#Gwco9@0#JwQax-gfb|x3tx-d1--ed3I!sim!{o=SFPiISWnG|7J z0NE7_E`Y1wa}*BTfc)xF+tgSIDzD>NyKPivYMdE}eMLEZQ3gksl-ygDjEQ;#3L<}( z`K*}JRfqK1@C(IJEoWMWwuQ#r)W5CpGEFz15RWYmaoj^`Ps&$iw}4^ZSJW?r`mv`F zQF@4G)CLK0I_*84r`YExPU+Zgs|x)!p;#|(573?TQ*pX28AVpIB>ws-;W{*~P$pJ; zfl_)7gk;3aGncyq_Q-U=E#hhAAk4+I;GEV#xQU~W%uT`$Pwn75S^YOH7C56(Jo8VN z(q}5Ax5=RKJ}>%=zI#d`r8ZoVF)H$Arl}{6oy+HF;|Eh`OtjNkTyZbg&B1@MK{d9s z2)thI$clZ&m*xw3%*uG9657lkX-=5vZ}@(RWyjNBd>!Tyl_#Zg-Vf8GwK=H>s99^4 zc-i4(sL>?r^hafQrPHbWlXmd!;R}lVq#(?FyY~V-V^F|2qFWFJ(#zU0;zLFe(!k8D z6F9hP+`8rmFFz-)cK6N+a}(-K^d!^IP(cdOaen%+bQXTsb26giGAuWb*s1xAYCcr1 zLij1)my~CMwr*i-KVQ-4mZ~l+bud_&5G`Y=>V@^B+CnY#OiwuuP;fpdC8x&N9UMqGyol;0*BJNiZvvP_0n2-Y$@kBO03Ky=APlc8? z)lLT_IQ>?ayI~YjWxvZ@TwtD={I37#T$KvRap{Y0432m^sByQI#@IF|P8G;- zVHlYL`!1&o(1>*C3CIvdrDya^eurA_`f*TuY4a<%_HL)6?Av#hQXCRqHsbG1bb#$b zvSm&cFSJH0dr5iuK)F_v3Hfq@b$YChdhTZ~J5}MW>$w5309sB(XXzwy%d0C@J5n85 zWUCjds{&RGFy`dj`09`otRn5_<5zWWMUNST8eB@cXAH&}ygbTUc-7zu{hqZMSfP5a zAi?AL>OdW*j_gHZA(T$6x>Cd|GB+&$C7;e{+oxz-f_ZqXa|wKR^vE+hEU+khH&rQx zn!KA8t6fJn&BqHp7h>ef<5|a*2YZq!ok*%SR7Z#&Gy+Eao1eVYMSA4@NVF=cp=X6| zU1-`W8rl*Yf%sEg04*bT1qjVmOm!56B0FdV)p3YzdSQWr4XGhQ6@ zSH__uhZzryd>%z(%3?!_uNpFWmRnI)EMJopC(y7Hem(N-?Rh&c32!E443iiK-`w@+ z+s4S7%qM*Hd-gTzAMCe{ANd9XgJ6ZjGWCAP{p~|-qC?iw^6_rDm}&8-$ASvaU&S)z z483__52;rnIYfZs_1;%By*8Eq`t{p$z56a2V=&B7p&YI{ZM}b!oj!Y_Sa0HK`KI4B z!HiENpPZH8RGEWWPdCK1!JF;)z=yRpk=OO!-*ao960Oo7dW*RT(8$r>f13zolo!k5rJi8 zWiyj7PO;x`L8FGkMbFiz0ga-%O=z@8fe+SW1nFJEt|0^Ket+S>mh|1*&Dm`^YSLTK zn8P}ujr5)}xN3f%-QxVCd`$-isOSaS(F>n7^PKl#67150uoF}1SGhy$n-1RQ=;v1g zu0}Jq+IAsryj^}t)D8i%g*{Ua#2Fa}7gwJCp_reyPZsA?1*1L9tb2Nwn=u73EcS)X zgGKdP+}kM*Z`clVAta&gRI^P?_~4OA-I}|tqa<$Q@kwtMH*(B|k=3fjbGi+uIziQN zz&<^kS{NhPkn?iUf}+FnCglER?ILr!U4bn^mz_Z7xhVF`bn*>%ZPv?)YO#PAgekDq zlQ)s`I@@**SyB<+>Uj|b3GdM{lg<$SjrT)f;+Hx&ACP^}Ej!VXxMHn6$7EVPF8^(M z=Lv_Ar3*L5lAfEF_leW>+z|D(UG2><=Tv6FfO;>&@D#ASMJ+2%3aY#0kvE(pPl)O^ zo|&K|aP%cXSV>!Z`o0u(_+Tpgx%BrSv0BJGhTA*T*`M^lpNKoctQ>1`J(`Jc(Knf+ z*DEpIH~X@BKRYSZU*ghubQb#>LN*-cD@hAMNyj(vPWnIR*Q zELt{KH>YzDrb+`y;b;hVs~yN{_V&O!gBg$0VjN~}A*Fuu_L$E-K6(E|E?30IbN2oH zjaXexE#T6q{lIyeklogHc-m^kZldPFU6~$7KI-*>Pt8OfgtnQ^{GU5BIk(YlY{^XnHjC*X(NDFOoTy6 zSJ}U3v~O(a(#x|)E1ikuluGl=uALJq(&=3i>{5eE)Y1!J=jZ>t@u^nYh?ui|aQ4(Y z&K-npI+4zVui5jn^h>Y7v+ho9k11~J8yYt6CjND!1o_6mY=gTXBo)$cIU&MY0%IwA z@RqTx^}&YeU1Xx4Ww^CFxWm@K4;ummt5lk)Wd#w#>;2Jip74f6N2Kl*#hNjyX3PV$ zT&VM4F1nvwV@`X^w_#V@by6aiHieqj2WPZ@ci(Y-ajq$AXap4PM!w@4)j!ujO)r{J z)8Qv=Ugozibh?KxO#ZkmA!oClrFa?q9Zv)fdOw@DDd8Gw=ziO-X|CqpRp=3DhPmOBm$cEok!Ro+|>6c|V@_dBNg-1wH!srZMZ%*<|n zB0W7;2aA5UVLBE|4Za$Hnr(9VAaqtGT^u&BB(OU0srJr$lDdNAp2VCF6Q-Y)s-4I4TLxT0ZUabXl*WQoA03gNw6e`HjyT zwBxIOb5o;HCETQV+8;%l&}9ha%zJS^W!!Nu7j8TI^iOYz-FW1kWX8ea5!1Yhv$}4o zJPLVI$rQ0L`yYNnlxaKF#Tj%$C!e8;af7vnmyvCKDT5V6v!&iFL0uV)4oTbYDGkc` zLg3&+ho7r}^qt`V|ID7!K`b0Or+@Rw9Oq1Lim~7_B;qLHZipxmhVivdqC{ISuZh%V z-0=>7r^T(uG8qHJQAV7EaTr;GUo2^=Fa{SiIjN(%UzI$dWHo$88hHb!R)Du77u-0- zq(iM7Jcb?BxUeh!yTz2zU`!}@Eh^$+_g zOlFatOc$|r%>HrHR$Ewcczl@{86F@CL~Hwh>!r{&3Eb>?1w+jLtwr|Oxg6#UqH=dG ztG%z|ESMKR|EP;$uU%+YuhQ^aU?o^b_ajpHjnufV7z5_`mu9+Jl$c`NO8lqP`a#=? z|9cjx1p8!*${RAm$Qhyty8#7zzUv=f0a>c?)_S3C1h8?_U;3MZyb3d7L@;zfUGGx4 zmM4&rtR5&Me>SH1J_3)2k}zs<5xJ~uELwi>D!$I(q6Jb&Zk@r?cecP6epmC3ticaw zLWqFRO#Uot&UW$Ts~$5%h?*wguaU^?h|EqG+WBgGsWDhUs~_T8)s_VDyp5;hu~}G} z&{*$GA0%(+4{hddwvA>P9IV=E6Mg6NZYhjTE26~*&st3Gt$mixN3N_LBxX$#ID)O@ zaim(XaI$UNp0=xY)~ed+G)!~1?4rE&L$v{JPT6?{@4<-BzQ&K6c}+N8#h7+DE;EZ? z54FCk>xN`-ZF_96mv;PE-8uX+S)t#hS7hu$>Q?wuN4LiH;-QP%m}OxehWkctcwMM^ z98|v~bZgj4q~P~1KosP7-xH-9y=P}%c2K?AOHPS!-E#1^y%TS7-h%S=n^G?QXa^fw zDuU@xyU`y9k$sru%0$-Dh)={=uUGQvU%-#{{~Ue$>CO1hrIJP&cig{a1*DzMk{>!g zeVRwzjXuw-q%;m^fdSW!zKPJ9(&fD$hNe8i{&qH^{bI|F1c~~hE#{dqc_UQtipX@= zBc4@j^Nf!XI*=P&BiINNE%i6d!==gGnn#H%_dGOW6Z>qNC9)A7pK|gC5K+*vaGz8$ zMx0#_kmDa!p--o}^o{iRet{9BfqOELMOWSE#;Wfj`k^p(&)vs%Ud#0KbU-8Hz1YEKAQ9gC17@vOwet3+HjSjk zco?9~OlzMDo=ZkA`NkhMV`Z8kHEmBzG=948q;m$DH#t2#`&B~#`4|~Z`VG{+UX55b z=9+Wx*Y)R5LG18yxvZ>}(q_v@)a_ufd*01)~mDobTuC-xfdQgtj!K{ z7=f676=cZ;RZxG{`;35&VTr$eH_hkn>`Dn^M=D_|u5YSu!6@|kiPSI&Lv#XolMWxz zz%i}+C=oc{_1vJe*X+%-%fv*~%jlGO_dcXwnoE7o)^?u;UiUX%^JGMhpK5bsX0aVu z;4|IWr^xr834?VGes*>awC_w`z{QyKFHE3WzWN9AYp*O#)kcktEj>{9%}3Z+-x7hk zaf_xG_%~5ZkLLML$6GjN87E3oi;aiFo!uCz2>G$TExT`@liPBsGdOA*c~{WL*E7Is z7L`5vq$5hM7tXUc@of{jo-gLLACUR$v_C;@s%#3qH7xN{a)_zv1~>W-M?a)& zGx?rQUA(GN?k%NbEi~!tAt>O6EFpfl7=ts9L_nt3f(JRi&|)?BG^*dN1Ca1#!iWDSkIl82qXTi$%% z!YN$^ewia^x)nKUYSqdgyS3ZCOdrJ=x(!cL3z9?UP7G_@*Fja;sERJwIBwayL&!F0 z4U+^lCTp46I1T{qt!u4y#^wGDy(eMwGf$cwmMt%nBKE&LbexN`1?E~1IQp5xuX<;) ziZ6!rC&F>(g}09qebtSgek&A*$E_?gq+-2mw+WDkaW|?KXq~9=F&)0j^Jz+hB<4)Jgnf} zY6j{b?MD1pPY(yefl2&@r4SK3pZoQaQnqjG_dx^sWpt>ruF9W9O)bHxR>4b zjW-Ys`6;UwR1G4Vm?nrAaCQgIp~>SCywYjm2}&Y182!HvBXT9D+&0VTgS9W$_>ug1 zhoBtMQaBgppyGmVS>Hk550Vw(dKGbf16tnYVs=ux=!t;TO`WeMWxcPx0qq^-$G}SI3$n-rVAzD?93nU&9rm7(G_iV{FNtpv}C)R zctO`Ky6w-b+`a(Hu9Qmh;6;U0?#7Ryy+)6Ly=|}v#|V1jwmNT9&Vmz_a|C;?L}(Cn zHV{v*QESABooNlI#uSl~XNKsBcz2|be@)LHh|~in#n6zK>;=vW%%-TLC@PM3|HGmY zhgriA`F+6^Q@)pwFK47#K`A_G7dJaN@=@+r63l%z>m4J&g5Y-;m!&0uO%0o&cLKsV zVsynx;dgE{r*Z_hvPjRbMOEX|!@XK+kJI3P$=?K!#$_d5@z^3 zC^JLQ(%a{m8O>orG18~<>NpZ-nkdMQ!mVx!09-ur$R0Gp#+nzF3kCT_Q{2+US0 zW+6!#8ru{XcWxey&^AzKJmeoH-`>#4Mx}EI!7kq`;XO{E7Wc_uZQKJAR?20H`7 z(%RvujKb}XRCJe3$`$4aI*zqP*0~qif0jHV0&JNYzuZ}ZR2jnB^7~$2SZx6*NOy;A z@7(kI-cxXI7n?XUZI=E5eNluRwtDd`?CH%nHqez(4L$JXfW9iiKOxkf`Mqz6)s7e; z0tWH!%R>GB7Nr*=0rO|*npM;8p~EOAN8elZ1ybE$(`zYH>zE19+&$e%8owq*Q#5f% zp&-BMs=pnZjsQmUKTDJSg|3Ung^+8v%GT6;dSnpT!8JM=1?I_s8zOI(6(Bx93}d}sLN;ceJ%0FP->?yIxs8@a&eg6Kz4JJQvl zo!@mTZq-y(sgVK?$}7CjwIn6cmRl6J9=M*?`(da}6zndb-{xnY#)w`4x~oKOT!*-B zjb@uAhP7X+C~ayT;M~#n^>5x7*xff7^tR4ZS#aebXTd^z7}}P9a9Ud3r-R`nG^6Aq zT{W*nZ$U0<{qs=E0%Apfu@I~mv#tx@NP-EY-6bBY3Szzqd|(j9-6X@i(EWk}%t)w? zA7u_9>Iwdhd&Ax}@M|2;J?Gg5!#@9T(SO0+G&$51bt`m@g9N&q!xjj0u^5gj2bV%i zSig;-AsvYO#<-vFn!(z`+8&@)4A(XVciAYMq(|w+NopcR8n?GRT znz>^kiz>#)qZl=>?%7X%Qb#YBYQQ1(dzEs~tm@7$m5}e?USP()P4miHF!1JGWWY*EY7iO6&C2D^Y)alSnuZj$*(CciS7=LK(7O@ zujZlK3<4yJXJ>IWcy#6#<8cv<^rIRXtjeR~K=T32>8d1%=_v+9az&X#cp?)}z;16x z+pIX?PEX`$7SnpC^mqG@*nRpVIc-&n)R4RNHuxxF9wS_wKke1nZ$8U)i}6bf@NH_} zELfy*dhkevqg%c1O=c>ze`{%l)>TzmRS*)n`on%3eFg5|J^(*#t4V(EHuu{z-%H zOaydEp`Drag%9iKf195ghE@a-$5z&=Iy|fVz`ba=m*T1;Db^&CQQE6Srep3;)6qNY z)fb;NLXoAa^1+2%kuhzL2A}D%;dU6_XrwlJ-*XLL9?aO3IL^~6n1xG(xear=d%gug z1qG{$Rste~4q8KW@mM8j-?cygJNoALJ!?k#d#sV;Y7YF_(Ucll5W1;f;AK~#m|t;9 zsIp6J%9nCI9PFF%fVYXm0-1md50?@>28H@0P<6#Lp0CGby6D zZok+f2Q&kEY%m%g{QQe$?kwSzL{PT%b}-D?^`_tqd`@jG7G2Wk4_3uOp@Czcu?p4& zIJjW>evJ$(d=uFXB`zqrZ1#iJnx_Gx#=%vy0;9(C+OGP^p`Ku3A{=<(XZf-E86WFv zt*&;%t_2h+u*x(cNo*S=80#Vd=lmXf_C>H6?FTthb1RTTB@)8cURJMx_=_+#qs~!R zcl$RQz@H8~ePl9mZcWa2^w9#{>2bhn#VD>+^^4XW!o=K-LEwRJw2drQYR6^p4M$`DV4ba*2fUz-Bb>fc0mnvG$Eu4EYDk; z>~oZOGO#X?tU{KEfGcmKpA(}^h}1Cjsm{c^)f}z4`*|Cv+%N}SdipO(9sHOBl79$O z&D{{#{P^r3;GdYip7UiDunUtf+@ zFXPF-Jx-cLHPxGUq;5Tsg?am;Y$_>z8c{`K)IlZzr#IU={&{?6J-G-Cg!LCIf}p-0s%qkQADF&(ok4|q)#(jFk;chCT^&N zkf`fJ%jEF4&Gx>6p)}mmi28yL(Q56E)3~h`yueL4^7{Cy4r36{w<(;su{N1CjXNE_ z-SR(G|bcXtaYA;Qv~OG$T!lG4&rg0jTY-QBr#NJ@9t_q^XPmTQ0P z?scE%o|!Xe&Pbs;kVJLXh=o(S1@yJ$!jlU%Uzs$-bFia8OsIP)YwFE<`a%w2&`gWd z;S7_z8Zajr`1{a}lB1Q6k&Zb91s4So40(7uU!xKHj(WOyBeqPkh0jEZJobL>$9%hxY=zL2IedNyf%5h^K{E^U$`)~dYb zB!+33X^l_OfVGYP`LIw7ad#`xrF+%S`rc} z>z1?iVwy_mE<}VQ^e=I67NH+N7~j%~vy9wg1PDShq|7Xbu8k1b_n=O z?Hg^|Hl<<&>f%K7)|@nCC)hU2UA&F39-tYV0sG7U0*jc>^J@xNRqWfOpw z>6&6rUbMMG-)WhtX;FAfeu4uB)W0wim?xP(Zcp9)p!dB{w>SH82qjW@T>&8U)VJ6% zK*BN$1k8!_AZ^RHKd;Cl$!q~sv-{0d@MwlP>zA;<<`L}!6 zdj$&(=b_cPpfLY%lddsua?%|oYqis>!y;?glZ2%A%&Ny+o6+=DLVV7ta8eidz3cAOIh&WrWMWXdU%wVqSvb{~&hrL5T$$`_Zx_5HKo5_| z&NfU`Kft> z9*pnWGZ91y6UU~>>XTNXY7%z7*~8ZIs*4_hB=+|ytPmShb2zy#*MKt$=8fWpElrdun=K5P^xF9=hG22^6v;nAZ*(e z`KW*^BY041;tH_{q_c$3o?E;WuKE;8-#y40ug?tN3TgKP%>hIG9?}6%8+k5fD#$%r z-_rHrlLnhj^ue5$@eAi{twno*r{UAW;rH}(-L&N75hCQe z-5)#_{lWkk&?5>s93F`xzEv5haqgP|dsTdxpJ^sX%AJzrv?wBc7KG6n zJ-SgWZhH7FX_8Ot1NWfUdYs{H7jFoem$mIMVj?l>>lxvP)qJFd5Wa9j7Isad9%(&H zL`Fyq%avrj?CBODk6^i53adm9h_7v>NRc;6Kady?@>BbBd*`OudNxNY`L3Q6O#6nf z*I4-!xtu6?v;>JyJJ3^3A(K2>QlxZ*{vGNWuv+Fb;vF$ZB4rj;4@OOnORb77(U*~(H^63E( zYAmoM^tZi#?qv8%KK5veBmYwa($oz2uL9IdB|d(H`53c^o;L1^J5$#2Xu~U+($D0S zj)zku8OVLHdfsI+F<1Xp9hk>?;4MpzSQG~eN`H0tl8(hz@*flwAW@Ziso>uKdBgV# zuSr54?RD56AOHgj{=fq^Ag~l23=^R5FpG#Zi6wpSPc|}81w8$knQgfK#N9}3nTp!5 zRJ`(G-loNdzr;$pL|%w}{P;-O(tq6(`8R+pqK$giEQa%#vV9>_%C?6%p&;JqVHp6> zRo3!!B$wm|@^|TkWLf2-)GC?&;*5JCC zZKx_VmzZi04}4DjkAo$eo49E-MKn32)umbc``}ydBy>DIT1jh4xV z*gzncY<52gp8ND5>o%7sEa=Q#KZ4y`v>pU6gY)m)f_deoOsIqbRc2+YOTQ6rHt61RY>p>;zS%O&8k<xa*<|t_`Rq64<+NHUBZGMG#}L-v04Uh2l{aX zs;oiIEb&i-5mt#`?;YOSz>p_RNVdec=-%?M_g197x8NqTRok_IqwH;~EGB0v$Ia3E z^iL57NwZ1s1sfhhNi!j9@TH3*DTs` z`8aB~s5y9Z(xdPx_ttb9nRF|rga|0!Xa_C{N+g#@+P->e$YqY8XUN568L8Id`Gnlz zl=qKSC?Ip}9#+@M$T^a(leF*>_~y~r;6J~myP6^NAN_{6-1iC3k&(8>Q|qWVqIQKH zO5Ad%Rs!$~+Dm%9R=cz`qVTxCNtIb`Wt`-h4B&Vh);=hh9pI;wv^}sw`k?BsHUkhx zz45Nh=^|r1C*< zxH}-txzSYhr%Z+q75goMb=uKdZo6;jWU$DjRRYB&`87|3^Oe`J|@dvE88aNz-o z(z05lPHEBrkso}I8+{cWiIis~r01bK=2|gqWS>-7b@h)+!ODc{g_so>T7!HijgqCIa^PL6c_nXD~kG+)($uwwbrR(Y-OaKa5?(Xem zVAh8RuDfPEHbx`$j&)(wV)R$|#QnoiY}XviP=TAjb=~nIC+YtNsDs??#^3==_fKRX zVi_=*?ZI zA=lL~?sxNn`PK9{?HvN2&?mq$cZ7OQ?}bJEQl@Xfru6O(6A<}E zU_xwYIM_1y79=W3G!uEwuH8ciEXX9q=auXnaE7JUl#GAk;!)V$Z5PFdKiYq-Uyt`5 zkYl;OqME8K-}qblmbxV1tLI`Q`-GG}R?RFi<=Opb)OHU!RJuAEr8|d#R)WTk(BQ|& z7pRG88Jxrs3Ln1*?-D9+j3OfhO)yW^*eFeVRe|R#e(?Blel(ht^Hvf4ampgwDTs{A zv$q^8P$_L?cuVy%SK53cK!8pC9IxAe(tYV0beDM>7UdDtxIz)^r;gfn;dpopcsC%d zN#+0To8wrJc=XCJJzkiWa>-E)shrcN=n%+?soP20GW+5~_7vA=5l3q#QWV8p()rRg zTN#>Jqdv0Gy1fxY^YKH^*R3tws#BX_m7lJ}@P%ebx#J9QUW{;ga7tG_0%YVmVLc~ z$t&LL^YRC^NdNgCmXQ53YMi{h#UUhncJvVL16i{HLl3t&@=%(46^;q1R}pUfiE?{k=n@p_5dm$AZ&JNc*~H}xvZ>Ak%=StZCd zvfwp2tZfban^AZDiBR7Bm}XDa2pV#cH7U4OWR+o$eUkSMAF^mjTS=oqWo5=N#-l}L z_2tLw=BFHbVYqr|K2?OjMcIV}HjO|>FyGG?)wfSMIcCnz6LF7TrvIs9?RE0dor{O$1GhS@G~%W?k)07Zy8Jaey#Ru=^e+!s@@MS`;$}UVQB+7nwiF` zY1OrHcr4?qa5xC3QAD-uwT(u4D>n5trhGmyC$W3!-5gSw_-Vjd;C$30$n5XE8QPi> zEB(>TXNF&X7_la7>i)s~{l3$FhGh1%-#l+9?NvH9p~7GJ{sN8I;9v=(tM zNtNzOrTlwBOa{hrLrJRERV;C8^@fyT3L(^BG<*LR+J0&HL|=68RvQF%!q;w&IcQ_C zC=s?-0wRd=?1umm@J&Wh|_@&2D7zW)t#GP9}DV;T--@r7hoa&PEKg`oHU232 zwz>JGYGUe~*sfqDa7RZRgrQ}5ve89G7)%+l6i~ z2$Fa}@~vsAgnzm<+Z{f-UNPV^{O?ICg>L*Vi2e^sRD04zxR{9GbguXRsv8=V;`69A zz9;neD{5Ok&~i8pH3B;k>ytV<4abWnYPRv@`-4T> zfn9g_tmCkh0@qeQYP6mJ?aAGb18a+}lZ$nPUdJexG-dR7AQhH3Lio{jvJ;4Rn}zTR zUaP&bA3)*A7)1T$cUL5JeR&$m#9`CiY+Z|aP%bMJfpJiqKSaL+`hoe}?lLH}X%7c} z!9TV`ty|y2361pLV~!<;$>Mk{%EM7{=|g&#DftyH2ESRNXQP-nbntzY6&=-yNR%9E+ut&b2uc~CYo)C1ZKL;iWQc_cM?`|`kJ zZ$st8^lV%)5JXQK6JDpy!8!aVX`FV(9`_>8SutRLjtH`v!%P7FiN_M?{)f8_DZras zN3mPKwu$F=un)tt$oh0p7hZmUQVmhF3L=$@c8s=Cfb8(gWpP1Z~? zpA^6KW{$Ux1Dtd*Yf8IpdiG|PCC-umLs`D()tN(kME8E5a*=AAr=ecTy^I+j9h+2# zj87(ZG-4d`!0$GRkl9xDBBHc>PIfg07>JWo+TVXBqJ^mMBj)V3z~{5^VcB1R|FuOX z6gFs{zi}+jIa_ez9z0E5UU^yTX)0tW`2~qB=$Ogj9Xa`k% zpx?v78ewO4f$riBWbkBcSaUM8C@T2qv#6l+Cf>Xt8zwx-^)IarnXasG;zJyppv^nC z=sdEN{ zmt@9YNYSs_+$hhJyuXfjFkXg`c|J5Iy@Qbey8QKL!q%-WQICV#eQ^8w7?0JC3S@5D z-h*A_%O+21*FP;I1EXb-N!Y`M+U&McL4~mWxmVo#esvKb;VfuTwFX&eqRplgeZ~+u ziLc$iW@?|pS@@)x*W`F&e(-wWBq%33+g`VSgqLVu2F=Wl#e#t2C|Vz&fd=F|ml znBMtkkl%4tLJ-s380>B1j)||T;eNb-rG>J30 zX8RQo6BF-fwt2=th%ph-w>HAb$cCb(GkH17+9_`T^=@G`e1e!K7;*PC5l&J~P55uX z+3Txl)RcB#+iNJNZnE#lAWiqU-mifq=(2G`r{~~Yj3zd$qfRTd*nB$neKK>tl|=3D z=W`k@=6z2G$=*?Kk@$}T@jJ&TU-t(;&{bM~f83An6s}`|y#dm-DW=jzDeBl+`qd6} z`?0@X<&_J2zJyh^M$nthAr#KpS`$j}eKxuZj@RDLNgnVo)L&?nWkh}nk%bLbVRb?O zF!cM&B|qcC0S+Q4a!By~0ie_RrKeYB`z2}ufE z&w{)|Gslt@EfYi6lcI~5!*|vT3hhcNP`w{CeG3b`gOB}=^Kvr83{<01?3kyUn@HU} z;)ed#aeW1s&MmlPqP6#4FXch%CH4N5Ezgq~;#X#qG=PG^X;Aa`wX3#0!f8OaY_1|7 zEm|l`LX9d7_7Ifv%*bMst1~QOU&3kLA;Y|!P7(1p6-oB>z@4zWIejMfB ze#JnR>^0+<(8b8(OKv3ov8g^}4z+f6PVf&33htBT*utBho2U1DWdi<@Q%0k@@CdoE zC>Q((?<$UFxW4#%447{+LjQz&&ZD>OiJj$9%5kM+SEQgS1p3ZnPU-0i)ye$#^*~LO zSL!LYF_;?n`UMr~!OMwop*@^RuKAI4`_8SOYq#k){Uagan_DlMmno27(&ZfUYxEbFXG!l2ChbNp^97Tb9qL2_20brXVlX<*^ZSp){{xS#BXXxX?dr0Dt)DSOAo7m> z@B=DjfBm`)T3@SVvr<=0(B!zcKs^P@iOgKVakYz+#_tt5l5d`1viK3L@m>aO`_}v>bUqQMKleh0MxvIMX*|a z8lbi8zVcP|UyC}mrsL`cu}s>>NTDAO+n1KhhYY~yYw)$Vz=0u5t~K^IU0(8mX)^Wv z{*#&GeM@evsCE;c>fQ8pZ|?5?!QIxW@cmZ31HTKi(I zsJ{h_>hc3Oj!zsEEA^9u7X->;dUb+;BJ-19B>M4wUVxv5OW?rU&y=I%kG+heoZLuL zesyEy?Jd=l!D(#0spB57)N;DXO3{07obn#`1~4F$jaMwgj}ag{*4HcQYpPcE4X`7S zNQ@Ei_Imk0#z0nW(25HAGz}n7SuT+>qlwrQH|z_fThHdxQ5OxulCfaY$SQm^Rj)sy z29_@Loi*Pj3f*Yo-4*(@@TFcPR~aLvC48p&C zC-^WQOeUsbZb2>JKC_31w2Tqd68k(VE^9D}E`* z@f2xo(}j6MN$WjoT-V9_2B0u+iHuUu-x5-fR zj_{ZMoF)}*PMLUO(-*gyg3aCPhq~vq0{6TKAi*H?<$!=)yx72=^@Od)Uhmh;nSu?{ z{Y%g4m!8iTm{Z?3jAWrYFH`VwSak?_FtgdbV6+ z6NH-xQg6kq@Zg#YZ^a#&u?o9>^P9_K7B8u2tsBOx+1z)05T6c-sA-o^{Pym2J<+hg zWkBL6gi>+7k&x%Zoi;v?HCa#2irZyr3irv8%f~m8M^4TIKu2|e zBk^{Ci*#BEBd;+TP0#z7Sw8-!gmyc}uB8)!Egn}Fbt;I*rVbXLyr~N5J{-AxDq5KcA&_p`UY?HN4j5@a^RhO$lR?aieQ&98~&CCPL zGl&-i;lU&VQ-+O6cAC~W_igVo5eME>u*dNa?9G=nN`vEnh}jRajz;LZqObX@M858E z4erXMvOpG>m+5a;#k6xZ<(eOSk=RKSnm0^^SK8PJVJB42HDkidXUpnEdGnXgqtyAM zC$@vFk^Px?DSaRzk0DS&J}WlXeS(5)xk$m9Y zplJADZB#dSfRIS$@>&}nGE{y_e<9y?F=0&dgnE+V#t?Qdt6qr-a z13qSG@O(3F5$7R^T!hkCWyG+lA+Fv`@(?CHTIf&Sj4tzoz)Yc2wtkm1_$EL9G``bQ z30)`4e^p%_vU_K&wkHPhgqKXFqhCY*E$cAswmJwP*eTxYSEqbkkHGmz?_tylko#8}4R~os*NNK)R9DB;;s_ z%f#ztrKRN&99bWHS5%2Git(z5E)ED;-$IlCkhf+@p$|D#R5dh2#l>Nbj*T^$fx&FS z0!$vRScG^czXgL7ia6>ATO)N%`3_L z<&R}akqgxGz6){MV*ewy*=aDiurGtci+S!Q{m`{R>rrR;izfj zTfP>-+8J#uu#if!nwudHVwip%`&wY%2pTXOI(S(tQ>aM%cj4VqbgN$tj^oeZNesS? zpD={_?qI^;TZ;SyHqmSPlZTH03zq%NffO{r?UvylCP;NME`&S0LMpAq6yvkWIr0k@ zVuj@t&a359VMR%~AQ;iQd|FMqgGnSOw{Sl;WaSGpq88zn%N%w@8W}>d{qR_WRnXb> z=`D7hVhA9PGZBf!C}mmA*aw6Ge==?ChDpGhJSgZx-5MZDXMrUZ69M+?oWx0sT82%s z>8Ci{Yk9xP8SUC1mRjgT6#9DTOAaMsW&rEV zx2}G)Sxe}y>Mzn%{xmKikZOPBF4Vc8jEjn05iIH`P?7QJ%Y727T5M_~sBmUZhn$qP zd%>1IHjdzn=_@)5+t@&Td5nwctFMM#S#nX%{g4!Lw#_#J%hc5Td|q-w>eKq*<8e>R z*0%8eNMCOz#y6C@UlRJdTXZYs&Byj%0#c3*JNq*8ayX8TkH6esSY-=;2_?6-wnhN) ztxFaHgZmJET=KxjjK|B&z)fh{U8xwVu_n5$L+P$`ITZK$OBthVIcFpD!Gav^aa>Lc z5};icLB-x|FDaQ#hOVHGLC5agfb(;?}uCY+Y~gQ&rq77CGU;WEA9_sqHloPI7IgM5Y4A!A{c)ytOzuHFU)A`FkRy_36yN;II8cY(6PS-qZvp z)X(YpIfjnLx^!U#3<_k#>9NHW>AHYP6sPb>9Tefh`hu$_eTRkRYwC>3w*0qJN(IwG zV$Wt0e)!qd*Mqa@r%iPfYC{m{;s|hD0(6QYymmK%u4p`9y^-r`UYqIhr1K8fQ(!On z#~7aMVwhw`-QtKv909D()Db3=%M)Zbtrq#t*GvE3m*E(JP#SKly^m|LdaeKrwY!nR zme+UYd`q9ve#)5Qdd)xTSND^t#=D^6aBXM0whL6=zf^a4o*4_DRFb4}d@2=)s9a;0 znpUviS3a$89Z(~SU_`R0TN%xa*V4rW(O>^HX{~Kq1CST_5UsJ;3YVFgeQqxpTPPe9 zXlH~0yK%5RArcVO>W#oSga#ba845Rq%;fs0{rQGGIyq_7T{TD1AS;(>2txabi}m@w zlxsrTNI~+?!NEpO62g~+fTKO&6mD*ASQgvD!|Eam&o_RR4|;M@Nz#~&opk;Yv>0=r zvHZtQJ@%}(pI{Bm}n_c=A;bF{n%AUIsxq8Rb8}0E@kaK#+KhY-n0jGCsefV-rct z`wt3$TY;LIn#;qL9b9LeZe`;#*2xNP?~L)QwS(ml`U0SuSh4l#8B=-Mliw*pb z)K1~i?z3Qacmv1kdNRrsQa%_YvS(<0o4zn0CJ%3xW>jxmC%0@|4zu2r`$9Cj&QE1N@abu63qTGNj z-_OT{oWFsgHT{Rt?*o$Gg#N5Wg#p5!&DcE+U_J}S;^%il8}UXsC+f<-f`aW~!?0gS|wyczR-tZGz413t_c%||o})@mJPRaoo8MG=44k5rEe z>vrPw?SB-1tEv=Wzx6}Fq-&I{IFtDjrqKJNF1_^;3Khw8_WX>cp9vPhM-k-etph)r zIklti)e;N2@OgL+?2J_$@0yQIno_!RLBdZWK64ALBZ3cmwA$9yh}e`V8Z&#|2@<{7 z*_j61BUEl%E<{AU_s8TDCb^;O&;Kuj-0BVl${|!$$70ZIo40pVxPb0+*n)5)suP7? z{DA0iV<|5N*-Vs-Rc|He`Vbr8pZ;1IMt$tRh#o$ap1liu`IV(`;yohsneS8!H@eh` zpc#YfFt~QI>VKse`)mTRgJk|9McofZltMLWxmj%2TXZ#5yg$63o_jB|s!h=_nr>O7(o znR~$C(}oY8p{H5pF(%%unz(zGWC`yq^}#zTF=rdZp9QoY^u$y{x;6;ce!_dzWnq1i zl1~r8z)-J%l=>jDu_uTPo6oPWh6h0<5#v8%|!UP4HO0TAcT&AHgKt!ep ztwcW_UqgQzLfYn&_BO3u@~CltiQ#{WbVr@am)rUw;9 z=JNpAXzq{@JiTrjh`gkz7X#Y+Ynn0wuUUpVa9;Ni`io_o!i2iXwzI&qdx?>Df_R!0 z!a->@=J(e{i|V#OqAKlQ^>A)@9V0#4@>c<%^9K8q+2D6LddSXSqnPT~*^*cGOY9E> zf-Q3yi|6&Un96p*EvOTjw%eWd216)e!i+ni>*x=eySt7HGdaK&Mgnz9h2dfv&A!0; z*KhkhmNn$XUHS%Ffb32=99iG>020vXqQ(x(m1>*{74YQ7qcWg*NlhEX*Z8=>==BEp%GI zf?WV+te9*2n}eiBdR$E6#GXKpirF9y&225FH_Bev!%y;!eG3KbuZ0Y*R`hV<%R5BRCyBxJQ>Cblyw((4R2z~-ZJs*?lRMRLw9Y9 zQ{JgU7LTdayJ?(Rtg0xn(dHt)EPCz<&_8_g zQ<<-HJI(&v*yRBU8QM+6n5@OI?^D^Vw02ZTwN>7PiIz`lMqMhnPX3t5a-%LTvhyPi zQ+ZcYJiNNIn(*!X7YaVU6$o@R5Pv*kYFb5}Hh(K)0LHE5-*bcZw|bc40;~;8Q0b?S zCpPe5nfS>b+E_;nG1i4w1>%SM{p_zXnvjjH_XYoYaW;So#uKgS_R>pR{sM7 z%(!oz)@;nT(gEt}`2g&GmfVybnVm{pCiFeo{SY1z(be0F#LCL*O^w@ij4;;}V89Ps zNsl(Hk~rAJv0T@ce_sXdwU8!*5H^<4U!I_g-=?Ia+owqebw6(rBH4(`Y0;7^K-E&@ zf6!jfXmXNHf z8!f6i%Aw1!qEh^@X>6l^s{NPqO>4upX2j-Za3e5t-a*l?7~SaalW!PJ0_~LbT`?TX1at%GZ zh4t-t0*lnbMpb{U@2(s=q`ze)B-|pxtw`lK=!uKFzjcay3A6bU_5H~*-5msen+#vt z{zB|>v8N+ZaW|q!pmQSabT8;cpzWZfx%1Xl`&d?Uj?;hibuvYyRp!OGDRU4LoC@E3 z*hkp$>{DA~ z5C-u0vw!}S%?NP6235gc?D}dmNMR0d@G*@8{tFO94BHez#gtH(dauV$o(`~5j z{enzQP4LS=e#Q@$Wb}11GRS}onD0MRw8FfddOlw5?fR+dY;3tOEN?1w;4RR- zPyQ+&m^Q?mcK2~3e&HC%OmfFxC`M^q> zH)b5vM4bkhJ>uh7cp*2{l?xOGO_CbM2&DRy894RpjN>Orvc9 zrkg0?FI~Gv@Arg23Mm87Glq>-n33tT4CcrAdlE?DB3+DESQMaB(*g99HBXSl|u1Kw1)V2FSxVas~!(VcMVj(vLRAAZ^@4?Y{7bwk_ry!^wsz^P;v%cYPgl zHuPoen5C79*TmE*3MPBO$NMr2ZQ|dCvstv~2zz++_55Y#I)AfBoFk%gALJ|}gU#lH zoC)RE{F`G_3V2f#r-w$$n+y|IT|X&W1aia_%{Q3FQPIqdW=yZWzJ@qByAV`@r(h$fYcMfnBIfWVn;Y z#OZ^*k&a{V*UQIsd`5vaz97AkFF;WDEoDp1Fmr;+mvU#E9Er=O@tPkS)5 zd;SqPY=VTkhC5EJ&(p2#ZTJtAYhu4m_H*AqM|-LtrFVd3*Rxksx(LNeUiCV_4`ty57-IS1~#mGkCbo?;y)_JbXrg) zoi+9ST%PFEP7D)7%`dO}8Y8fZ!Di-A^p#%F`d*uf?=d@0<~P+k(qp*$v z6N<{9#tDzrpjh|RL%CN)wG#_|e%P02^7%A`xa`=2bS)yP0f!`eDJONMFE|YJkaL@V z(;mKc=yccx+KKm!(Lv>A+butaT^%QLixaOba|I;btbCqhgfhjlh{0X+1ld+G0udy+ zgh^SW-BubQMjO1hYQt(i+51;kl z4;gj*z~%1Wjsfui9cm`ylbC$MiF+8PSaZsF8F!T`KPz+(Vek2Tl8yuvqS}^2=@(dP z<*~&R4t+?70c$aXh1V8O%ZOl2uXJ%*3rFq#hNo7N(Jdd$eoe!sYAhfjYucI{&oRIR zqB`iuu_3TzzRQwIYS5Ib?|kdhF3v-%4P3K*YJLRXJqwZjU6)^?7< z24VuKV))5EtQciG`5RduFS=>>^(GB_%|eYj$iU5PN*HSFXZ-hiOrXc!rRYCg?d1sb z%l~XSVeEMl;5m!rX|BI@K|&0~+uu0I8D{{{LCY}nM%QTlG4$H&U>5TjeF zc+51N7E_MvI`j%Z?5&|cADreQmT6rHTa)TKAG(80{@~{JJ2}s7WzZ66Q|w2xl6r*c zP&|ZOx!osLl>14r`&cqHowb#u@eb(gr8J41y3u@_`lPcrBVX9`>w((BGodA3VUFKMCCV(cyq^w^vBrg$|j* z-ft_5kYROoNkG<1@dMBm19p+5QG|eYPyw>TA5wQ&0AYInn0d-Y-ju4XWwBWLbkmuc zH$LjMEefVz4;+~oUfOl(N@X*6zcqE`4wF{5A!KKtGXwjX;{j9S*N3Q_#?6rE8XMv3 zg;GD=kS_5^)pTtUMsZA-N;Z~0(filw3*vI0NJ#&0#)x*hfD_3|dE(Z9v%7EFAhzc_ zV9R$Eh~=#qm&?WPA*(_rE=&O*vr?5y>Cc(Of2LbCHax-&E4n;{U^m2uZ8XbuQEF90 zl&zQo_le~Q>43j8F_@TI9H%4RmY!65@tzBqQK8{IP8>YSbUf3{cBPC-uIx8GYNcAi zKYw|T_}&zwy+Rei;K%YOyxCc1OzDPS)kugaSfO2UbWje*7nggyStM$ z4L~~X&ddz*9>>cx^4sAiXll^OXE=%moIOwLQ5T8OT|Do?Y#>YAPi!$kmkOJsihqF=rT>ttiq*^VYvM3LeRDL>%#b%0t)P4B6o`aSP#@i!IZ#qk^d4p8?Q0~}vsj@;)Y}D#3MrFFN2*+t zMeG<3Rb(BGyCzlUr?ahuA055|6)Ss|vyhw&x+$y84LC|wRolgd@5D}f2_;@bAitV4 z@rBo8jgfzGgWC52QAFT3!7!R#ja7N@Do1CFn_h~Ku^mBM>?~HMGBx`# z*i&2UV7toFlJOLoVag*_Eni2sj{H{l>GOy#f!7Fd&b4*KLS!V6u>2Qa67qMOcT=BN zHX9EPz(0Tg{sUYjW@a)0rsj~|40jh(k(*P4sFC!~S=C?2O-*YhRoOQz{%ze^o2@1h z!V2_Witzm*m2B~L z1-Utq(>Sg&hPo1q?qLt8Pv0W-dB>wi7R=#y&rT?b!2{)x-uf{%cV?RyK z^h6Jar#1Bm5|%=d0AH1#qHG`*So^=%zSX?}J_)mPPZkJ5e*iv20zFMBLy;NKwqKT; zNzbDHG{x9&wILQXdQdZA5b?`N%L_|sMW8U2L?%~q55M>tuR%723%pEXP06CF`6AEk zKsC3ICGE}<$72FWj~Sj@)u7h=U}mmAYipUb$rI6zqRGrsaDc5(NA(yUSWNxNJVH z=0=@*)yPNyma9Z_0gLy4hK`AUZID(y2-r{ix|S-w7-{amUW5F|X@o2HRL29La4mm# z`ufPmX0;ubYq?+uxA!Osp;^=+KVs(#botPV&>@7wCSk`1EBRHqLYU1nubsc`I%FKx zq>eIp;3A#V>yLo05cuQEi#e_J&A-I$3iUmV?K~5^H|`rk139_0`>W4RtG@o$NeEonqw@~z-+ zSyWHyStN04h{}3Y7xNKhTKv~G@l>#wwOmqgKVyvPGMZkh;soL9FbiK zYg2Yvi3%`CPprC;o5*cNdz?(si%I70qIm$&;KP` z^mnz3E5``)=RWBi|86`k8Ovll* z_4h({pN`fu?qm87JvZ+&IG}B8yWOL&;DC9%nV7SikTeM1#zS=W{(fW4bs=s|KTwDd zh&2gn5)zk~L_NCLa-15y8p$^)nu7#jYc(ZrI7yRyW!O8}O*(eX9n13b?=l_UGb5c? zC{+G;_?-)gbkNgi^)--xvq#B+N}$}_+o6TX3g)V^OmgB_Zw?^c35>=7Cjd|HCXynV&U@zL zhg>UM1rWJ-4MsAO)iqbU7z7_y@fjc2!137cZ9M!6X}U3^sN^3T;4byd>)ig$U5eQB z;6M>&i`O$_M*+gKyo}b$gkEgkzq;@_b&(-x0Dy;l9hS_< zU|+B6o*s!XKFp;$T7WMvu-4r^5U2^9uU*eR5ag2#7wqFI5LA|q-AEadDEE|^Kw}(*!B8JDUG!M)k5oX5D*A}j$qTEo z0+8QlP3yn|kwiWa&C@E{!C&qUMFT}-Jl68I##-U<-Z1R^@|O4BZmc2c3wA)L0T3g# zX-`baIIDj(Oig8n`FP&?zvi52d|+Z?0%`=i6}mF?4AS~vKLglJ08D49{`nwbO9u1X zgu=h+!HD^+fW7;w+4x%>5_-kf_z*Z4hR>B`okflyWy+ zzyH=UjXr4zu}94e8l=uO;C!Sye*aE8y~RKO^PuvXICih7SJ|5n?Xf5ppzPVRon_1- zvV)j8U=;ljKrzsTAF3 z%2#V1Ip7ar+{7!#Uv3cYdkiYvkWZjTL8)wMpU)HTh=E%u!(7?aY$KC8Sk<>!OOd@+ zpA|bgz4!?{hLUsHfp=BMm|xj}Dis>R)?P~j;|cqhk2^0$I&M61sERo;k0J6oThKSy z>Ia8ezU+qSBo#eazv99$KTeV{X_EfLqohKBdfL>O3OYZzd@~aC)(Z-TlC`?^wU#Q0 z<*TwQC>#T0(rV@OZN?Bo ze?=o?v$cOrUuJs8)b1)C2R5IJ#W+&%SF`QWf5YRS;(_BO4bX{EiBCmPSBC=tHjpYY z4J)Mmo(q1~PXIma;LWUL#HGq-!4vUi{c&Wv%YXAlPD1{a*<}&h@602h5pg1MY2{Qu ze#TYbo4m0&I2r{QZ!Y)x8hi&b=p5GeSzat!qV05Sz_AM1HG9ZHeC0=$y_gzpP1(d?IY zwqp($eZO*d*}d9l}uD%<4_L{=w`rzaZQX5?0<^< zXAuN7b}7VjEob2+LW-w4kQ_m3mh)f#Q716zzHCs2hr=D{Xf9x)@hFBeuRKzYXA4lX z*;NRT!%4dz_l!0>+p7^oeZ1hbd;mz^!#@E03J>YddyD0N_oGwmuky>38|xmYCnsyB zU%#xEVGpCy*I2YD8TjQTffDz}#6Wxm8!(7g5kEN5M_cx)}f}=KLi-t=cF-+h;Y0+DKRFF?Hw)Mr7l8X2P(+hT9 zWR&}D!km!Pbt%bA34r2oV5@z;;f%3dw9J2Od;$6#F0Msvys8XG+_rHADf0-<;mu<{ z+Eg>val;41kio$TF-=9v+xuFdajM0R9o9b9lf$?zPBebfPRa>Ox_fw>B1$!&MWy)F zfZa2s5%5rN-zk1VslGU{PHXoHX-{RPYOPlGQE0&E%x|bnb1@W)$G`&zus?=~GegNy zb|4$eb2_h%pE!g){?i}&L1vT6!z#9gXPUFt%o{j=+d&G>aTay=`u(m6QI)yUZioG? zd?SU!rEYNGdWzj7aV##xT;Q}XbIw0#pXaS6sl?Zl!=JAr@OT`1aT>>gP;Z;=JqB}@ zcmmjzulzTVoVTAmJnST{0Z&>x5o|7%J^?HVNT6OWi!YoR&>l(Hk!Vrwvw(%V-PjD5 zlYEZlO+IEo(u43bxJI*So}o=ypHRbtu%y!{rp`us2Cn(TJvMuj0={Mzd0=t4`Phuk~Y5b&1?Cv zR|ddHlRLfNDbakooU1?pD%X(vOA2)G7`yj|xD6+Jd{aS8Q&eG0_DbyQG)OHqF_YPx z_wPe;@OVjP#3iJkq9OO z6V{%JF%-4G7sJjham**@crJ3@ld^M*6{{jraI&DDyqlc_|`5r-+q z3nSVwxgqRjFDBs`J)-fnWPa7$(wbcwGZqB-G-h1o+RiG(Y;wDxUzJo<$cxIl5odXw z)G!?|01&io-@g93=68oCS!{->IcMfrvrGEN@Gs=gX7F>-++x>3bpHWyn{X3)RT-zF zfAN{*>b2#|KQSTG=WyZhYu}*$GWw$Dq_?p_ebWPm?|5SUMg8q0`xg(#RE;b6(}evU zX<|IG#0-8=)UL#mAL8JUk(E&_`UxJj(R^DD)9sQCFx?kBTRY~Fs`%U!_2h#t%h9Vt zbI5}X9=dgluSw|%gWk1nZ&8QV;o5(zm;9#vxxIlY!4Akz0r{iUw>2Q5``+*!34ajF z8?b=_$Xg3na$-|VqLo<bJJ2 zJx1)~%25SHH#R4>U(FfiK5$IkZ8*<_>{KYmh4*D#&lxi_*`z3!aZ8KCE2{*wY{r(! zyGj8PNy7yd%JCj#8vFKtl;hE`vEe>`mEfL z)ScHgR^bXvF5RB;FFG#xO|oNUd(Gd);I z(!^r=G9oOPgpo-AF#0nOG~2k z{tG%B5`$CK`U1?VhtuYR#JY{$`|Uq(G-(6Y9zza8s(LcgM+Efz&3`{HJoGuYy|1?Z z1e2wzqL{r;gPSSt&ahvn3E28;VI(;Ig33i{$(M5q^lCqxl8U=MpGY|1lR6?F2`NXbpmX;c($b}u-9d-tP;P!cK}R2MHygtUxl}i zZP$u`7ajbAsvrQ}asy@p>o8lq0RpPPY{68^#+!QpW~d8gIqWy!5S(@h@k7%3Yw2)4 zO#hv1s>@hN$QvNycrGc~)p<8#>~#J)V@a>Wt>t`q=Q5Ye#?~!jA6|$B7d#f9^mcpa zjk&p~E4Q*LyqP5eJNq(E8;FR*ft&+KpzJc)ytshFDd2v>R67$^Hg9JQ%*;-v$+I#9 zP3{1PlgslyMD>ll!D_H<{#7!ToAB|u4oPy7fDl_&(u9%n+0{>(sM*?uiM1cZ36JaS<+`6tFgfKGz@96s7&J-f2?Esw zZ3bBsJd|u?Lfe|8C~g!4paV@cI!2Uh7q!jYLUi8`!vB2$oL#_WPW?7LrkwgPrPBM5 zh#FnnEh<4hTu{{rZzHIz7WZ~03a3lzI_#vjTj%%Ma(eh!W5(0_B0oObo3A{^J#SA8 z3jh%#+@+h<#I?A%0_wPAZ!(5~J{{2+3oKaeRQ6i~Fx4-VGjnb@njao;4dH%GVg4yx zKI7BM2`O-9h5!S=8_jn!m&c4x$1T8F+y8a#fYu(NWZ03{)J7bQb{+n$l^RgUQx%M6*u6eSIJa+!Le*J;sD4pE`gZ zo`zO#!`5w!wn{%=5JC`W>xp0&&n$5@yI!I+Aa7qkmc&j$ z_BSq5ZlQ@dmQG@6{QED-iAWJ7u?>d_>ly=NWV-%+Lb zk||ovOFiuI6nGpz#1b)>EWsrIOfAAAUE)J*V??DbB^8_n1$=Lax=BOBE{- zfkeNgskRROPfmP+v5gbYmE?N2(y&=h zS0o8(O;DYA9XD_mog_wOoAndKv_i~b2LKxLJJ+JLBWRkFL&|tl`_1>f4l1~JGdTWV zfrgE`(Fn2$yaR%0o^VoI=VBfy(4urr3l#1DB|!UD+s4*rwN2X$qT7fL(8TP7HZ`!W zgi#()DL8Dt@utKB+duhBIf=30ff4`92t1|uMHSZo2Z!*377mH<@JZ$3{$B)_Pfmg5 zy2CG;PlYLjWb|>l41;Ftrfve1WzMlgtflwb%?a~T->Xqc-j4+4PaV(=q{SE8^Uh_4 zb;DX7gtB^I*ZhDt!@XQU39R5x$W|&WVIhUXfnK-9C$-|b2lZ3XdC*#sg?!Xn!~uH> zFK(OPuMz{Ho!&~-R^hC*s+(dAe^n z2Yxqb3woov`_mrr9m?qPYdfD#8*!vI4&1|rt^HSba)JHp@hdudv27ept-fA6D+BlhOAI(K#8q$b3-x~}(~pkV$I;lhhj=i&ZZu2HqB z><@zPqv_MS!*+l2faAlIk|n1ccW0+)WU#k;W7DuK@y&Fel4|ejB_+4pqBtazhn|%# z70KY=AFF0a1n39pVjKt7$EqyUAbF2gI&{w1oum|fsy{~{0-z{-OIcxcW1ekAxqXq~ z(q<94Ppm8?_V=$O(?mcPn(M=Ac_us&A4K?s`OCIPr;QJ?|CcCFz=aDL>H4D(946cY ze7Ll&*xO+f$XAS<=mybmPn>X8a4+isZ&dYCoyQUgP>#COh)+ofol;U#PT!3<$TFHq z{+!Qrf?L~z+7~khR%p8-hIfB8RMsF40!E(zayF^^0dJ4vx%5bGfbsDoJ#RCJF;+*> z4cZ^Kl+K#C`8VU8}Y{1s(tWZXn(^`UMO-d_SA~I~gzVqw=O09ZYmQY<{`g1N2I=AgCR)h)p z$w|QHHWC^I{qMQY^mthE=+74l;W{2~F00&9`=v8<27mtpi^GUAId9I=Q|1O^;_iL5 z!{$91)2;R0?W%%YFlB~UjvmOT| zNQ-LFKV53NHv6mkWD!SNDH|Th3f`ESvb~3Yhl-Zp>he>$nqWMg>Wu%s-8_$SnRIqT zu?)>OS^(zFQ7(ILTijv z=sm#o)0se3?|PUm<<_(EFWJ@vL<68lRQ@>wa%5|E+X;p%C&co%>mhuTNA@H0JA_rj zHARFWk+hI0C7>1`tnT9#e89T;9m0&{K-Oh+vPC%6f_&6?G$WI4R6%#supS&XxWv($ zkqE}{c)X>?K#y(u8khl;%nWaiXduZ2G-YIDFh5*mcB=yR?RNpUvHK1>bibo=Y=Ua0 zEowlmN41!S!Ykr#QzR@fQhyLC%4vS}IZ;f4tTAHEin3t_rDL?Ru@R%J1Hzf8QLcuD zw+i7b$KOuA9*8d|xMSPykJDTbU{4!%nYN4CBtt(%2Sa~j;<2ndtF9)r`k2}j3HY-T zP4P?kbT8%l>AqO*`TuU*zj)Eja%au3Dev^LLK&?*?%h>DaXd=HfTW;I%uRD zH{OVLlxL0AR#1=uD<47F-25>rPtJ=sIJo|#r}JTE4#lWKV3a!P3wUO8*nW)6O@V@ce2jBj_C3U1NS?ln=)D#xd%Zw5NSuK$I2ZNzkbR@AZ=W~ajHMnhH zhLf9TWVcQi$@PYB0Dio=>eKH>>Q0Dz5*djGfC#~NWb^#J%oGldBJ-?<5u3t+5Z=Qn zzsXp>N@ymP8 z7ZNSI{m8CGO?;7+&!pfg-0qEMo2D9`{EIpwS=HR0MN4d^wlK=iuJPgUa~W{fD(O$^ zvehJF0MS-alQlfuJBP1-{eC&u%fv#_G8XMXjCV$&UX^-bLU;A$OIiccrrg^_URxv1 zPR7eh#yONVG{^FzfLN+!#8?K}qi5HYarRzBF52QwZ(~o!|KRDXfs7>wKojImHVR|D zr<<^gt7gxX-(wYE=v_vHq(>{u1MI|lLoP>}LDRF^3)YZlB3@PF4LUDj5aa_^TTd_b zIES=CG#uGkDpkbf0OnUKWzCGOcYzzQ5&CjlGC+u{%` zQm45P45b4$@bs>6z3Z-rMZ6j|T!xu733@(jtm@Z68o@^-L!4t&h;fUD2PYc&s&T-G zv%RTk%n(%4jL{RPz_+7o_indY7S>3 zc~X~eA+qmy8;HG#2JXw(8XWkqkHKZVLG?xJ+mtO(Q=39rC&S;%I&$XN%WW|{t#z3`I#W>t$ne|mDVqn4z*N5A2MBRCo{J7&UvGXRu)_y(vR%lzA3cz z*mJ(Kijs~b!PtG#t<_RYJq5Y^w!CdYEyrsz|sy0BXL2oyUVp7Jl9bQ=Mm z)>_IdRV`pneJLmS)6a9jO=1i;v@4f8YwXVm=qz({CY|DmmrsS$%Wg zn)Bid`S}fj&6S^@nIYsNF-f~=+dfk}F|%tLjqrs8EY1OrcahFvb+rJn62Kt{9w^v& zzsfHO@VO}z-f@cv%j?_aG@XOrWcFt!OX}LO0Q2vuqG~6|Umvf;UPg7aKf!u_cLzT^ z*;|?awNS6y1Bz>?2cTy-7l4pGK-Bimz6_b*KkFMk^$>=FQCmh?*a#drM}%CYfxre= zX5yQq*#RDXHP1&F?|_WW?YgHSOTR2-6=ihAn+bOjls>?H2ISrDeJi_wRADF?C9|Y@dkf=bmN}G12o#(USSg7$YQY{qPSWOeU4n5%SPQ z+2ELe-e@IP_6oTnh`ZOL!1TlcUlQG}ruYSB@I@3D4_Reolg ztSbN>mL@wW*`f|gGu-s#hCFfJw7U|Z%2Zu6eZ|at+kNps0W<*MwW>~fXMb5Qjp3$9hss29F55^H^^Mokp|}F|RL7Mpn{wjYDkB~Rzx4Tr1EV^` zH7iv4;faekBDaiu;snnQTSVvEq5<&a;>hdYgYCAHLG7TKU%XCRRaC`sbqSnZ;s zG*b+Y&NhRr_``Bh*xmBreV*P$1pf<$w8GdwtJhRM14trYuL~5b&fUilB}z80a`;5c z2;c(B(!osXG-dyT?(!{gDNZ*3Bpw-t}C_e78`K{P;eKUaAinlmoT0t0w-{tZ@uGENO-00?Ha zP|3fR%}=!mWz7r>jpV-S8vY0rJie_tR8&~$ljRFsY{R^?fV7m(5q1Q$o@<-|F+H6) z>rGZcleRW6W9sPG!wFWo@@{*qifJT#)>nSrP2>I`sSo5@oC24-&vewV+74=<$1_Zv zGpcjo4nh8wKA;4%LUKeFw=1FoEQ96{hJ+Hi zL*-YF_pV0B#TkZvtY(7eZOHAajCd++C127hkofb)-e~M;(a!FeOCsp{PkS{y-Z*nN zaHXp27pl6t7bg=fELt5Nwh8}#cwDZ_h&W`ZPvOrOL9z$0!xv6dm~7#GNztedWM@qr zFqsI+$yqgE4Go1ztW@V0W+rTd@t`p`mdKLBi<3^uDW^VnvFFTx@#edtBP5~X!lD@s z+4&vcrO}Kh%AA{pwHg{qThecaGc%IBQiPBiD`wM=60SP;sQU!lh zW`bLcROs?2zthFA*>4M}k)bg~8MMJN_J20XF_1OPeJLcN)q@YJWu0yeW$ zOvpz{Do%ME$ejmpv)sQ>0Zbx7_Z1o-=cyBEagF|8+CWT+5ZRTT%>3A%&!w5^kUQ8Iq4v^^~j zVKH1(?bZDY`(s3Pb7T-WU8)ISlgZP78Cak;BwtnD48$7ZN|p#o$+%sAzGL8znzHeh zzU);4z)b&fZ(caNFEQMcjl*vb#fomV^em!bQ|)7#salB~bvL_! zi6h2k3SC02=_zpwC{T8KxO>wv1%IXY<1B$1_F9_z^1!b+(^eFsgg!hprit+H{LxOv1I*)5iM3LN98 zOb7wc*->pO0T6e47BWDG2=5<^qpRWGWQp3IC;+KkjoS*A;Pv=_wsksoW!0|aL5q)V?k~$zQh?FICvd8EtEG1?0U)X4IT`^u z<1FStk^GmIy8v%cjFf)pbSZD|s0}TP{wln0I~3^k=f05j=x|NE{lNf7^3^1|QPj?w z;cP$gPQ2Ns!P%<5&!&SQfA{~l!U+>#-H16H&79K)9Fh4NPaRF^;53@fGYPoD!@n;^ zyKviy6U-ISl9bNP>bRaa`TbtuSUB9;S>={)4Keyf3(_*bt)8{B zZ~~CiWvvL4M*FjaimGkY<-+cYp2Z+$iKe`kPA^JpssV(2k)hA{kJ9nv9;;8~aK8=S zhOHkka@PQ%K!*w)?4oB#Kjf#Q^dchoXN~kArw5fhA{!2NhZc>pH%U2N^=VI&H?X>? z;ZTsfB|6NF30vgj#^3Up3&F?n&&vWX+$XzYm%H!l1g**d#ny?-1{lTaqhv^W_JV+` z?viI)&D8`8w@Is5m7X@F)^b|0#uO&?k~P{o;VZw(L?YNqw)FU=v;tZNOz*&dw_*v- z?-n2yYD=i!968?_E(PkMX`@?s%m5zTqyUw9%yqu#3)w@<@N!gEe7wnq|3({Ho`iOh zCpAWq3Gi6}H^|)$gmKaf=;Ro9`JE4beNVicVC-Q6`c|>ngTH@WlCZ$D|5m;edJmUk z;m%8`Eaj-kvq)!?@nquO@xhZNkQLD@KTa84yL>>=DB?6drL(?O8I!6-CSrq4Qjdzlq8O(K|$Hpa{q=R@W$s!GA4oZ(tp5t#1|8Gxyx~?qGLZbBWwA4$dA# zBTZ~)Ie6Ie5Z?KB#*>EnEPS)CxnCP5M6qBXjCV3$=cngL9j-lTYgQ^AC5rnqBIPWx zO0r^Nq)mIh(*Yi=$71~I69HEwbq!mni0TDnM2pMm4qn=Tz~Y@sYmK3V4V z`Ta+Y%e7{4ydw%{@hzXm+SofWP@jrqoM#j(FoFK_6rnM{|*{5nJ3If9{A8~oA z=G-Ae9CcD}h?4~X1*qzuPIyJ55X5hy(hWtZ%dMpk+dU`r0;Y_|<_t|*Gtqki5dVnxicFeUxuB?p zG6Kls(gMNi)glZL&&D4ID}vWJ9$?A5QkR4Sv#zpYK_pU|8uY9CUz#-6o3jv7$f+Ra z$BGIqxOD0aDOOfc`nrbwO-beNTo8WWxA(?#88IY8KI0P!gkVSE6X5u2pW9Y9c=eC^ zj_i=L+*D=Mm`wB_XCy3=7zkMo0_j}JF8t9-JHDVBQ(%5K3!6Rh8gLr(t2%EIfncP` zq5-c(AcBoUIXHcR1*9(mN7Wn6_Qx>y(U8wRRciV+;WZCBKPL?0^cQDX`)I}e^r%sn zZJ*E8yuwtj7f>&U95T7B$)!JfS!6X)qDrXE)l6h8y?gy|%L%>!at?A6lkInY0~t0o zxQ-@66AH&s#iNiK>L~3IYwJC#as>#jXPfFj+pcX25Glm6%2`Ag@Oe-9dOm^V&bPf$ zCEmkyD7k6hyx1$sex0x5&2hN&X02I?ZtX4ZUv$3A`7F;r?TtXTH^@ZVo*}dNdacih zNWR&&&Y`o@#DYrkb;B}t7dmZd+8Fv^|}#IvRL+OeI>PZxbEV} zeMBl>#~7jd6^LP%#_sX@G_*B_QQ^S~iN4RrIdy*)+U@zzHp_XqGlILK@4AigpamJ6K9-1?vDGsBMz6VzWDlAuy4N$DVDCGQv@!ql=vo71imYDciW(UByO3!? z-uE;aP4(@xZkT3%E)(bFQ5gRTJru5;-I*+&KzLQSHOfU zm@wPDUKs+HV4jPg6DU?uydg%T|)~*XJKlt<4(T zUhGJKkJ$-e`v8{P*&U`COAiY+|=rnn(@{?H$j&(k^ot^Zga3Mp+u_00*hY9wR%Dx;#(gQh9W z#3iYPhSObm+)4PqFI6amx3dzfx+G?;Sz;|)Vx_xohNL7dsmD%)p7ySBr=_uNjd*c8 zK7{nVGX^DpM<361v-ZYw4OL4knt!OxYx=jt!A3{j__5xK{XMK0+h~71(BQ`ZTNR{v zS^h3#WTZm~R)#04Jb88~JToQ`dZ3?CrZ$};4VaJ3qfxKpzxaD6cWeuL`#~R8&pa;3 zFQJ8|dd_XWUY&97t+#cLJ7Y@)hMg8p93H#gG6Ze-m*BOkjAMss5h(Zy;kqy6RAV3y zDmJ&Mrcl>V{(Zz$$X(u+&%8_n=(RHF;K*ArBzLDxhuSG4DC!QUoq`gXVLh`rNdIir zcPAK7h3pSU7&uvJ9qBWN#gJl^ofs$rLX4y!#)wY26Ic|Ld(bX*a~w+<|BwMZ*k86M zLyKIFvPbWfDXjI@`M=|=b|0Wn7!#h9dGQ|)J2I1$a#u_l?#%`!ho zI=xe`JULe%!fjdy771U8MbM91(E+ir(Zf=EwTSYUtMt`@V_hTWBp_rfP!&ebB2yei zjFc7IPDfl$z*VutqYfp}g!5i=G2V-)S9Wc-g%fzn+-u&F)@)Y2W%*XBnp(mod9vCr zCwF_IpW)i+}48UewUo@G5Q}(NBP+7|`UA(X&+{QXrv5=Ym56s|kbIJ7h z>_>*_-Y@W6miV(_*pKmgDVLc)fC_J1;iMj5ZaW0N*P8-f*gXEq-=iPGogz8C<_BE6 z-s7q9ov24s4@)c~_~|oY3xb?-c%@JP-f_;<47qOMc-c@Ezh&ACCdOt>1V)cP;d!V0!zZ zV^e#X-x<=P)n9|#z4DOk-|_!8xjBBXHKjHAn#tn zfVFOaBmW{zn|kVi68%H^YyDfuF?QEts)NwfBQdTxSPRk@9B)a6*SEaMl#!E0dG6yn zf5NkW*>Q40f6m_M^Q03Eb?5ZY2JpWdoQ1I?o&Q1G+-c?OR&IPh-ekv4>ZAS$sDE7Q zo=Q{cT84#Y>krcI|1y3qV9n9(~_7eTlqfMTe91yGi-n~_++0U4?7kcNYAjUty(K7^=#%~vL8+1}nh*c;2dr3w+}|7`P^ zjv!lvQaaJg>52WpJe;XUbM8!9s8lx*!;qa@_jvBzuJmqvwMCJ$EF2h%6CBg}O}Xxh zAT;5<6K|8@dWl=Fj@yDz00&EPybAyq{1yU5;I&T>#&`hV0(?TkT^Kd2cH~EQ*a7bs zgka<*LqBo>EB<78OG4siCM)2dc~+RX*=Bi_zAscbVU%uW|0Bkn%ykODAQ0JL7Ivzy z3zB&mA=R$jQXMRdSC?TE*KqZ|VMGCbD-xOyP=ZB>PvA`A;pW$@62c**Pd@xH0yR!> z_w~~Nz##ZYg<%xJ-v~CsZduD!`Nm*DE!VkF-GoI3MpmY!l1e9)UYMJmHxgi*Xc!Q* zft|A%cAIrh$*9fw6U=R|ZKRMRCAgNPE!d@%_w;i6%R+F)rLA4?*IM)RYjwr(k5?~Z zSC=DL3Okr7Wf@J;Lcwkoh{JHfHqiu0C2kYHq0@#`q*iV0?kCHB7Z(?MA+~jYXk)cZ zGLHU9NkBV6MXQx=zkdhHH+L*-_bmiytZ6sYqvm&e8=|Vq(wydsxLuCX~d6`S>oWQ2&Vinv} z;1q0^&D_>@We_J@|4Hgbp^xO4ut|57wFHcmUnlZFuzEW8un@4Seha{=wh_6YmlCOu zE9kYVUB34Gc!N+BVd(#?;di#)jo#C+(kex*&xKLO+zq_5e!5l+a9B|#0N)|IbpoNm zYo=D>8+_g#=I!p%H4+c}4Ed%eGPW>+mqRB0xxxEsSYh+B7=XG8bEXBCh3STo>-2+A z{CRM9mj9s8~YF*l<{ zVb)|sg8}uP1eB4c)91d_aR7Q@?I@`^6;@i(Q;loN2WBJy*KHy$5-{A2T(q4j`WEy) z22HDG_PS5p1(%}unTqHMf4Sa>_$c1H{`+J}AcL@>pn*3`P=RIQu}oaW`HM&`dCIWS z-ywz=Z9{X?NfAxIr!%RnDG-YWG5eQ)BJiIq-M>AjsS%FkySfpfb5=E4+N(8%kLu`+ z$SXUq0i%0#KwF}%r3F~`^5&B(G0sEX#r5dvlF~0bW$I(QO=iA4A?~exXce`)w%81= z!U_07a#Uj~xW?zzJFUi!*FbvmsPH#htoX=SnI03bAyWK>Qia{iJq)4T$qx094 zDYLqS-$%d(N7C#uSFVJFP)&PXAM7wHJY9^inm*p1`peEYU>g=PtVWhh4HIm%dtG0c zFTZd`G68-eYMpXJhv1E2bKOk$>>exE)MKexNHb73djLWZ}NXv-99UZ z(Zwfwlf<5D?w`1;=?N}PY;;YPI&Dzd5# z?ZP-xv@bt=qgE8HP*``j(C1)Jq_^Eb%0mgu=pA%JTNZ(Q{PAhp(gmR5fRZb%^VdyD z0Ey5*o5{(e-PMuguj&*)uQ^pm&ySMiFRta6FiNgE3rJqiV|Fx;ayZV^_~YY3l=CH< zR`ZGNPaPJfRUNyZ*DW2;9Fa;x!7K?=4(-_eR)%0(JiLDSt+#}ULkXU2_#FWKP=^CV z`%h-Rm)bk*xPrPy-<$6q zKi)jkIGC1?9Gj@_lz%n`Hiv0rGlnLW`lnE$RP#Wds+XUCSn*#ZjogliMD0SKI18S? zbF0GzqeLD`Bb0Ynvw1bEp9;yYIgg1XJ5^c%sl`=4LtudyIm&NI)ABb5L{-l!rPk|maxT}-Q-K63VEd~_UWqr83)zCs$Og%)LHXGX^ z^uF?w3wzc!h@&0xuh7x%fatdBa5UKNx@fDb(l4~XL8c- z_sTyyEu=I%Y3+EcBtwP3r4P2;H$UyP8Rt75zb_hOy%-N}89r}Mr>hS4znJ^4tdC60 zB|xDFg3AVSFeF#HnZFFrxP~#IeXFzg$BZ8?{gOLd54pY^=?eCfRzi~TwqNtLX{!i4 z%_YTNJ~XfRQaX_k_hqhBwl2-y{4m&eYRz(*|HSKmc;V%JO}6Z>%(vI9uXulBN|Jg_ zELc!Aa(ImMZSdT~ZX)?&-fZ*`nn>DqB~*Nmm#E0^SE`xjbASx^$k8w9?ES(HSupU$ zKJCc|>|K#RFCEc|&f{E!rNe=3vKlil^sIx4$zbWM0esW(qoY&>8*OLKe$b9Y>TLz+ z{wDrn$A0L-a5e=BK>_~<+TU^?XJENzUU^xl@2V{c%`P;2Tx0r!7-?R5ST)DJv6*lA zZcvQO7jih?aJ)F=P3EuH0rcyMIW8lA7|;At()>2Vyf;F@*%=oi@-(2XeaS4M9|Q8} zB)g|3blMgowY>)9ILb&ojrdNUjpF`7as%a2gTV%^ zNBN5@c_chwc{aXUi$CTVTPX1OrZ)B)baL~xnrV%s{O?@Y8E-@F)E-^=IEoG55h&uP zc>B+;|7i>0SavSGIwZezZXGc=nT*GPzxH^bRTHlAo^ySe+OxA3>739A0Wj|IpLDb6 zR-%U76@@hHvIVs&@wsGL2og#A(VGasJ@}@i{i_a~G+O8+42?dQ(OG{W=`Nf!z~>kA z$5Z9rx%0D?9!j>u6k8{(KHKdedQw9z`FAi(qiP!X`GZ_!=ARlC7S;|xXOngQGPyIu zDwRRNhiacv3FRcW^|To89+uD8O{0?AS1oH_U+a4s%u#lJV+f}lFE!2y3Eu1;+1z)X zZM4L?3uIp$=-S1{Mj>}>cmK%}-2R=(Su}E-(&^(J@U!-2B0}v~(bcTq#nwB)VW_{| z?2052QxaYbWQC-8KP+uigL-)UERv5!P-VvljIhlSI-GyFyG5Z#d z#w9+YiJPQr@`a3g-|vdMlGoO5N5B3_p9{$j}!*T_woH zkX|lmRP31fMdRN`lojM^@dqM zj#Mw*iHn#{CQ*_)+39(g`J(aqP~js~q1Anc;BTkz;c(hu2JC`F4pudJ{qp)Wuv=*5 zVRB)nU)$jJr#L%ed9PxHvzHQsnO`!+|B|)YuSrdJSKgr&)m0tv(-{BAqWDx+Et0#>?{=CT9Y3EGE+#GKw`_r^k zFYf`xNg}UkXQlBQ51Q*UFPac%gRwuohv`Yj$RbT&a(EvUveLM43NmZ(zC@9nldgUB z%UO6Rb7&?=JTZ+;c5Zb~&8-K!U)jfsTqn6SuUw32RupBj-|thW4mPi}%nHY{^J~|~ zk!8tEjirq~&UKpQ1|U#jt<9oH6IXnZ^5xS}OjnuRyg&OpS$n!YB-Sfo+8=cw?Biiq zw&fK?bZe@S3V!kT4WY9Cz(d4vZ%!QWxlSl z?Xo*;=T+!Vx#H|7j(5jfgsJTER+-{)-*OugIQub#6_*9Vo#<>W9rolgkK&}+ZGC?q z6?^z(KNN)_|FPD{`0!@dHTb7j@8$QAe_IlA=m*oV@LyK`94Y71v8lQiOkCB-HFF-+ zzv~CJ``+5yiDpadPqe6LmzhL^!E8ry`|6URcUz*S`Pwc3@vd7Dpm&ZnkE#)o;;Val zeO>zF=JYtjbMYrZ&1}cratO^6kL3CBp>eUQD(#pL!7w#6L{IRd{=E)NlX4GE2F{Q4 zkJVY@tpQUnvV?(;`oFujmH(?(!2YH%d;3$)Q?>c=@xG~9x{hZX$F={84}$Ous3nyd zg%D{hN9f^RNyL|D#<92K)$e>#RzY8`td0ECNYdCKSLOmbzqi#_-0=qZSu14bvIs%b zZGe5OC2Q+7Q~6fR1gg&)e@z*GpgV9}thwJ4=&9d7?KGtXXTH4r8gHMgc$U&i_w^r| z+VGcK$YSr;BULgl+bZ?HSnGX<2Tg+ZY^GfN*~G0KUjtr+;WqMp*2+C+?Z;nwohDcf ziqs+f=`Ank$<}qhvzEh+2sK1Vguc##H|inG(&KO9;GXWbxu5rLJ1!25jO|W;?M?U` zg|1l$yKOzB$F4N_-R1dx(lCB zM5^-Kn7`(GYf6WF$zu4DR;lqfa6M(;OL!~jh~u@L707OD%}c#CD{Z#4n`_5* zd(I;pUk>8Ad$SC8^>`}s@^-Ffe<|HS^cF0@0k@*Zrfq`d6Yi|KnqaEk_7G2!?a-&) zk`l3D(@41+eSP%+$sXYxKKm!F5A>(A{H6 z;oNF1SS{PT%0LjH_Mxh}Mei?fxS4Kd5Hdsi+K9fw@f<8rs~PudVyiupc}fdLiFbyB zn=Ydp%sn*xcn@m=BZDN3U;H{F%J}SXN|T~_l21A-n`ZWySt_0wKbjriX8VWHg4aeP zys{eXAE7a9$4n23jSwLos|39UtEI_5?wf)w=C*re&WFq1v-X#AF2g$R{rHFcz7Ioj z+;Nz-G%E@R!;9f~PX~jZek(;Hz{!W(9nvv=nFE4bvDMT6RQ>}l{f;*)Gs}6qs``Ph zs@{Ph7KZlYR+TE6ws)N@40kTur+Txss(`fAc1deiFHbE6FDkD1SG)1x;g_kalF_WyPFrtwg=@!#;ME0M?~d-g2FkY(&!jD1UrX0lX_FqX@{ zmO>fXB_vBGYX&t8*$dgX2_uA%J!B`_eT?h6{`Y;qcwRg&o;S~YKFkN_c^=2{TfV>V z?|Yo%WPjbh8orX!jL1Id&van zmpb52a|uLMCLwZ_b;;_KcWY{iUtflYum1#=(0=Q!koii6DTvkCAf>8g?%j6x>OjEo zcuFju;GZ-V2(Ryx89I5c7shNXW&w{hinBo=Orlv0`|K0#l<-Zp+kw~QIJ@l`<-9xq zm?(`KMK4I$sl^0WzeBss;}nnfro-KA`Qvji&0G)dzXZ11kup3+VHlN`=tA$Ji?PCU zt8ZsJyA)Hm3-jb&j*JSEMWnSAYV<3?AD_|xjjY-6ing%+}g<_aq&%+o0bc&uY>UHHk3>yD-@OEjamzx zkax;I#^8(g9jy)RTv}!_(a4GXE6BY-lcJ-i2}I}EJo0K*c0pcCU$Ob45^mpj!zWBl zkts(AHo}&&ZB*s=<#k>R?~?_+`Yhhv1I&?{TTEtWmqsxa#1J30ruzI1dMWu&+}5$1 zIv!TIURkdN)7bm?vA-hgW52KbdC7q7W=|*DArq$cLOYk;uZo1N#Ri`HvKg*}%|is| zfs}Vi#H*n7@#(;+Ft4~?Df(5v+@m5F2E*LR&Ae`%VqtBaqLv}qywRT8vB;IrJnitugS=;(U#KQ_pc0X9P zGgTxmx!71P1b)oVmMZyJ=8=)H>b5c#oOC;c&Yu?L!IcbCPVo0-!*NYRI)>O!ov;vr?yJ+HKYmG3b!}2is zk(z_rHDIC67r;l3-u8+v7d`ElY}X#rKm6P>dP8>PhCi>ZXXQ$g5jt?s^-AGI>zk8% z`23~{LVh;kSFJc}zbo>!@;iO9xp;{v#vnN66`ncSd>ObH1{2>jk`NzN%Fgc}eEYG} z_n@_;zE(7mW-~>$NYFNF+At%?{u06b#jn+=o?yHFeE7Loj@>(Nez)ewV@g#r4&U)w z@3+p83PZS#e&@&6qXU&6uCj#ARp6&r3SgLBlbwU$@^M9H9{xcH#QyMpnxzg|_GjjV zui?{7JWRcNz}_j3EC_$ZF!`g^M%fLLfRoPuDcgN{eP^(wI2-d+CH0pD} zdo3t*jo>Tz-Bt@;tYEnIb44YHj1M2PJ-pouR+4cy#&MHh+beIKnBlatoD3xD^KOGw zU$?5S>>PA8n?E#H%2vL7J!rGcYje|__gW46$yK>vnGY~iqv`l)^ryR+px$XO z*#gzPQsU}^Yq1rwN$D;|)wqo(4*ahoSAwUf3AH7)Vug)|!qyyS>nR64wPiIkN82ig zlRXKQ#)xGQ%L&99+CzfSV2pB?3Aug$5&ZCRT47h2agp!f1%j_qg+C~_xnsXq*7wma z!}LWGSl=LT9|IJ-ZRfO49!ETp(uuBcbO+LACI-EaHL`>@CI2I1F0!sf@nDrq2B4Ylso zRe-eR_H}|GMO!7lgv9dM6}`39f$=T=@{5ve9;;vd<#~X1S)e~sm0-7n1x_}Rqa>45 z?lmaHx1v}BBUykAYJh!W=b*%Qy+65|**csC)-D{($|R*1871{!P=Q40pAXo7ZxI7VTr zIlY6;7Q9)p72~~)9aBj|!+r+x4Fhe%x5xa)T?EBrA9RF_d+Z|6u0t-t#=KSL*Ca zI~E^0xo>rOMlRR*EhbkGN2Q1_rvBb^e^FvGqLjUDwNhO(mUo}aLph^3z^*8KaM3R6 z1Gn{x;6&?ild$-K?>60?<*L1X7OC&Q--s42gv#tNgBHy|Wsn`vMb9?mVo8r({y(FR-WJ4vgA+0_f%X{rZyl%-g9pdMLs zAxGlIzn5&fRE+->D!~y@cgshievk0F`*8I&=P-UpXmOH$eR)MhZh!j>l02fESQ;ID7_9Z$9W^uix|-vw*p z?zsilWU^aPVg;YKX4l)ft1AZ-nCTxACLOvZADq6{5|RVGWY%3PJCR&7nQ}js*GIkCUyeVT_)HiPk9*S4 z94M6o8uL=_Me$HB3!jlwYw4`q(Uo`rXQ7%tp#in8q9kWLs+FDzU|s~;ypo#9Dqm`( zJ;_sjFf*7@w-{q#z36#MA~w%xIiHWUW^2RB7jYqDX)GtXrB8_CGstM;Ol$c2ix3C| zOUs#5BlXU&jTb51nQ`!ml%teu(&Kcq#`#Ba>e2Z7ooVV(`l_Q*i@jgzf7aR;e!TwU z@wxkQiR-xP^uXvq*3LoJoxPat806vD>!rp=9-!$#ho|-inW?+L?6>k|iFVp5@Bw6s?I&6#=+A z@M?2P$I_z<$CH4>IS^(xw`{?Z2KO!;TL!}W*$y@`HQ&Q#JH*|y9tQ^_LmoH1AhO;< z1~UJ9)?LEPTEJ<_JKvtWz~+zoJ_d)aPs&-$-6peNn^^CLMsW9n#UWqR#ZMYy(Y->%8;pOgW6c|}%IC2eB;UV1B-1lY_8 zTe(&Q?F~H09`huRzPMc45(BsR25^1#_~`2^3ctrb71#M%Ck8a$Un!)>D{iBqcJi}E zT-&*?aODCci?viH=O|a+N-7wOm1QEy@HGuYFb4CdI{f4+?pa}j#1Wg4iNW_T zVQLdB$sYIlFlp;@^xq$t7n&Ct7f7rezfYv~$i~e!PO={*CSKs24T3-qbP=cNlQO*# zGS599Jq1mK`~dR{`lh3YTBgR2@l+6AOvEUG8W~xK*W*XU$H%+hAkK}WwRWagFsZW9R`f66F9!)I*3f;4N~-`Lp5rR0|#Jq$ySBKHT@nw=Xf zye#xnv(pYHEnWJ7aq;{53L14ZeUUX(VAY z{k+Ead)nMh`ebbWN$3HGrr!EFG@@TvRJ73aeCu$$U-Ufz94#1&7ZV3>MK!c?K$pr# zmX7qxx52}_8KDO+&k~Zr^v^^6ao}+y2P}6(=P*SMTUYA&+C)l z317T4vA2;Ev34GgEJYQ~r>(;ycpV=(Zb3;WBg8OH%6T|6z}VyC<&!nAX?Fg}QAzQ) z+fs2N6-@CGBt)0Q5_f5}R#zgN=xm?$@4;|Caf6zh`JB z!}SoDsoLpSn^*GjShAL6?K5!hXR2TkuLz<{F&AxggYP&8}!?kt|AiQ@L=t?$F9}vpjZ;V zFUPR!=XSPwpFaW4UnO?NeaHl%=^}gQ`IujJP7YiEcUpe-iz(K`8rPnBMSD>5e>;Ky z3Ln1#ek~4*N8C)EBQz^yx_Bt(o{xur4Wc+0N_vsD?!h!wCP&E-#7j8;uy~j}r9dC& zNI(ZzdF73ETU>fD_%1)Yem|}J9#naj&amc)ia($iuUv*In*F;wSk8TkcrD@z#%YJ< zzmse5?CgJY+aIbx!(YYT>)?P!U&V|Ybnh~owzl+P=T4WRN*acqi7^m_o-%H0{2x;u zen3R^M3|2ioSfYjuEHy%cMBoPMzRG(q~5;FWC(w&04;)Fj#YeSYGD}DC@lv6`t+B@ z>3gywl)&S#gE`&-saN#3V!BahcLfC}5M@%vjvOpA(sgawX}f1rnIH9auQhzzJz4I0Y|VkH(H+r%t?YY=~NC?f zGfvX}7lFSCxywBMuOiTkX9N~^zM#nRvxSDO^}#Lg4D~O^^5K}Z*Y(_3{glzs*ewb& z(6@6`U88!8zh`RMiu&4-`ZMh)BO~JxfwZr0V1Q{>ka)gM)~`atfI|$KmS>~AD=*<< zgqo@?a2=@4`wajgjVN+q7orlg@`o+U+A#8h4Yk=%Vn?fi4kKO8ZG9s(r}>;vH=M zr@NNtbgC|$IiBk^GC=FE1TrNZC(|t0V6P{N{{}4Q^5x49+`E0*YkVua>MqMkXjT}b*D2JSz z1Z&-UoXRTs0?r>W{-c&V%3|nd%V`d$qOYYg&)5mMGn-~YiSZF2lhyHH;V122?%@3O z9wWczb5B>rKN@}<&x04UN&_J5(JzZ`X~c(LiEdI85SYro-G*{?T?U_sZ|H>^A~ogy z3zQFbT@!z&eO4HE{HcKIQ8}=Ci*bjNuJJXU;^^7r=T~(2 zJYuFn;g7_`91Iz5p%hf2MlqdsS-Pg1+#V&)(|5VA;r0i)Z&B+bB+t z#@+F8&13w4C(>-Zh|?%PlEogtYRJDuAp0d8P9JLMx9m3Hm+LQN>2@~`91Mpb%}P|r zVUn>u|Dn%~j?}(+=lc!K<2qrCgN!6%+PZ*t1&;?vOiD3{ii&!HbqFOD|G9hT5hwx_ zz~RNvN?@WS?Lq$Y;T}@2e0YT|=cs8`eck2bgQbkZ2&VMGz;N!J&0CRnTBd(d0YK!H}&$rtdv zzM(MBx=sfR6{yweW8-jS;5YLn2)VgBe7FSp#8)aBG+Ga=)yW(w_yWgWIp`AHucQbR zgPd`168#wpX0YH+JZk-MFc!7m^81wo>PnO1c9WckzHNDcQF%s3Jj+oS=9q6<=Vr z>1in9={qR0I7fX7LST9I>zlJIch4sUwTpB}vkTMIq0B4W?vJ{8*ey>k)Ped@I57Nm zuCrUWI)`FuM(=05I2Gy@%?V-%=p>xqM@G34>$4+Z3 z-C=X1tl%$x6z3`EZ8J|bQwlU#{_+W=;~&271-0k@!~PF4v!DX%ByUf+(v>Otm$ib- zvhynS;774HuVW+el0IdHf8ruwPNyU0_)nsl0FHykOek!}B(Zvd4|5#zPEYsJXixO2 zH5WOZOMU-&W2B82vmhPt86W$aaZ(}l_@qLl?D(@|$B^8Ri+4ltF9A_-5?j+=;`n}F zBhAf-1=&Fvx~y6^x<#YbsveY_RBlQJrL7MfvkcgCLK2`;-=c1uGU*+1h*S20(y z?sBcD&;`s@aNx5-i40?`el61#if^utw?$&-cq#28GBN>Q@Uo~STfBS__ew$o3!aQy zvFVu7Bd`oSqBf$gY$^28h+;)i?vuO}5Jg>}@R_F(%nsus5yL9lYhOx2>S63?qv#Gi z&9Yiw)1D?sDevpm3#{st`brdOORbWhR%8+-eK5`|jbQiBYF_eYnktu8QK@w#w5N`` z9hpDxEdJJU&mZ)BLy2e`>nInL8Y{28bgDVR6~l zFfA^f?btS)`Q+d7slwG1lx3V;xZ4wnm8Ih=j?ox0l85<4x&+oJ2{qAT9^)f3Va;NSr4<7ipdtqRG^0XydSr z$^p!YNn?W-r+_FkrMIEu^Z5GpYpPa(v(O7Xa&niOiG+J5f+;8{c;M0POJS*-R^-Eo zaD2g3Bg>Zs3rXlw^h7QBsggO2goYU(hfm*&tvevvD4wx1DzcN1s@{`?o`70C_~B0} z5OKuceQMmt{7CB>na$RaW)-2rM3g(LdJec10Ev*Jqhpu+Z1BI{YE9(`*V{yp?b%0> zR3M~7@mgu8lR4Q#=_qMPjid_!RZ2W*Y7p0tE0;|e$~-1OUHw6RT>pEJPWw`K{^6u- zoHq&JsCo$v=GV7#eZB zn1KeC85W(@v(p;bqm+GRuqKcI*TNWMk5QizV1Fj`rqWmiKjtsJzb)qS_V<6s^m|{d z{|EC-6F`K3?gwUD`O+|DBJg!r^V^Mo4^!~JFNnKw%ve~aZCXGQfd2`oj@{tCgi9_Z z5ID3}|FI;KSYqd?=AQP!a($#41 z7EL^J<1E#2gVk9iXDo#nWj$&nN3}I#>sB=;)OkWufMtlgD;{CE;wr zZq*HbfY`w@R@Ks+3Fb!n`t8Vtl=4jM+?&r2R?p1_(ar{`E34J~Lqip5_=;r-VvNAR zz?5CQl-9OzG+Tm+w1+6Wkxo04StkIvceg+2!Uei_)%mN4w+hYkF6sJH@{Si{+5pJb z&GFq$Co2f@99X*u9u&Cgv8g-JSZ{eR~&7v_3-qJjV{HrKd;YV@Aj##efoLU-7=&W(<11(Pxb#= zSJ41D+RckoikQ>m=g>z?Da^W^C-ouvVH|Xw!=T5Dn4{|6E!DtQL^xc8(z9qN1BpVD z#yvHDF)uGKvr{1x_e>A5@t7pfWSYzYK5K%g{2aroVQDsX`yABDPKH4PdksFt=sfMA zWvV!+OxESa2~Q#GuyY!vU(fSC)Rb9%H5+V+LZPGudU_~Ux-jk2?XSkqVThOtV|4TE z&J@I*E;7ed(N1q%hS$A_aKMtduHG;+>)GfD@9*!IerqLh`FX-M#JPeI{B(i;CdX`z z#B5C=$twrYxtzDohBIk^xy$vh1*Vg#F6#!E{7CcV$5|s14G?d!t%bRjnT-6REV~R4 zU9oFQG1xzhBn_?+MXrf72^Ni?cJ}tNvcI7p&hS0+><%6*<;kronR)9|eXv85JtK33 zIm5RsEut^6W98|Nohm)3_@u9=_kn$%@o2%fEAIqXhr-H(N$NEl?dx^i@WLQkK36G8 z7;1#RsIPSqcG#kDF-DqyXQKD!0p#e-@;b*ARV_qf$%EH95cE&cqX!S{SWiZNNm$^~ z0pB?U?>QFfw+nIYdSN|PV?CEC+>8HXvOvR{M;LbIJZV(hm&+ae4m)>`|Bv>p{jyH~ z(t`Dk3)44)+93HY`Fk8JT05pA>WGRkc!18zArm2*#sfl-T9xC^~cdl0PvXyBrmd zAE;R|0aWVN$N85xxCO#KW_limKiNwLAg;A$%L0k}lwUvD{CdMZ(5k1zJMxQ%Yc`K) z^lvCMovi?0FmVph94Qvi)B2ujRdRa>6ppT7aEKaz z*vHEWQlPWEqh&@GV=++TA9lTAZDeR-;>J-Bru`@Amru1yR1*0Gg*&Zi>Ik}sxrao~`?z5l8;&14jR>!M*g)$R1&MPv+7jPwr?e~ZbSNBQJ?rEV z79=SZo=#qZkiu*!|9l-Uj@8HkS!lf1xEcrNPt6i(r=Oq(B7k_tw5Xuq7j1-!ci4$& znb=-;Yq?T8BN}%ODC`XROOJvLE@}G%LgrRiq{_AKg6kKi15OfzBJwXK#0JRR0d1yL zgo9h$_D>1^)VDD_oM3dI;fNsy|A6L&q@0ih=l}EBiI`!=CC1WGse#3WF>XUii)rgt zRLi@y-DKs@3KD1e?iNPOdGfEln~>&5UBEvG!H#{gV0iq853}-@Ut0woAK@fXwqVMo zs^I*s^_tIos<&O_XVFy43OY3IMsHU2fb#^QDs1_p(b&3UVx$gw@7fDsznhUzp6R(9 z6^f4rE}2X-d5+#alb|LoGubQIfO!hTR905Dyz|H!I?<>Qe5n_*+1c6I41^b;_A%=M zb~`c@-wvwZ%jdYXI2wXBg7rM94zju*g%v-ykx}@CMUI1sJdB988ORuQZAA7!g z_Y-8hM)q4JL->^Zk%#rvcNN6OSt#;kdgie@H>!ac|C4UR{!1C6zJ?ucq`HEgXJ=;@ zp09kCxW?AKE3k0-*WO(#ddlzIyDc!^OB1kFbI}!wx+-ew-! zO4HC}n-5x>4QG>0NV>GyxLIQ^hkS2N%>|;t!9-qq8Pw$jY#qa_0%#UaU9mxI7?-tM z04ZoVHR$%#p@h!uWr<#=bOtz5JsjlX0=o&)?7`E$7RQPKCl%p_q)|DdkOK{0qDUJm zwfTUBx^nOfRYs$(0NUEbR|3MTfExINIZ!-TA$lpTJrt`ilhpOG<*s5~uWn1(RkTYq z`G;k#|2?k|DyR!xFI@R55)C6yi^ijktX>p$H=PyWu_Z=+?7rP|7beL;C>MI%YKaqO zus6AS{lTN<0c>AaytOpYTA|PbHe-y%V_NDeI~Zu6Z+H9W92w5=qNfasR^79nft!~& zAV?%KT7FhZ=1R7)WKv_Lc8O@%bx?h^7g5ix0Xba6Iux3VUKay;F#2*;{^Il9l60@q z?tLRRq*=$Hap_NyR}>Wbk}D}W!w5wh>h@J|to(-55^}}o{={h4IS-V!z8ivJ5Hhy~ zRCqYMghAg~epIc6*gL5rEdawfDVFbzVoVC>b4VRTBNBaCtS8~Pwldw0&?JyPC=4#A)VG< z#ZR6y@VBgx-uHGz-6yi_AE|4!MW7<4Q~l2sEg{RkUeG7^Ygq;laM-Zs-)S{OnyIlY zMa(_cz8Iim06;W2A9H)2b_{XF2B?=5j)rjww)3bAzO@q7eV34XsAhpFmxUfEQNu7j z4&SRY+t8naL;BQEMSJnBK$IWEcu!F~-6|b&RwiqjLLhq7v->7THeRHf%x9f{T{N@j zH8PMvow^9_>)O=O#mqRpfq(BzBJzd=UhQSeU~`kZm8Lr_3i{^4>C4|z2s9lJZEESj zlM~_ILQ~E~LtjxaRr_|1*?m~8ZTs28C(#@$hmMM9msgCFxeITMX%-9+yNC_X$J27E zv}NLNa3bueslQgePeA(dWek7ZM$e;>flAcOs~3wRPJ;Ocd^bXkYNV|fYom=Y^Dz*| z)cjSdZ%GLW9_IWVsoJ~4qm1aT`ya}r39Gf;zKg2SUeh}Kn0I%}`<-%xaPt9n{g3fR zU;QN9q@+vaXAR(AU*?*T8%C3*)xHIUXkZJ8KgH8FD2lX!o}Pl}%}=c79{+qvJYTPW z-z^c0xapJJ=J>~Ss?pswn~-b1T;kMJZAe2BVj<+Z{;Ac#)L%P-Rh~B_-$&J*|A=0? zSafjN=uw47nQV3JO>+N~Ohw?@%2#PKGS;E2`G8=UL>%Byn+wZe(M+G9fj-Q7kar3e zFN`Z`>?~DQS#9`d@Mw;g=KU<9^u4#%quRFfrL7T*7HD~(+0Nz@7xvzzjtFWQfL082 zNjZO|6v%ZN0vw3|l8?!I;L`YG7^O&o!v-mTcb|4hoNb8)3Qq3IAa*Y+l_}nkyELgV zKF0aQdV77KwUf7*_ArQR--YPNszdAaVsp^QyqPhmYdv~e4t9G?Yi#7A^_yJCn*&<6 zMfV?!IvUZ?=#ZT*+Tcwk>v;)Q=csMaFYQWFU_qG_z^ANlv?DhIHCZHl3(wgjw zEUZ(7E}fIP0~-eFYZkfg9rM868=(~Zm$D$iclZMTTEnS2*3^{Z@%<7W2eB=gEVSj4X&_-pIReW1997X7s*Q-T7POFvgOnrGhu*|GKT@Y!K%cE{IuBU^yZ-zU!ca zBD+9hU7J(kD9tpHo8LXxr>hw(K*+V;HjB1ZlVQxuqt9KZ0zTUegJAYAl2X~3yT~kj zO@F@~yv3UQ9p}DO2=?9J^w!ZqduCIi&Q(Bn^2!}s;c(w+4Tf*{wF5mZ>vnPp*hc|m zHOG80@>l?kO_YNdt8@eC=~xVwsqz$rZc6&Q_0Dr_`;i<}(ROxe+3;mE8uurrNV68f z<^GRb2a0Q(9IzpVvhSc7*PqR%*g)ln&3Wp*o+c>9E z4(laA7G(rdL6_*6!~NP(%EzHwC3y465MAB7BByB%$EsFmq*>2HZJkQFOO)!A1CQx& ztvKrVQ5vjQC@QC`+<>}}jCCcU4CVWu{kqn!Z39ffd-qGe`TcWRtD(CEWd5x9!-Sg` z1T_~!q0Z(>P^2y1iG>wC^aB;ils-WZD!$pSZz?E39Ux19% z@P3DVk7`Z5!jk#yim$ev5$yFiCL3d%Z!{g6T^Xuqd2PEi*6#Glq6lt8+0~mhL_{oh zYcV~a(*E8nEAHy!2p0_dD3`XFZ<{+dk;Zyz4+6T}46^z|{#4FoAH>4rpH>{JHXQdQ z^)(5(?=pS!A&^rjJ;b$}=2$MG{exyu2_DD^flj11e9&R{OuzTbRzmLcZvG;30kEqU zQnQ1aLY)SBKPU_U5rDiYjPoWX!ugtVsH!a0gmFxbHDoyA<~#QyH_d(+xQiLQN+FT7 z_1SNgALhB&r$6x#eS)Xz{dE2GxfR;qA9zag_MTviH($q>3*25;gQ{G7gF6bvE;GQr zYbm`x4WBh#FK)|MU;jA_amw{Jxqnfa)dXn(dXjg7kcdkdgk7}vu08@;xLUl#(Ayx3 zz?8a0d%VaYK1)YE^Ym&OiKED5U+ya;ds?#>;|n@cK4Kj}wHUZ7Efg`=20_or@h{*6 zGL4xu&JKPPQTf5Z3y}X|NpbHE)K}!@0WKy`^y?K)4eacO>vC~S7bn;sc=PDYND->u zuFxXGqFU7w(N6-%ajV*-`V1fSZjUs&pQ!P6dB(-?3T8EdE(jiNY0kG{|1H{Rnp`5v~_*rW_|@~ zOkyg{7wp7qA_R!4Gw`ny1MF^%ATwu|{{|Z8@>ccsvs5|*%=f8L-uY2og#UA#moRRz zUlB(KRe86}`(Eyozbf#PB5Fw>0S+J2ESu!i|JZLGuCC_nQy3UG<_y%6&7X-BQhlsw zRUy;&Z$9w`h%?^cBMse+S`N=M7n9~np=&AEX>690YkHA|sblc0%CsmHSk^lE?{43el%tY4E8PJxunn+U zZcWM1F5;LsU(>@U#U;z!QcX=k!yL|QPmzR~zvW~4Qf1T;Od9DJ(3(O?`swql^C}EB z@{k26*rgQ45vi{!tCTUFAy2THWFp4ts+6{imd~1|--zu}$)mE{ViwxJRXKq# zOL(^2j4t8o3o91B3T&jUOHP#Q7B*fRi@h`od3;H8DdeN&(z6s3#3D975ex345bEB& z!T$$A6VKHxVa~Pv>y0t@PK;;WGwh+cXr>FIOoOYhucdX^nIeD#{x*1)qK47JUW9m# z>6s=jYDPzb9rLce)2L5!A+blN1_sY2>G)KSE@z&Of2NznhSdSxtkS_0_x!>RU_1vU zXgfYLZ-o?V#0EPQW>Ni%fsr&0ZtF-}_eaR2V8vs-Xz763d0TABsd)&r>v3fu(=f`EGt*=Blz@ST}!j8^BQZnl>iec1{Oc0W8 zSAZhlErxLn(qhW-fRz}({3F*N@FC8#PIGiL>t=ci@j6$ZkHsZ5a38||$FC0)M^yK| zNH5B)ps$0W1iiFwd)eLdLMz&OpurnN>Z+tXOi~IaC390!Mp0T)QA$?y>Q%+7SHFde iz4^aRaB;J9wD Date: Sun, 24 Feb 2019 15:04:16 +0100 Subject: [PATCH 03/66] Fixed FOSSA status --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 29f2adee0..bc0f3ff39 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # NITRO Backend [![Build Status](https://img.shields.io/travis/com/Human-Connection/Nitro-Backend/master.svg)](https://travis-ci.com/Human-Connection/Nitro-Backend) [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitor-Backend/blob/develop/LICENSE.md) -[![FOSSA Status](https://app.fossa.io/api/projects/git+github.com/Human-Connection/Nitro-Backend.svg?type=shield)](https://app.fossa.io/projects/git+github.com/Human-Connection/Nitro-Backend?ref=badge_shield) +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend?ref=badge_shield) [![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discord.gg/6ub73U3) > This Prototype tries to resolve the biggest hurdle of connecting @@ -155,9 +155,13 @@ npm run test - [x] add jwt authentication - [ ] get directives working correctly (@toLower, @auth, @role, etc.) -- [ ] check if search is working +- [x] check if search is working - [x] check if sorting is working - [x] check if pagination is working - [ ] check if upload is working (using graphql-yoga?) - [x] evaluate middleware - [ ] ignore Posts and Comments by blacklisted Users + + +## License +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend?ref=badge_large) From 56610d0d3d449e3a037bc5a2c9baf8cee5b737dc Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Sun, 24 Feb 2019 15:07:41 +0100 Subject: [PATCH 04/66] Fixed FOSSA status --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bc0f3ff39..5350f212f 100644 --- a/README.md +++ b/README.md @@ -164,4 +164,4 @@ npm run test ## License -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend?ref=badge_large) +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend.svg?type=small)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend?ref=badge_small) From 5a255db0181b5f497bd488b7398391a29c0b0e5d Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Sun, 24 Feb 2019 15:08:00 +0100 Subject: [PATCH 05/66] Fixed FOSSA status --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5350f212f..bc0f3ff39 100644 --- a/README.md +++ b/README.md @@ -164,4 +164,4 @@ npm run test ## License -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend.svg?type=small)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend?ref=badge_small) +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend?ref=badge_large) From bc6089dbf5e1c8cb7a663969ee2597e7e003f1d1 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Sun, 24 Feb 2019 17:05:59 +0100 Subject: [PATCH 06/66] Fixed readme --- LICENSE.md | 21 +++++++++++++++++++++ README.md | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..9d4508b38 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Human-Connection gGmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index bc0f3ff39..b3b4764b7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # NITRO Backend [![Build Status](https://img.shields.io/travis/com/Human-Connection/Nitro-Backend/master.svg)](https://travis-ci.com/Human-Connection/Nitro-Backend) -[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitor-Backend/blob/develop/LICENSE.md) +[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitro-Backend/blob/backend/LICENSE.md) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend?ref=badge_shield) [![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discord.gg/6ub73U3) From c4f15e626d433ca2f740d2bf5497909407b604a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 26 Feb 2019 00:46:14 +0100 Subject: [PATCH 07/66] Fix lint and unblock port 4001 We have to specify the `yarn run test:cypress` in `.travis.yml` in the Nitro-Web repo. --- docker-compose.travis.yml | 1 - src/seed/factories/index.js | 28 ++++++++++++++-------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml index 3d577e638..27f53dfef 100644 --- a/docker-compose.travis.yml +++ b/docker-compose.travis.yml @@ -15,4 +15,3 @@ services: build: context: . target: builder - command: yarn run test:cypress diff --git a/src/seed/factories/index.js b/src/seed/factories/index.js index 752ae3369..d9bbd700c 100644 --- a/src/seed/factories/index.js +++ b/src/seed/factories/index.js @@ -1,14 +1,14 @@ import { GraphQLClient, request } from 'graphql-request' import { getDriver } from '../../bootstrap/neo4j' -import createBadge from './badges.js' -import createUser from './users.js' +import createBadge from './badges.js' +import createUser from './users.js' import createOrganization from './organizations.js' -import createPost from './posts.js' -import createComment from './comments.js' -import createCategory from './categories.js' -import createTag from './tags.js' -import createReport from './reports.js' +import createPost from './posts.js' +import createComment from './comments.js' +import createCategory from './categories.js' +import createTag from './tags.js' +import createReport from './reports.js' export const seedServerHost = 'http://127.0.0.1:4001' @@ -25,14 +25,14 @@ const authenticatedHeaders = async ({ email, password }, host) => { } } const factories = { - 'Badge': createBadge, - 'User': createUser, + 'Badge': createBadge, + 'User': createUser, 'Organization': createOrganization, - 'Post': createPost, - 'Comment': createComment, - 'Category': createCategory, - 'Tag': createTag, - 'Report': createReport + 'Post': createPost, + 'Comment': createComment, + 'Category': createCategory, + 'Tag': createTag, + 'Report': createReport } export const cleanDatabase = async (options = {}) => { From 706fccc7332e07babac0a077dc25d28eb107306f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 26 Feb 2019 12:16:27 +0100 Subject: [PATCH 08/66] Fix Cypress with a dedicated docker-compose.yml --- docker-compose.cypress.yml | 18 ++++++++++++++++++ docker-compose.travis.yml | 3 --- 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 docker-compose.cypress.yml diff --git a/docker-compose.cypress.yml b/docker-compose.cypress.yml new file mode 100644 index 000000000..3d577e638 --- /dev/null +++ b/docker-compose.cypress.yml @@ -0,0 +1,18 @@ +version: "3.7" + +services: + neo4j: + environment: + - NEO4J_AUTH=none + ports: + - 7687:7687 + - 7474:7474 + backend: + ports: + - 4001:4001 + - 4123:4123 + image: humanconnection/nitro-backend:builder + build: + context: . + target: builder + command: yarn run test:cypress diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml index 27f53dfef..e1998f6dd 100644 --- a/docker-compose.travis.yml +++ b/docker-compose.travis.yml @@ -8,9 +8,6 @@ services: - 7687:7687 - 7474:7474 backend: - ports: - - 4001:4001 - - 4123:4123 image: humanconnection/nitro-backend:builder build: context: . From d0df4e8df0d2620b9b375c9a77340c45c59ec184 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Tue, 26 Feb 2019 15:16:38 +0000 Subject: [PATCH 09/66] Bump @babel/core from 7.3.3 to 7.3.4 Bumps [@babel/core](https://github.com/babel/babel) from 7.3.3 to 7.3.4. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/master/CHANGELOG.md) - [Commits](https://github.com/babel/babel/compare/v7.3.3...v7.3.4) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 58 ++++++++++++++++++++++++++-------------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 39c9c14c6..65db47e1f 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ }, "devDependencies": { "@babel/cli": "~7.2.3", - "@babel/core": "~7.3.3", + "@babel/core": "~7.3.4", "@babel/node": "~7.2.2", "@babel/preset-env": "~7.3.1", "@babel/register": "~7.0.0", diff --git a/yarn.lock b/yarn.lock index 2fe610fee..34884e22b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -38,18 +38,18 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@^7.1.0", "@babel/core@~7.3.3": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.3.tgz#d090d157b7c5060d05a05acaebc048bd2b037947" - integrity sha512-w445QGI2qd0E0GlSnq6huRZWPMmQGCp5gd5ZWS4hagn0EiwzxD5QMFkpchyusAyVC1n27OKXzQ0/88aVU9n4xQ== +"@babel/core@^7.1.0", "@babel/core@~7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.4.tgz#921a5a13746c21e32445bf0798680e9d11a6530b" + integrity sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.3.3" + "@babel/generator" "^7.3.4" "@babel/helpers" "^7.2.0" - "@babel/parser" "^7.3.3" + "@babel/parser" "^7.3.4" "@babel/template" "^7.2.2" - "@babel/traverse" "^7.2.2" - "@babel/types" "^7.3.3" + "@babel/traverse" "^7.3.4" + "@babel/types" "^7.3.4" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" @@ -58,12 +58,12 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.2.2", "@babel/generator@^7.3.3": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.3.tgz#185962ade59a52e00ca2bdfcfd1d58e528d4e39e" - integrity sha512-aEADYwRRZjJyMnKN7llGIlircxTCofm3dtV5pmY6ob18MSIuipHpA2yZWkPlycwu5HJcx/pADS3zssd8eY7/6A== +"@babel/generator@^7.0.0", "@babel/generator@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e" + integrity sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg== dependencies: - "@babel/types" "^7.3.3" + "@babel/types" "^7.3.4" jsesc "^2.5.1" lodash "^4.17.11" source-map "^0.5.0" @@ -253,10 +253,10 @@ lodash "^4.17.10" v8flags "^3.1.1" -"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.2.3", "@babel/parser@^7.3.3": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.3.tgz#092d450db02bdb6ccb1ca8ffd47d8774a91aef87" - integrity sha512-xsH1CJoln2r74hR+y7cg2B5JCPaTh+Hd+EbBRk9nWGSNspuo6krjhX0Om6RnRQuIvFq8wVXCLKH3kwKDYhanSg== +"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c" + integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ== "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" @@ -636,25 +636,25 @@ "@babel/parser" "^7.2.2" "@babel/types" "^7.2.2" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2": - 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.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06" + integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.2.2" + "@babel/generator" "^7.3.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/parser" "^7.3.4" + "@babel/types" "^7.3.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.3": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.3.tgz#6c44d1cdac2a7625b624216657d5bc6c107ab436" - integrity sha512-2tACZ80Wg09UnPg5uGAOUvvInaqLk3l/IAhQzlxLQOIXacr6bMsra5SH6AWw/hIDRCSbCdHP2KzSOD+cT7TzMQ== +"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed" + integrity sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ== dependencies: esutils "^2.0.2" lodash "^4.17.11" From 69bf53e05e2529d7cef21bd816da1c39fc73ce59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 26 Feb 2019 16:28:22 +0100 Subject: [PATCH 10/66] Impplement currentUser query * remove dead code (passport-jwt) * refactor resolves to have a separate folder * currentUser and login have the same response --- package.json | 2 - src/graphql-schema.js | 178 ++--------------------- src/graphql-schema.spec.js | 184 ------------------------ src/jwt/decode.js | 29 ++++ src/jwt/{generateToken.js => encode.js} | 2 +- src/jwt/strategy.js | 42 ------ src/middleware/permissionsMiddleware.js | 3 +- src/resolvers/posts.js | 22 +++ src/resolvers/posts.spec.js | 61 ++++++++ src/resolvers/reports.js | 51 +++++++ src/resolvers/reports.spec.js | 68 +++++++++ src/resolvers/statistics.js | 67 +++++++++ src/resolvers/user_management.js | 52 +++++++ src/resolvers/user_management.spec.js | 183 +++++++++++++++++++++++ src/schema.graphql | 4 +- src/server.js | 27 +--- yarn.lock | 28 +--- 17 files changed, 555 insertions(+), 448 deletions(-) delete mode 100644 src/graphql-schema.spec.js create mode 100644 src/jwt/decode.js rename src/jwt/{generateToken.js => encode.js} (90%) delete mode 100644 src/jwt/strategy.js create mode 100644 src/resolvers/posts.js create mode 100644 src/resolvers/posts.spec.js create mode 100644 src/resolvers/reports.js create mode 100644 src/resolvers/reports.spec.js create mode 100644 src/resolvers/statistics.js create mode 100644 src/resolvers/user_management.js create mode 100644 src/resolvers/user_management.spec.js diff --git a/package.json b/package.json index 39c9c14c6..e313b45d5 100644 --- a/package.json +++ b/package.json @@ -59,8 +59,6 @@ "neo4j-graphql-js": "~2.3.1", "node-fetch": "~2.3.0", "npm-run-all": "~4.1.5", - "passport": "~0.4.0", - "passport-jwt": "~4.0.0", "sanitize-html": "~1.20.0", "slug": "~1.0.0", "trunc-html": "~1.1.2", diff --git a/src/graphql-schema.js b/src/graphql-schema.js index ce84dde36..6d10183c9 100644 --- a/src/graphql-schema.js +++ b/src/graphql-schema.js @@ -1,182 +1,22 @@ import fs from 'fs' import path from 'path' -import bcrypt from 'bcryptjs' -import generateJwt from './jwt/generateToken' -import uuid from 'uuid/v4' -import { fixUrl } from './middleware/fixImageUrlsMiddleware' -import { AuthenticationError } from 'apollo-server' -import { neo4jgraphql } from 'neo4j-graphql-js' +import userManagement from './resolvers/user_management.js' +import statistics from './resolvers/statistics.js' +import reports from './resolvers/reports.js' +import posts from './resolvers/posts.js' export const typeDefs = fs.readFileSync(process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql')) .toString('utf-8') -export const query = (cypher, session) => { - return new Promise((resolve, reject) => { - let data = [] - session - .run(cypher) - .subscribe({ - onNext: function (record) { - let item = {} - record.keys.forEach(key => { - item[key] = record.get(key) - }) - data.push(item) - }, - onCompleted: function () { - session.close() - resolve(data) - }, - onError: function (error) { - reject(error) - } - }) - }) -} -const queryOne = (cypher, session) => { - return new Promise((resolve, reject) => { - query(cypher, session) - .then(res => { - resolve(res.length ? res.pop() : {}) - }) - .catch(err => { - reject(err) - }) - }) -} - export const resolvers = { Query: { - isLoggedIn: (parent, args, { driver, user }) => { - return Boolean(user && user.id) - }, - statistics: async (parent, args, { driver, user }) => { - return new Promise(async (resolve) => { - const session = driver.session() - const queries = { - countUsers: 'MATCH (r:User) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countUsers', - countPosts: 'MATCH (r:Post) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countPosts', - countComments: 'MATCH (r:Comment) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countComments', - countNotifications: 'MATCH (r:Notification) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countNotifications', - countOrganizations: 'MATCH (r:Organization) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countOrganizations', - countProjects: 'MATCH (r:Project) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countProjects', - countInvites: 'MATCH (r:Invite) WHERE r.wasUsed <> true OR NOT exists(r.wasUsed) RETURN COUNT(r) AS countInvites', - countFollows: 'MATCH (:User)-[r:FOLLOWS]->(:User) RETURN COUNT(r) AS countFollows', - countShouts: 'MATCH (:User)-[r:SHOUTED]->(:Post) RETURN COUNT(r) AS countShouts' - } - let data = { - countUsers: (await queryOne(queries.countUsers, session)).countUsers.low, - countPosts: (await queryOne(queries.countPosts, session)).countPosts.low, - countComments: (await queryOne(queries.countComments, session)).countComments.low, - countNotifications: (await queryOne(queries.countNotifications, session)).countNotifications.low, - countOrganizations: (await queryOne(queries.countOrganizations, session)).countOrganizations.low, - countProjects: (await queryOne(queries.countProjects, session)).countProjects.low, - countInvites: (await queryOne(queries.countInvites, session)).countInvites.low, - countFollows: (await queryOne(queries.countFollows, session)).countFollows.low, - countShouts: (await queryOne(queries.countShouts, session)).countShouts.low - } - resolve(data) - }) - } - // usersBySubstring: neo4jgraphql + ...statistics.Query, + ...userManagement.Query }, Mutation: { - signup: async (parent, { email, password }, { req }) => { - // if (data[email]) { - // throw new Error('Another User with same email exists.') - // } - // data[email] = { - // password: await bcrypt.hashSync(password, 10), - // } - - return true - }, - login: async (parent, { email, password }, { driver, req, user }) => { - // if (user && user.id) { - // throw new Error('Already logged in.') - // } - const session = driver.session() - return session.run( - 'MATCH (user:User {email: $userEmail}) ' + - 'RETURN user {.id, .slug, .name, .avatar, .locationName, .about, .email, .password, .role} as user LIMIT 1', { - userEmail: email - }) - .then(async (result) => { - session.close() - const [currentUser] = await result.records.map(function (record) { - return record.get('user') - }) - - if (currentUser && await bcrypt.compareSync(password, currentUser.password)) { - delete currentUser.password - currentUser.avatar = fixUrl(currentUser.avatar) - return Object.assign(currentUser, { - token: generateJwt(currentUser) - }) - } else throw new AuthenticationError('Incorrect email address or password.') - }) - }, - report: async (parent, { resource, description }, { driver, req, user }, resolveInfo) => { - const contextId = uuid() - const session = driver.session() - const data = { - id: contextId, - type: resource.type, - createdAt: (new Date()).toISOString(), - description: resource.description - } - await session.run( - 'CREATE (r:Report $report) ' + - 'RETURN r.id, r.type, r.description', { - report: data - } - ) - let contentType - - switch (resource.type) { - case 'post': - case 'contribution': - contentType = 'Post' - break - case 'comment': - contentType = 'Comment' - break - case 'user': - contentType = 'User' - break - } - - await session.run( - `MATCH (author:User {id: $userId}), (context:${contentType} {id: $resourceId}), (report:Report {id: $contextId}) ` + - 'MERGE (report)<-[:REPORTED]-(author) ' + - 'MERGE (context)<-[:REPORTED]-(report) ' + - 'RETURN context', { - resourceId: resource.id, - userId: user.id, - contextId: contextId - } - ) - session.close() - - // TODO: output Report compatible object - return data - }, - CreatePost: async (object, params, ctx, resolveInfo) => { - const result = await neo4jgraphql(object, params, ctx, resolveInfo, false) - - const session = ctx.driver.session() - await session.run( - 'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' + - 'MERGE (post)<-[:WROTE]-(author) ' + - 'RETURN author', { - userId: ctx.user.id, - postId: result.id - }) - session.close() - - return result - } - + ...userManagement.Mutation, + ...reports.Mutation, + ...posts.Mutation } } diff --git a/src/graphql-schema.spec.js b/src/graphql-schema.spec.js deleted file mode 100644 index 7aa47835f..000000000 --- a/src/graphql-schema.spec.js +++ /dev/null @@ -1,184 +0,0 @@ -import Factory from './seed/factories' -import { GraphQLClient, request } from 'graphql-request' -import jwt from 'jsonwebtoken' -import { host, login } from './jest/helpers' - -const factory = Factory() - -beforeEach(async () => { - await factory.create('User', { - email: 'test@example.org', - password: '1234' - }) -}) - -afterEach(async () => { - await factory.cleanDatabase() -}) - -describe('isLoggedIn', () => { - describe('unauthenticated', () => { - it('returns false', async () => { - const query = '{ isLoggedIn }' - await expect(request(host, query)).resolves.toEqual({ isLoggedIn: false }) - }) - }) -}) - -describe('login', () => { - const mutation = (params) => { - const { email, password } = params - return ` - mutation { - login(email:"${email}", password:"${password}"){ - token - } - }` - } - - describe('ask for a `token`', () => { - describe('with valid email/password combination', () => { - it('responds with a JWT token', async () => { - const data = await request(host, mutation({ - email: 'test@example.org', - password: '1234' - })) - const { token } = data.login - jwt.verify(token, process.env.JWT_SECRET, (err, data) => { - expect(data.email).toEqual('test@example.org') - expect(err).toBeNull() - }) - }) - }) - - describe('with a valid email but incorrect password', () => { - it('responds with "Incorrect email address or password."', async () => { - await expect( - request(host, mutation({ - email: 'test@example.org', - password: 'wrong' - })) - ).rejects.toThrow('Incorrect email address or password.') - }) - }) - - describe('with a non-existing email', () => { - it('responds with "Incorrect email address or password."', async () => { - await expect( - request(host, mutation({ - email: 'non-existent@example.org', - password: 'wrong' - })) - ).rejects.toThrow('Incorrect email address or password.') - }) - }) - }) -}) - -describe('CreatePost', () => { - describe('unauthenticated', () => { - let client - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(`mutation { - CreatePost( - title: "I am a post", - content: "Some content" - ) { slug } - }`)).rejects.toThrow('Not Authorised') - }) - - describe('authenticated', () => { - let headers - let response - beforeEach(async () => { - headers = await login({ email: 'test@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - response = await client.request(`mutation { - CreatePost( - title: "A title", - content: "Some content" - ) { title, content } - }`, { headers }) - }) - - it('creates a post', () => { - expect(response).toEqual({ CreatePost: { title: 'A title', content: 'Some content' } }) - }) - - it('assigns the authenticated user as author', async () => { - const { User } = await client.request(`{ - User(email:"test@example.org") { - contributions { - title - } - } - }`, { headers }) - expect(User).toEqual([ { contributions: [ { title: 'A title' } ] } ]) - }) - }) - }) -}) - -describe('report', () => { - beforeEach(async () => { - await factory.create('User', { - email: 'test@example.org', - password: '1234' - }) - await factory.create('User', { - id: 'u2', - name: 'abusive-user', - role: 'user', - email: 'abusive-user@example.org' - }) - }) - - afterEach(async () => { - await factory.cleanDatabase() - }) - - describe('unauthenticated', () => { - let client - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect( - client.request(`mutation { - report( - description: "I don't like this user", - resource: { - id: "u2", - type: user - } - ) { id, createdAt } - }`) - ).rejects.toThrow('Not Authorised') - }) - - describe('authenticated', () => { - let headers - let response - beforeEach(async () => { - headers = await login({ email: 'test@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - response = await client.request(`mutation { - report( - description: "I don't like this user", - resource: { - id: "u2", - type: user - } - ) { id, createdAt } - }`, - { headers } - ) - }) - it('creates a report', () => { - let { id, createdAt } = response.report - expect(response).toEqual({ - report: { id, createdAt } - }) - }) - }) - }) -}) diff --git a/src/jwt/decode.js b/src/jwt/decode.js new file mode 100644 index 000000000..0ab1e4529 --- /dev/null +++ b/src/jwt/decode.js @@ -0,0 +1,29 @@ +import jwt from 'jsonwebtoken' + +export default async (driver, authorizationHeader) => { + if (!authorizationHeader) return null + const token = authorizationHeader.replace('Bearer ', '') + let id = null + try { + const decoded = await jwt.verify(token, process.env.JWT_SECRET) + id = decoded.sub + } catch { + return null + } + const session = driver.session() + const query = ` + MATCH (user:User {id: {id} }) + RETURN user {.id, .slug, .name, .avatar, .email, .role} as user + LIMIT 1 + ` + const result = await session.run(query, { id }) + session.close() + const [currentUser] = await result.records.map((record) => { + return record.get('user') + }) + if (!currentUser) return null + return { + token, + ...currentUser + } +} diff --git a/src/jwt/generateToken.js b/src/jwt/encode.js similarity index 90% rename from src/jwt/generateToken.js rename to src/jwt/encode.js index 7cbc70330..62e85616b 100644 --- a/src/jwt/generateToken.js +++ b/src/jwt/encode.js @@ -3,7 +3,7 @@ import jwt from 'jsonwebtoken' import ms from 'ms' // Generate an Access Token for the given User ID -export default function generateJwt (user) { +export default function encode (user) { const token = jwt.sign(user, process.env.JWT_SECRET, { expiresIn: ms('1d'), issuer: process.env.GRAPHQL_URI, diff --git a/src/jwt/strategy.js b/src/jwt/strategy.js deleted file mode 100644 index 5b1ea1231..000000000 --- a/src/jwt/strategy.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Strategy } from 'passport-jwt' -import { fixUrl } from '../middleware/fixImageUrlsMiddleware' - -const cookieExtractor = (req) => { - var token = null - if (req && req.cookies) { - token = req.cookies['jwt'] - } - return token -} - -export default (driver) => { - const options = { - jwtFromRequest: cookieExtractor, - secretOrKey: process.env.JWT_SECRET, - issuer: process.env.GRAPHQL_URI, - audience: process.env.CLIENT_URI - } - - return new Strategy(options, - async (JWTPayload, next) => { - const session = driver.session() - const result = await session.run( - 'MATCH (user:User {id: $userId}) ' + - 'RETURN user {.id, .slug, .name, .avatar, .email, .role} as user LIMIT 1', - { - userId: JWTPayload.id - } - ) - session.close() - const [currentUser] = await result.records.map((record) => { - return record.get('user') - }) - - if (currentUser) { - currentUser.avatar = fixUrl(currentUser.avatar) - return next(null, currentUser) - } else { - return next(null, false) - } - }) -} diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 1a3f04ceb..0c6723b4b 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -23,7 +23,8 @@ const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, ctx, info) => { // Permissions const permissions = shield({ Query: { - statistics: allow + statistics: allow, + currentUser: allow // fruits: and(isAuthenticated, or(isAdmin, isModerator)), // customers: and(isAuthenticated, isAdmin) }, diff --git a/src/resolvers/posts.js b/src/resolvers/posts.js new file mode 100644 index 000000000..6a8a0c25f --- /dev/null +++ b/src/resolvers/posts.js @@ -0,0 +1,22 @@ +import { neo4jgraphql } from 'neo4j-graphql-js' + +export default { + Mutation: { + CreatePost: async (object, params, ctx, resolveInfo) => { + const result = await neo4jgraphql(object, params, ctx, resolveInfo, false) + + const session = ctx.driver.session() + await session.run( + 'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' + + 'MERGE (post)<-[:WROTE]-(author) ' + + 'RETURN author', { + userId: ctx.user.id, + postId: result.id + }) + session.close() + + return result + } + + } +} diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js new file mode 100644 index 000000000..a6c1d7e3e --- /dev/null +++ b/src/resolvers/posts.spec.js @@ -0,0 +1,61 @@ +import Factory from '../seed/factories' +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../jest/helpers' + +const factory = Factory() + +beforeEach(async () => { + await factory.create('User', { + email: 'test@example.org', + password: '1234' + }) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('CreatePost', () => { + describe('unauthenticated', () => { + let client + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(`mutation { + CreatePost( + title: "I am a post", + content: "Some content" + ) { slug } + }`)).rejects.toThrow('Not Authorised') + }) + + describe('authenticated', () => { + let headers + let response + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + response = await client.request(`mutation { + CreatePost( + title: "A title", + content: "Some content" + ) { title, content } + }`, { headers }) + }) + + it('creates a post', () => { + expect(response).toEqual({ CreatePost: { title: 'A title', content: 'Some content' } }) + }) + + it('assigns the authenticated user as author', async () => { + const { User } = await client.request(`{ + User(email:"test@example.org") { + contributions { + title + } + } + }`, { headers }) + expect(User).toEqual([ { contributions: [ { title: 'A title' } ] } ]) + }) + }) + }) +}) diff --git a/src/resolvers/reports.js b/src/resolvers/reports.js new file mode 100644 index 000000000..c471d7b7a --- /dev/null +++ b/src/resolvers/reports.js @@ -0,0 +1,51 @@ +import uuid from 'uuid/v4' + +export default { + Mutation: { + report: async (parent, { resource, description }, { driver, req, user }, resolveInfo) => { + const contextId = uuid() + const session = driver.session() + const data = { + id: contextId, + type: resource.type, + createdAt: (new Date()).toISOString(), + description: resource.description + } + await session.run( + 'CREATE (r:Report $report) ' + + 'RETURN r.id, r.type, r.description', { + report: data + } + ) + let contentType + + switch (resource.type) { + case 'post': + case 'contribution': + contentType = 'Post' + break + case 'comment': + contentType = 'Comment' + break + case 'user': + contentType = 'User' + break + } + + await session.run( + `MATCH (author:User {id: $userId}), (context:${contentType} {id: $resourceId}), (report:Report {id: $contextId}) ` + + 'MERGE (report)<-[:REPORTED]-(author) ' + + 'MERGE (context)<-[:REPORTED]-(report) ' + + 'RETURN context', { + resourceId: resource.id, + userId: user.id, + contextId: contextId + } + ) + session.close() + + // TODO: output Report compatible object + return data + } + } +} diff --git a/src/resolvers/reports.spec.js b/src/resolvers/reports.spec.js new file mode 100644 index 000000000..253cdadcc --- /dev/null +++ b/src/resolvers/reports.spec.js @@ -0,0 +1,68 @@ +import Factory from '../seed/factories' +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../jest/helpers' + +const factory = Factory() + +describe('report', () => { + beforeEach(async () => { + await factory.create('User', { + email: 'test@example.org', + password: '1234' + }) + await factory.create('User', { + id: 'u2', + name: 'abusive-user', + role: 'user', + email: 'abusive-user@example.org' + }) + }) + + afterEach(async () => { + await factory.cleanDatabase() + }) + + describe('unauthenticated', () => { + let client + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect( + client.request(`mutation { + report( + description: "I don't like this user", + resource: { + id: "u2", + type: user + } + ) { id, createdAt } + }`) + ).rejects.toThrow('Not Authorised') + }) + + describe('authenticated', () => { + let headers + let response + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + response = await client.request(`mutation { + report( + description: "I don't like this user", + resource: { + id: "u2", + type: user + } + ) { id, createdAt } + }`, + { headers } + ) + }) + it('creates a report', () => { + let { id, createdAt } = response.report + expect(response).toEqual({ + report: { id, createdAt } + }) + }) + }) + }) +}) diff --git a/src/resolvers/statistics.js b/src/resolvers/statistics.js new file mode 100644 index 000000000..17c4be956 --- /dev/null +++ b/src/resolvers/statistics.js @@ -0,0 +1,67 @@ +export const query = (cypher, session) => { + return new Promise((resolve, reject) => { + let data = [] + session + .run(cypher) + .subscribe({ + onNext: function (record) { + let item = {} + record.keys.forEach(key => { + item[key] = record.get(key) + }) + data.push(item) + }, + onCompleted: function () { + session.close() + resolve(data) + }, + onError: function (error) { + reject(error) + } + }) + }) +} +const queryOne = (cypher, session) => { + return new Promise((resolve, reject) => { + query(cypher, session) + .then(res => { + resolve(res.length ? res.pop() : {}) + }) + .catch(err => { + reject(err) + }) + }) +} + +export default { + Query: { + statistics: async (parent, args, { driver, user }) => { + return new Promise(async (resolve) => { + const session = driver.session() + const queries = { + countUsers: 'MATCH (r:User) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countUsers', + countPosts: 'MATCH (r:Post) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countPosts', + countComments: 'MATCH (r:Comment) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countComments', + countNotifications: 'MATCH (r:Notification) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countNotifications', + countOrganizations: 'MATCH (r:Organization) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countOrganizations', + countProjects: 'MATCH (r:Project) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countProjects', + countInvites: 'MATCH (r:Invite) WHERE r.wasUsed <> true OR NOT exists(r.wasUsed) RETURN COUNT(r) AS countInvites', + countFollows: 'MATCH (:User)-[r:FOLLOWS]->(:User) RETURN COUNT(r) AS countFollows', + countShouts: 'MATCH (:User)-[r:SHOUTED]->(:Post) RETURN COUNT(r) AS countShouts' + } + let data = { + countUsers: (await queryOne(queries.countUsers, session)).countUsers.low, + countPosts: (await queryOne(queries.countPosts, session)).countPosts.low, + countComments: (await queryOne(queries.countComments, session)).countComments.low, + countNotifications: (await queryOne(queries.countNotifications, session)).countNotifications.low, + countOrganizations: (await queryOne(queries.countOrganizations, session)).countOrganizations.low, + countProjects: (await queryOne(queries.countProjects, session)).countProjects.low, + countInvites: (await queryOne(queries.countInvites, session)).countInvites.low, + countFollows: (await queryOne(queries.countFollows, session)).countFollows.low, + countShouts: (await queryOne(queries.countShouts, session)).countShouts.low + } + resolve(data) + }) + } + } +} diff --git a/src/resolvers/user_management.js b/src/resolvers/user_management.js new file mode 100644 index 000000000..b0c9d3df5 --- /dev/null +++ b/src/resolvers/user_management.js @@ -0,0 +1,52 @@ +import encode from '../jwt/encode' +import { fixUrl } from '../middleware/fixImageUrlsMiddleware' +import bcrypt from 'bcryptjs' +import { AuthenticationError } from 'apollo-server' + +export default { + Query: { + isLoggedIn: (parent, args, { driver, user }) => { + return Boolean(user && user.id) + }, + currentUser: (parent, args, { user }) => { + return user + } + }, + Mutation: { + signup: async (parent, { email, password }, { req }) => { + // if (data[email]) { + // throw new Error('Another User with same email exists.') + // } + // data[email] = { + // password: await bcrypt.hashSync(password, 10), + // } + + return true + }, + login: async (parent, { email, password }, { driver, req, user }) => { + // if (user && user.id) { + // throw new Error('Already logged in.') + // } + const session = driver.session() + return session.run( + 'MATCH (user:User {email: $userEmail}) ' + + 'RETURN user {.id, .slug, .name, .avatar, .email, .password, .role} as user LIMIT 1', { + userEmail: email + }) + .then(async (result) => { + session.close() + const [currentUser] = await result.records.map(function (record) { + return record.get('user') + }) + + if (currentUser && await bcrypt.compareSync(password, currentUser.password)) { + delete currentUser.password + currentUser.avatar = fixUrl(currentUser.avatar) + return Object.assign(currentUser, { + token: encode(currentUser) + }) + } else throw new AuthenticationError('Incorrect email address or password.') + }) + } + } +} diff --git a/src/resolvers/user_management.spec.js b/src/resolvers/user_management.spec.js new file mode 100644 index 000000000..cb12efb2d --- /dev/null +++ b/src/resolvers/user_management.spec.js @@ -0,0 +1,183 @@ +import Factory from '../seed/factories' +import { GraphQLClient, request } from 'graphql-request' +import jwt from 'jsonwebtoken' +import { host, login } from '../jest/helpers' + +const factory = Factory() + +// here is the decoded JWT token: +// { +// role: 'user', +// locationName: null, +// name: 'Jenny Rostock', +// about: null, +// avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg', +// id: 'u3', +// email: 'user@example.org', +// slug: 'jenny-rostock', +// iat: 1550846680, +// exp: 1637246680, +// aud: 'http://localhost:3000', +// iss: 'http://localhost:4000', +// sub: 'u3' +// } +const jennyRostocksHeaders = { authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc' } + +beforeEach(async () => { + await factory.create('User', { + avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/seyedhossein1/128.jpg', + id: 'acb2d923-f3af-479e-9f00-61b12e864666', + name: 'Matilde Hermiston', + slug: 'matilde-hermiston', + role: 'user', + email: 'test@example.org', + password: '1234' + }) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('isLoggedIn', () => { + const query = '{ isLoggedIn }' + describe('unauthenticated', () => { + it('returns false', async () => { + await expect(request(host, query)).resolves.toEqual({ isLoggedIn: false }) + }) + }) + + describe('with malformed JWT Bearer token', () => { + const headers = { authorization: 'blah' } + const client = new GraphQLClient(host, { headers }) + + it('returns false', async () => { + await expect(client.request(query)).resolves.toEqual({ isLoggedIn: false }) + }) + }) + + describe('with valid JWT Bearer token', () => { + const client = new GraphQLClient(host, { headers: jennyRostocksHeaders }) + + it('returns false', async () => { + await expect(client.request(query)).resolves.toEqual({ isLoggedIn: false }) + }) + + describe('and a corresponding user in the database', () => { + it('returns true', async () => { + // see the decoded token above + await factory.create('User', { id: 'u3' }) + await expect(client.request(query)).resolves.toEqual({ isLoggedIn: true }) + }) + }) + }) +}) + +describe('currentUser', () => { + const query = `{ + currentUser { + id + slug + name + avatar + email + role + token + } + }` + + describe('unauthenticated', () => { + it('returns null', async () => { + const expected = { currentUser: null } + await expect(request(host, query)).resolves.toEqual(expected) + }) + }) + + describe('with valid JWT Bearer Token', () => { + let client + let headers + + describe('but no corresponding user in the database', () => { + beforeEach(async () => { + client = new GraphQLClient(host, { headers: jennyRostocksHeaders }) + }) + + it('returns null', async () => { + const expected = { currentUser: null } + await expect(client.request(query)).resolves.toEqual(expected) + }) + }) + + describe('and corresponding user in the database', () => { + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('returns the whole user object', async () => { + const expected = { + currentUser: { + avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/seyedhossein1/128.jpg', + email: 'test@example.org', + id: 'acb2d923-f3af-479e-9f00-61b12e864666', + name: 'Matilde Hermiston', + slug: 'matilde-hermiston', + role: 'user', + token: headers.authorization.replace('Bearer ', '') + } + } + await expect(client.request(query)).resolves.toEqual(expected) + }) + }) + }) +}) + +describe('login', () => { + const mutation = (params) => { + const { email, password } = params + return ` + mutation { + login(email:"${email}", password:"${password}"){ + token + } + }` + } + + describe('ask for a `token`', () => { + describe('with valid email/password combination', () => { + it('responds with a JWT token', async () => { + const data = await request(host, mutation({ + email: 'test@example.org', + password: '1234' + })) + const { token } = data.login + jwt.verify(token, process.env.JWT_SECRET, (err, data) => { + expect(data.email).toEqual('test@example.org') + expect(err).toBeNull() + }) + }) + }) + + describe('with a valid email but incorrect password', () => { + it('responds with "Incorrect email address or password."', async () => { + await expect( + request(host, mutation({ + email: 'test@example.org', + password: 'wrong' + })) + ).rejects.toThrow('Incorrect email address or password.') + }) + }) + + describe('with a non-existing email', () => { + it('responds with "Incorrect email address or password."', async () => { + await expect( + request(host, mutation({ + email: 'non-existent@example.org', + password: 'wrong' + })) + ).rejects.toThrow('Incorrect email address or password.') + }) + }) + }) +}) diff --git a/src/schema.graphql b/src/schema.graphql index 55f23d5ca..4413e1deb 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -1,5 +1,6 @@ type Query { isLoggedIn: Boolean! + currentUser: LoggedInUser statistics: Statistics! } type Mutation { @@ -7,6 +8,7 @@ type Mutation { signup(email: String!, password: String!): Boolean! report(resource: Resource!, description: String): Report } + type LoggedInUser { id: ID! slug: String! @@ -14,8 +16,6 @@ type LoggedInUser { avatar:String! email: String! role: String! - locationName: String - about: String token: String! } diff --git a/src/server.js b/src/server.js index 5867e6952..2fc3af871 100644 --- a/src/server.js +++ b/src/server.js @@ -8,10 +8,7 @@ import middleware from './middleware' import applyDirectives from './bootstrap/directives' import applyScalars from './bootstrap/scalars' import { getDriver } from './bootstrap/neo4j' - -import passport from 'passport' -import jwtStrategy from './jwt/strategy' -import jwt from 'jsonwebtoken' +import decode from './jwt/decode' dotenv.config() // check env and warn @@ -42,20 +39,14 @@ schema = applyScalars(applyDirectives(schema)) const createServer = (options) => { const defaults = { - context: async (req) => { - const payload = { + context: async ({ request }) => { + const authorizationHeader = request.headers.authorization || '' + const user = await decode(driver, authorizationHeader) + return { driver, - user: null, - req: req.request + user, + req: request } - try { - const token = payload.req.headers.authorization.replace('Bearer ', '') - payload.user = await jwt.verify(token, process.env.JWT_SECRET) - } catch (err) { - // nothing - } - - return payload }, schema: schema, debug: debug, @@ -65,11 +56,7 @@ const createServer = (options) => { } const server = new GraphQLServer(Object.assign({}, defaults, options)) - passport.use('jwt', jwtStrategy(driver)) - server.express.use(passport.initialize()) server.express.use(express.static('public')) - - server.express.post('/graphql', passport.authenticate(['jwt'], { session: false })) return server } diff --git a/yarn.lock b/yarn.lock index 2fe610fee..5ec9c94a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4348,7 +4348,7 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= -jsonwebtoken@^8.2.0, jsonwebtoken@~8.5.0: +jsonwebtoken@~8.5.0: version "8.5.0" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#ebd0ca2a69797816e1c5af65b6c759787252947e" integrity sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA== @@ -5348,27 +5348,6 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= -passport-jwt@~4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.0.tgz#7f0be7ba942e28b9f5d22c2ebbb8ce96ef7cf065" - integrity sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg== - dependencies: - jsonwebtoken "^8.2.0" - passport-strategy "^1.0.0" - -passport-strategy@1.x.x, passport-strategy@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" - integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= - -passport@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.0.tgz#c5095691347bd5ad3b5e180238c3914d16f05811" - integrity sha1-xQlWkTR71a07XhgCOMORTRbwWBE= - dependencies: - passport-strategy "1.x.x" - pause "0.0.1" - path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" @@ -5423,11 +5402,6 @@ pathval@^1.1.0: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= -pause@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" - integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" From 7a063d1707bdff3b680e1be8fe2a5866519964c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Wed, 27 Feb 2019 04:34:03 +0000 Subject: [PATCH 11/66] Bump apollo-cache-inmemory from 1.4.3 to 1.5.0 Bumps [apollo-cache-inmemory](https://github.com/apollographql/apollo-client) from 1.4.3 to 1.5.0. - [Release notes](https://github.com/apollographql/apollo-client/releases) - [Changelog](https://github.com/apollographql/apollo-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/apollographql/apollo-client/compare/apollo-cache-inmemory@1.4.3...apollo-cache-inmemory@1.5.0) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 41 +++++++++++++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 173f1ced7..989925a2c 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ ] }, "dependencies": { - "apollo-cache-inmemory": "~1.4.3", + "apollo-cache-inmemory": "~1.5.0", "apollo-client": "~2.4.13", "apollo-link-http": "~1.5.11", "apollo-server": "~2.4.2", diff --git a/yarn.lock b/yarn.lock index 91e971746..473cf22ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -953,17 +953,18 @@ apollo-cache-control@^0.1.0: dependencies: graphql-extensions "^0.0.x" -apollo-cache-inmemory@~1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.4.3.tgz#aded4fb8b3de9e2fb2573a6c03591b07ef98ed36" - integrity sha512-p9KGtEZ9Mlb+FS0UEaxR8WvKOijYV0c+TXlhC/XZ3/ltYvP1zL3b1ozSOLGR9SawN2895Fc7QDV5nzPpihV0rA== +apollo-cache-inmemory@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.5.0.tgz#90b1a38f744762ab47c415b9d7b2d32fbd803088" + integrity sha512-hyg8R7G3XOfZhl8fQLs0vGEwi2rr8PuEIiumCY4qmwviaGsmwjs/Dm63cyeMm3Sfu05vzFQkwMdBf5u7xBg3cQ== dependencies: - apollo-cache "^1.1.26" - apollo-utilities "^1.1.3" + apollo-cache "^1.2.0" + apollo-utilities "^1.2.0" optimism "^0.6.9" + ts-invariant "^0.2.1" tslib "^1.9.3" -apollo-cache@1.1.26, apollo-cache@^1.1.26: +apollo-cache@1.1.26: version "1.1.26" resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.1.26.tgz#5afe023270effbc2063d90f51d8e56bce274ab37" integrity sha512-JKFHijwkhXpcQ3jOat+ctwiXyjDhQgy0p6GSaj7zG+or+ZSalPqUnPzFRgRwFLVbYxBKJgHCkWX+2VkxWTZzQQ== @@ -971,6 +972,14 @@ apollo-cache@1.1.26, apollo-cache@^1.1.26: apollo-utilities "^1.1.3" tslib "^1.9.3" +apollo-cache@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.2.0.tgz#7ebfc9a53027264e4a5bce9b886471cc77c50784" + integrity sha512-aZduG7o75crjWeONq9kVzkqO9hAcEklo3oN6Rd+ANpB+ImRklCFkz88XdXQ2/GHV26jh8VtQJIhiNIow10QzIg== + dependencies: + apollo-utilities "^1.2.0" + tslib "^1.9.3" + apollo-client@~2.4.13: version "2.4.13" resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.4.13.tgz#09829fcbd68e069de9840d0a10764d7c6a3d0787" @@ -1198,7 +1207,7 @@ apollo-upload-server@^7.0.0: http-errors "^1.7.0" object-path "^0.11.4" -apollo-utilities@1.1.3, apollo-utilities@^1.0.1, apollo-utilities@^1.1.3: +apollo-utilities@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.1.3.tgz#a8883c0392f6b46eac0d366204ebf34be9307c87" integrity sha512-pF9abhiClX5gfj/WFWZh8DiI33nOLGxRhXH9ZMquaM1V8bhq1WLFPt2QjShWH3kGQVeIGUK+FQefnhe+ZaaAYg== @@ -1206,6 +1215,15 @@ apollo-utilities@1.1.3, apollo-utilities@^1.0.1, apollo-utilities@^1.1.3: fast-json-stable-stringify "^2.0.0" tslib "^1.9.3" +apollo-utilities@^1.0.1, apollo-utilities@^1.1.3, apollo-utilities@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.2.0.tgz#ba5a1271c55e0d79069b1919c9d533164a70f9c3" + integrity sha512-4+vxCpEuirQjPqLCckBnOMGZxK/c+2eJtYEbh3xhccNM1pYB5Gx/lrfCdZS5MKDykuWZYY08EvidDy21nr049w== + dependencies: + fast-json-stable-stringify "^2.0.0" + ts-invariant "^0.2.1" + tslib "^1.9.3" + append-transform@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" @@ -6720,6 +6738,13 @@ trunc-text@1.0.1: resolved "https://registry.yarnpkg.com/trunc-text/-/trunc-text-1.0.1.tgz#58f876d8ac59b224b79834bb478b8656e69622b5" integrity sha1-WPh22KxZsiS3mDS7R4uGVuaWIrU= +ts-invariant@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.2.1.tgz#3d587f9d6e3bded97bf9ec17951dd9814d5a9d3f" + integrity sha512-Z/JSxzVmhTo50I+LKagEISFJW3pvPCqsMWLamCTX8Kr3N5aMrnGOqcflbe5hLUzwjvgPfnLzQtHZv0yWQ+FIHg== + dependencies: + tslib "^1.9.3" + tslib@^1.9.0, tslib@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" From 5fd44230ddc5660757185a78b572979da0d5e6de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 27 Feb 2019 10:42:55 +0100 Subject: [PATCH 12/66] Get rid of LoggedInUser graphql type This was causing a lot of headache on the frontend. Now, there a single source of truth. If you query `currentUser` you can overwrite your local copy of the user and update the UI. --- src/jest/helpers.js | 6 ++---- src/resolvers/user_management.js | 15 +++++++-------- src/resolvers/user_management.spec.js | 10 +++------- src/schema.graphql | 14 ++------------ src/seed/factories/index.js | 6 ++---- 5 files changed, 16 insertions(+), 35 deletions(-) diff --git a/src/jest/helpers.js b/src/jest/helpers.js index ff6a535e2..0d358ed40 100644 --- a/src/jest/helpers.js +++ b/src/jest/helpers.js @@ -7,12 +7,10 @@ export const host = 'http://127.0.0.1:4123' export async function login ({ email, password }) { const mutation = ` mutation { - login(email:"${email}", password:"${password}"){ - token - } + login(email:"${email}", password:"${password}") }` const response = await request(host, mutation) return { - authorization: `Bearer ${response.login.token}` + authorization: `Bearer ${response.login}` } } diff --git a/src/resolvers/user_management.js b/src/resolvers/user_management.js index b0c9d3df5..552f6201a 100644 --- a/src/resolvers/user_management.js +++ b/src/resolvers/user_management.js @@ -1,16 +1,18 @@ import encode from '../jwt/encode' -import { fixUrl } from '../middleware/fixImageUrlsMiddleware' import bcrypt from 'bcryptjs' import { AuthenticationError } from 'apollo-server' +import { neo4jgraphql } from 'neo4j-graphql-js' export default { Query: { isLoggedIn: (parent, args, { driver, user }) => { return Boolean(user && user.id) }, - currentUser: (parent, args, { user }) => { - return user - } + currentUser: async (object, params, ctx, resolveInfo) => { + const { user} = ctx + if(!user) return null + return neo4jgraphql(object, {id: user.id}, ctx, resolveInfo, false) + }, }, Mutation: { signup: async (parent, { email, password }, { req }) => { @@ -41,10 +43,7 @@ export default { if (currentUser && await bcrypt.compareSync(password, currentUser.password)) { delete currentUser.password - currentUser.avatar = fixUrl(currentUser.avatar) - return Object.assign(currentUser, { - token: encode(currentUser) - }) + return encode(currentUser) } else throw new AuthenticationError('Incorrect email address or password.') }) } diff --git a/src/resolvers/user_management.spec.js b/src/resolvers/user_management.spec.js index cb12efb2d..a3bf6fdd0 100644 --- a/src/resolvers/user_management.spec.js +++ b/src/resolvers/user_management.spec.js @@ -82,7 +82,6 @@ describe('currentUser', () => { avatar email role - token } }` @@ -122,8 +121,7 @@ describe('currentUser', () => { id: 'acb2d923-f3af-479e-9f00-61b12e864666', name: 'Matilde Hermiston', slug: 'matilde-hermiston', - role: 'user', - token: headers.authorization.replace('Bearer ', '') + role: 'user' } } await expect(client.request(query)).resolves.toEqual(expected) @@ -137,9 +135,7 @@ describe('login', () => { const { email, password } = params return ` mutation { - login(email:"${email}", password:"${password}"){ - token - } + login(email:"${email}", password:"${password}") }` } @@ -150,7 +146,7 @@ describe('login', () => { email: 'test@example.org', password: '1234' })) - const { token } = data.login + const token = data.login jwt.verify(token, process.env.JWT_SECRET, (err, data) => { expect(data.email).toEqual('test@example.org') expect(err).toBeNull() diff --git a/src/schema.graphql b/src/schema.graphql index 4413e1deb..1f9bcb477 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -1,24 +1,14 @@ type Query { isLoggedIn: Boolean! - currentUser: LoggedInUser + currentUser: User statistics: Statistics! } type Mutation { - login(email: String!, password: String!): LoggedInUser + login(email: String!, password: String!): String! signup(email: String!, password: String!): Boolean! report(resource: Resource!, description: String): Report } -type LoggedInUser { - id: ID! - slug: String! - name: String! - avatar:String! - email: String! - role: String! - token: String! -} - type Statistics { countUsers: Int! countPosts: Int! diff --git a/src/seed/factories/index.js b/src/seed/factories/index.js index d9bbd700c..ed35d2c3b 100644 --- a/src/seed/factories/index.js +++ b/src/seed/factories/index.js @@ -15,13 +15,11 @@ export const seedServerHost = 'http://127.0.0.1:4001' const authenticatedHeaders = async ({ email, password }, host) => { const mutation = ` mutation { - login(email:"${email}", password:"${password}"){ - token - } + login(email:"${email}", password:"${password}") }` const response = await request(host, mutation) return { - authorization: `Bearer ${response.login.token}` + authorization: `Bearer ${response.login}` } } const factories = { From f9b60fa0b0b7bbec42eab38c22e39d58695c066d Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Wed, 27 Feb 2019 08:10:20 -0300 Subject: [PATCH 13/66] Fix lint --- src/resolvers/user_management.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/resolvers/user_management.js b/src/resolvers/user_management.js index 552f6201a..ec4ae7ce2 100644 --- a/src/resolvers/user_management.js +++ b/src/resolvers/user_management.js @@ -9,10 +9,10 @@ export default { return Boolean(user && user.id) }, currentUser: async (object, params, ctx, resolveInfo) => { - const { user} = ctx - if(!user) return null - return neo4jgraphql(object, {id: user.id}, ctx, resolveInfo, false) - }, + const { user } = ctx + if (!user) return null + return neo4jgraphql(object, { id: user.id }, ctx, resolveInfo, false) + } }, Mutation: { signup: async (parent, { email, password }, { req }) => { From 100aad61e12424e868453005366754a6de1aafa5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Wed, 27 Feb 2019 19:33:45 +0000 Subject: [PATCH 14/66] Bump apollo-client from 2.4.13 to 2.5.1 Bumps [apollo-client](https://github.com/apollographql/apollo-client) from 2.4.13 to 2.5.1. - [Release notes](https://github.com/apollographql/apollo-client/releases) - [Changelog](https://github.com/apollographql/apollo-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/apollographql/apollo-client/compare/apollo-client@2.4.13...apollo-client@2.5.1) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 47 ++++++++++++++++------------------------------- 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 989925a2c..a50bf4bfb 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "apollo-cache-inmemory": "~1.5.0", - "apollo-client": "~2.4.13", + "apollo-client": "~2.5.1", "apollo-link-http": "~1.5.11", "apollo-server": "~2.4.2", "bcryptjs": "~2.4.3", diff --git a/yarn.lock b/yarn.lock index 473cf22ce..ab877303f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -964,33 +964,26 @@ apollo-cache-inmemory@~1.5.0: ts-invariant "^0.2.1" tslib "^1.9.3" -apollo-cache@1.1.26: - version "1.1.26" - resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.1.26.tgz#5afe023270effbc2063d90f51d8e56bce274ab37" - integrity sha512-JKFHijwkhXpcQ3jOat+ctwiXyjDhQgy0p6GSaj7zG+or+ZSalPqUnPzFRgRwFLVbYxBKJgHCkWX+2VkxWTZzQQ== +apollo-cache@1.2.1, apollo-cache@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.2.1.tgz#aae71eb4a11f1f7322adc343f84b1a39b0693644" + integrity sha512-nzFmep/oKlbzUuDyz6fS6aYhRmfpcHWqNkkA9Bbxwk18RD6LXC4eZkuE0gXRX0IibVBHNjYVK+Szi0Yied4SpQ== dependencies: - apollo-utilities "^1.1.3" + apollo-utilities "^1.2.1" tslib "^1.9.3" -apollo-cache@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.2.0.tgz#7ebfc9a53027264e4a5bce9b886471cc77c50784" - integrity sha512-aZduG7o75crjWeONq9kVzkqO9hAcEklo3oN6Rd+ANpB+ImRklCFkz88XdXQ2/GHV26jh8VtQJIhiNIow10QzIg== - dependencies: - apollo-utilities "^1.2.0" - tslib "^1.9.3" - -apollo-client@~2.4.13: - version "2.4.13" - resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.4.13.tgz#09829fcbd68e069de9840d0a10764d7c6a3d0787" - integrity sha512-7mBdW/CW1qHB8Mj4EFAG3MTtbRc6S8aUUntUdrKfRWV1rZdWa0NovxsgVD/R4HZWZjRQ2UOM4ENsHdM5g1uXOQ== +apollo-client@~2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.5.1.tgz#36126ed1d32edd79c3713c6684546a3bea80e6d1" + integrity sha512-MNcQKiqLHdGmNJ0rZ0NXaHrToXapJgS/5kPk0FygXt+/FmDCdzqcujI7OPxEC6e9Yw5S/8dIvOXcRNuOMElHkA== dependencies: "@types/zen-observable" "^0.8.0" - apollo-cache "1.1.26" + apollo-cache "1.2.1" apollo-link "^1.0.0" apollo-link-dedup "^1.0.0" - apollo-utilities "1.1.3" + apollo-utilities "1.2.1" symbol-observable "^1.0.2" + ts-invariant "^0.2.1" tslib "^1.9.3" zen-observable "^0.8.0" @@ -1207,18 +1200,10 @@ apollo-upload-server@^7.0.0: http-errors "^1.7.0" object-path "^0.11.4" -apollo-utilities@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.1.3.tgz#a8883c0392f6b46eac0d366204ebf34be9307c87" - integrity sha512-pF9abhiClX5gfj/WFWZh8DiI33nOLGxRhXH9ZMquaM1V8bhq1WLFPt2QjShWH3kGQVeIGUK+FQefnhe+ZaaAYg== - dependencies: - fast-json-stable-stringify "^2.0.0" - tslib "^1.9.3" - -apollo-utilities@^1.0.1, apollo-utilities@^1.1.3, apollo-utilities@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.2.0.tgz#ba5a1271c55e0d79069b1919c9d533164a70f9c3" - integrity sha512-4+vxCpEuirQjPqLCckBnOMGZxK/c+2eJtYEbh3xhccNM1pYB5Gx/lrfCdZS5MKDykuWZYY08EvidDy21nr049w== +apollo-utilities@1.2.1, apollo-utilities@^1.0.1, apollo-utilities@^1.2.0, apollo-utilities@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.2.1.tgz#1c3a1ebf5607d7c8efe7636daaf58e7463b41b3c" + integrity sha512-Zv8Udp9XTSFiN8oyXOjf6PMHepD4yxxReLsl6dPUy5Ths7jti3nmlBzZUOxuTWRwZn0MoclqL7RQ5UEJN8MAxg== dependencies: fast-json-stable-stringify "^2.0.0" ts-invariant "^0.2.1" From 85a702c8de32f6b1a5e799b253e3808a553085aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Wed, 27 Feb 2019 20:54:07 +0000 Subject: [PATCH 15/66] Bump apollo-server from 2.4.2 to 2.4.8 Bumps [apollo-server](https://github.com/apollographql/apollo-server) from 2.4.2 to 2.4.8. - [Release notes](https://github.com/apollographql/apollo-server/releases) - [Changelog](https://github.com/apollographql/apollo-server/blob/master/CHANGELOG.md) - [Commits](https://github.com/apollographql/apollo-server/compare/apollo-server@2.4.2...apollo-server@2.4.8) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 100 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index a50bf4bfb..1ff3ce9ab 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "apollo-cache-inmemory": "~1.5.0", "apollo-client": "~2.5.1", "apollo-link-http": "~1.5.11", - "apollo-server": "~2.4.2", + "apollo-server": "~2.4.8", "bcryptjs": "~2.4.3", "cheerio": "~1.0.0-rc.2", "cross-env": "~5.2.0", diff --git a/yarn.lock b/yarn.lock index ab877303f..ab8c1d402 100644 --- a/yarn.lock +++ b/yarn.lock @@ -946,6 +946,14 @@ apollo-cache-control@0.5.1: apollo-server-env "2.2.0" graphql-extensions "0.5.2" +apollo-cache-control@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.5.2.tgz#47931ede0b11c64d45429850c274b30d19322362" + integrity sha512-uehXDUrd3Qim+nzxqqN7XT1YTbNSyumW3/FY5BxbKZTI8d4oPG4eyVQKqaggooSjswKQnOoIQVes3+qg9tGAkw== + dependencies: + apollo-server-env "2.2.0" + graphql-extensions "0.5.4" + apollo-cache-control@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.1.1.tgz#173d14ceb3eb9e7cb53de7eb8b61bee6159d4171" @@ -1014,6 +1022,18 @@ apollo-engine-reporting@1.0.2: async-retry "^1.2.1" graphql-extensions "0.5.2" +apollo-engine-reporting@1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.0.7.tgz#d326b51b12b1f71a40885b8189dbcd162171c953" + integrity sha512-mFsXvd+1/o5jSa9tI2RoXYGcvCLcwwcfLwchjSTxqUd4ViB8RbqYKynzEZ+Omji7PBRM0azioBm43f7PSsQPqA== + dependencies: + apollo-engine-reporting-protobuf "0.2.1" + apollo-graphql "^0.1.0" + apollo-server-core "2.4.8" + apollo-server-env "2.2.0" + async-retry "^1.2.1" + graphql-extensions "0.5.7" + apollo-env@0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.3.3.tgz#e758ece2fbc4f81abc6f07442680ed9e314ecf6c" @@ -1091,6 +1111,32 @@ apollo-server-core@2.4.2: subscriptions-transport-ws "^0.9.11" ws "^6.0.0" +apollo-server-core@2.4.8: + version "2.4.8" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.4.8.tgz#47e503a345e314222725597c889773e018d8c67a" + integrity sha512-N+5UOzHhMOnHizEiArJtNvEe/cGhSHQyTn5tlU4RJ36FDBJ/WlYZfPbGDMLISSUCJ6t+aP8GLL4Mnudt9d2PDQ== + dependencies: + "@apollographql/apollo-tools" "^0.3.3" + "@apollographql/graphql-playground-html" "^1.6.6" + "@types/ws" "^6.0.0" + apollo-cache-control "0.5.2" + apollo-datasource "0.3.1" + apollo-engine-reporting "1.0.7" + apollo-server-caching "0.3.1" + apollo-server-env "2.2.0" + apollo-server-errors "2.2.1" + apollo-server-plugin-base "0.3.7" + apollo-tracing "0.5.2" + fast-json-stable-stringify "^2.0.0" + graphql-extensions "0.5.7" + graphql-subscriptions "^1.0.0" + graphql-tag "^2.9.2" + graphql-tools "^4.0.0" + graphql-upload "^8.0.2" + sha.js "^2.4.11" + subscriptions-transport-ws "^0.9.11" + ws "^6.0.0" + apollo-server-core@^1.3.6, apollo-server-core@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-1.4.0.tgz#4faff7f110bfdd6c3f47008302ae24140f94c592" @@ -1113,10 +1159,15 @@ apollo-server-errors@2.2.0: resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.2.0.tgz#5b452a1d6ff76440eb0f127511dc58031a8f3cb5" integrity sha512-gV9EZG2tovFtT1cLuCTavnJu2DaKxnXPRNGSTo+SDI6IAk6cdzyW0Gje5N2+3LybI0Wq5KAbW6VLei31S4MWmg== -apollo-server-express@2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.4.2.tgz#870daf04fd9b727a54b1339cdb55066450b3e05a" - integrity sha512-Q5/unCAi6a2dT39LQaIiLC1d8O4fmBDU2CrRhVhPWP8I344xPgNOcrs7MsNN7Ecb56UGbgDKxBoWowFG65ulKw== +apollo-server-errors@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.2.1.tgz#f68a3f845929768057da7e1c6d30517db5872205" + integrity sha512-wY/YE3iJVMYC+WYIf8QODBjIP4jhI+oc7kiYo9mrz7LdYPKAgxr/he+NteGcqn/0Ea9K5/ZFTGJDbEstSMeP8g== + +apollo-server-express@2.4.8: + version "2.4.8" + resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.4.8.tgz#ec9eb61a87324555d49097e9fade3c7d142eb6cb" + integrity sha512-i60l32mfVe33jnKDPNYgUKUKu4Al0xEm2HLOSMgtJ9Wbpe/MbOx5X8M5F27fnHYdM+G5XfAErsakAyRGnQJ48Q== dependencies: "@apollographql/graphql-playground-html" "^1.6.6" "@types/accepts" "^1.3.5" @@ -1124,7 +1175,7 @@ apollo-server-express@2.4.2: "@types/cors" "^2.8.4" "@types/express" "4.16.1" accepts "^1.3.5" - apollo-server-core "2.4.2" + apollo-server-core "2.4.8" body-parser "^1.18.3" cors "^2.8.4" graphql-subscriptions "^1.0.0" @@ -1157,6 +1208,11 @@ apollo-server-plugin-base@0.3.2: resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.3.2.tgz#4609c9a9d154568401d84b7ac17d1e30e3529104" integrity sha512-yzXrkVSPBoux2uPgbTGROGh7W0axRWopMZM+KT9aF9H/+yMCwtt0EhGOGyNUDMOFA4rT3z+cLVvYuZr1rSQWcg== +apollo-server-plugin-base@0.3.7: + version "0.3.7" + resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.3.7.tgz#bfa4932fc9481bb36221545578d311db464af5a6" + integrity sha512-hW1jaLKf9qNOxMTwRq2CSqz3eqXsZuEiCc8/mmEtOciiVBq1GMtxFf19oIYM9HQuPvQU2RWpns1VrYN59L3vbg== + apollo-server-testing@~2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.4.2.tgz#5c87b34b0b6a1a3e5a1784cadc16bc495dded2e1" @@ -1164,13 +1220,13 @@ apollo-server-testing@~2.4.2: dependencies: apollo-server-core "2.4.2" -apollo-server@~2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.4.2.tgz#6ca42906664918c32d8be391db1178c6eaa8dc54" - integrity sha512-oFwHUhJtJedIPCtZ2ftvRWrVA3KpmeWsgDBs4e5jW/7f5ozkUF22h7L3iy8rEljppiEa2X/GgSi11SVxIOL8gw== +apollo-server@~2.4.8: + version "2.4.8" + resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.4.8.tgz#b18ec24e6356dd2a191a6fa0f1429b07ef7c89e3" + integrity sha512-IU6RekO2dqrDdC+5hU6aeVvGg/2t/f01inBMjDhAn1a7hoITUXEh8Sa57TgmYEZ5uAtDuWW7cdiZN2j0cMI3/w== dependencies: - apollo-server-core "2.4.2" - apollo-server-express "2.4.2" + apollo-server-core "2.4.8" + apollo-server-express "2.4.8" express "^4.0.0" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" @@ -1183,6 +1239,14 @@ apollo-tracing@0.5.1: apollo-server-env "2.2.0" graphql-extensions "0.5.2" +apollo-tracing@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.5.2.tgz#cc49936fb435fa98d19c841514cfe05237c85b33" + integrity sha512-2FdwRvPIq9uuF6OzONroXep6VBGqzHOkP6LlcFQe7SdwxfRP+SD/ycHNSC1acVg2b8d+am9Kzqg2vV54UpOIKA== + dependencies: + apollo-server-env "2.2.0" + graphql-extensions "0.5.4" + apollo-tracing@^0.1.0: version "0.1.4" resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.1.4.tgz#5b8ae1b01526b160ee6e552a7f131923a9aedcc7" @@ -3107,6 +3171,20 @@ graphql-extensions@0.5.2: dependencies: "@apollographql/apollo-tools" "^0.3.3" +graphql-extensions@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.5.4.tgz#18a9674f9adb11aa6c0737485887ea8877914cff" + integrity sha512-qLThJGVMqcItE7GDf/xX/E40m/aeqFheEKiR5bfra4q5eHxQKGjnIc20P9CVqjOn9I0FkEiU9ypOobfmIf7t6g== + dependencies: + "@apollographql/apollo-tools" "^0.3.3" + +graphql-extensions@0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.5.7.tgz#2b647e4e36997dc85b7f58ebd64324a5250fb2cf" + integrity sha512-HrU6APE1PiehZ46scMB3S5DezSeCATd8v+e4mmg2bqszMyCFkmAnmK6hR1b5VjHxhzt5/FX21x1WsXfqF4FwdQ== + dependencies: + "@apollographql/apollo-tools" "^0.3.3" + graphql-extensions@^0.0.x, graphql-extensions@~0.0.9: version "0.0.10" resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.0.10.tgz#34bdb2546d43f6a5bc89ab23c295ec0466c6843d" From e74962a2beb2b8ea351a3df92e06afa58a200890 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Thu, 28 Feb 2019 04:29:33 +0000 Subject: [PATCH 16/66] Bump apollo-cache-inmemory from 1.5.0 to 1.5.1 Bumps [apollo-cache-inmemory](https://github.com/apollographql/apollo-client) from 1.5.0 to 1.5.1. - [Release notes](https://github.com/apollographql/apollo-client/releases) - [Changelog](https://github.com/apollographql/apollo-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/apollographql/apollo-client/compare/apollo-cache-inmemory@1.5.0...apollo-cache-inmemory@1.5.1) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 1ff3ce9ab..f39b28a22 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ ] }, "dependencies": { - "apollo-cache-inmemory": "~1.5.0", + "apollo-cache-inmemory": "~1.5.1", "apollo-client": "~2.5.1", "apollo-link-http": "~1.5.11", "apollo-server": "~2.4.8", diff --git a/yarn.lock b/yarn.lock index ab8c1d402..15f107045 100644 --- a/yarn.lock +++ b/yarn.lock @@ -961,18 +961,18 @@ apollo-cache-control@^0.1.0: dependencies: graphql-extensions "^0.0.x" -apollo-cache-inmemory@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.5.0.tgz#90b1a38f744762ab47c415b9d7b2d32fbd803088" - integrity sha512-hyg8R7G3XOfZhl8fQLs0vGEwi2rr8PuEIiumCY4qmwviaGsmwjs/Dm63cyeMm3Sfu05vzFQkwMdBf5u7xBg3cQ== +apollo-cache-inmemory@~1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.5.1.tgz#265d1ee67b0bf0aca9c37629d410bfae44e62953" + integrity sha512-D3bdpPmWfaKQkWy8lfwUg+K8OBITo3sx0BHLs1B/9vIdOIZ7JNCKq3EUcAgAfInomJUdN0QG1yOfi8M8hxkN1g== dependencies: - apollo-cache "^1.2.0" - apollo-utilities "^1.2.0" + apollo-cache "^1.2.1" + apollo-utilities "^1.2.1" optimism "^0.6.9" ts-invariant "^0.2.1" tslib "^1.9.3" -apollo-cache@1.2.1, apollo-cache@^1.2.0: +apollo-cache@1.2.1, apollo-cache@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.2.1.tgz#aae71eb4a11f1f7322adc343f84b1a39b0693644" integrity sha512-nzFmep/oKlbzUuDyz6fS6aYhRmfpcHWqNkkA9Bbxwk18RD6LXC4eZkuE0gXRX0IibVBHNjYVK+Szi0Yied4SpQ== @@ -1264,7 +1264,7 @@ apollo-upload-server@^7.0.0: http-errors "^1.7.0" object-path "^0.11.4" -apollo-utilities@1.2.1, apollo-utilities@^1.0.1, apollo-utilities@^1.2.0, apollo-utilities@^1.2.1: +apollo-utilities@1.2.1, apollo-utilities@^1.0.1, apollo-utilities@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.2.1.tgz#1c3a1ebf5607d7c8efe7636daaf58e7463b41b3c" integrity sha512-Zv8Udp9XTSFiN8oyXOjf6PMHepD4yxxReLsl6dPUy5Ths7jti3nmlBzZUOxuTWRwZn0MoclqL7RQ5UEJN8MAxg== From 4b2ca9368b6eb155175426b7d9f21cecd4194699 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Thu, 28 Feb 2019 08:58:00 +0000 Subject: [PATCH 17/66] Bump apollo-server-testing from 2.4.2 to 2.4.8 Bumps [apollo-server-testing](https://github.com/apollographql/apollo-server) from 2.4.2 to 2.4.8. - [Release notes](https://github.com/apollographql/apollo-server/releases) - [Changelog](https://github.com/apollographql/apollo-server/blob/master/CHANGELOG.md) - [Commits](https://github.com/apollographql/apollo-server/compare/apollo-server-testing@2.4.2...apollo-server-testing@2.4.8) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 81 ++++------------------------------------------------ 2 files changed, 6 insertions(+), 77 deletions(-) diff --git a/package.json b/package.json index f39b28a22..50b2eddf3 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@babel/node": "~7.2.2", "@babel/preset-env": "~7.3.1", "@babel/register": "~7.0.0", - "apollo-server-testing": "~2.4.2", + "apollo-server-testing": "~2.4.8", "babel-core": "~7.0.0-0", "babel-eslint": "~10.0.1", "babel-jest": "~24.1.0", diff --git a/yarn.lock b/yarn.lock index 15f107045..70a337743 100644 --- a/yarn.lock +++ b/yarn.lock @@ -938,14 +938,6 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -apollo-cache-control@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.5.1.tgz#db7e86cd7ede6cad0a2e0ea97488d9fb5c33d913" - integrity sha512-82hzX7/fFiu5dODLS8oGieEE4jLjMIhIkQ4JTsWj9drv8PZJltl0xqORtU2jA/FottjxfYab8+ebi3BgGPOaqw== - dependencies: - apollo-server-env "2.2.0" - graphql-extensions "0.5.2" - apollo-cache-control@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.5.2.tgz#47931ede0b11c64d45429850c274b30d19322362" @@ -1010,18 +1002,6 @@ apollo-engine-reporting-protobuf@0.2.1: dependencies: protobufjs "^6.8.6" -apollo-engine-reporting@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.0.2.tgz#5dd5411f17d76e6788a1166367a2ab7b52794224" - integrity sha512-g6JkO5WaMuqXfn3WoZMQyyFzpxfHsw/f7P7XTHSEqTSd/M4uk7/uih/xcqmgBGt4ET30KbaGFz2l4FJzO06A5w== - dependencies: - apollo-engine-reporting-protobuf "0.2.1" - apollo-graphql "^0.1.0" - apollo-server-core "2.4.2" - apollo-server-env "2.2.0" - async-retry "^1.2.1" - graphql-extensions "0.5.2" - apollo-engine-reporting@1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.0.7.tgz#d326b51b12b1f71a40885b8189dbcd162171c953" @@ -1085,32 +1065,6 @@ apollo-server-caching@0.3.1: dependencies: lru-cache "^5.0.0" -apollo-server-core@2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.4.2.tgz#916d98636b1bf576a84b4a469006c1c73741e03a" - integrity sha512-IOWhqjjI1sH38sj7ycjke0dXXEgaqOkb2hDpLBTSiVWKBIqFfo4gchWK5wcWW9jReDpf/+G2wogH+UvONs2ejg== - dependencies: - "@apollographql/apollo-tools" "^0.3.3" - "@apollographql/graphql-playground-html" "^1.6.6" - "@types/ws" "^6.0.0" - apollo-cache-control "0.5.1" - apollo-datasource "0.3.1" - apollo-engine-reporting "1.0.2" - apollo-server-caching "0.3.1" - apollo-server-env "2.2.0" - apollo-server-errors "2.2.0" - apollo-server-plugin-base "0.3.2" - apollo-tracing "0.5.1" - fast-json-stable-stringify "^2.0.0" - graphql-extensions "0.5.2" - graphql-subscriptions "^1.0.0" - graphql-tag "^2.9.2" - graphql-tools "^4.0.0" - graphql-upload "^8.0.2" - sha.js "^2.4.11" - subscriptions-transport-ws "^0.9.11" - ws "^6.0.0" - apollo-server-core@2.4.8: version "2.4.8" resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.4.8.tgz#47e503a345e314222725597c889773e018d8c67a" @@ -1154,11 +1108,6 @@ apollo-server-env@2.2.0: node-fetch "^2.1.2" util.promisify "^1.0.0" -apollo-server-errors@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.2.0.tgz#5b452a1d6ff76440eb0f127511dc58031a8f3cb5" - integrity sha512-gV9EZG2tovFtT1cLuCTavnJu2DaKxnXPRNGSTo+SDI6IAk6cdzyW0Gje5N2+3LybI0Wq5KAbW6VLei31S4MWmg== - apollo-server-errors@2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.2.1.tgz#f68a3f845929768057da7e1c6d30517db5872205" @@ -1203,22 +1152,17 @@ apollo-server-module-graphiql@^1.3.4, apollo-server-module-graphiql@^1.4.0: resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec" integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA== -apollo-server-plugin-base@0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.3.2.tgz#4609c9a9d154568401d84b7ac17d1e30e3529104" - integrity sha512-yzXrkVSPBoux2uPgbTGROGh7W0axRWopMZM+KT9aF9H/+yMCwtt0EhGOGyNUDMOFA4rT3z+cLVvYuZr1rSQWcg== - apollo-server-plugin-base@0.3.7: version "0.3.7" resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.3.7.tgz#bfa4932fc9481bb36221545578d311db464af5a6" integrity sha512-hW1jaLKf9qNOxMTwRq2CSqz3eqXsZuEiCc8/mmEtOciiVBq1GMtxFf19oIYM9HQuPvQU2RWpns1VrYN59L3vbg== -apollo-server-testing@~2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.4.2.tgz#5c87b34b0b6a1a3e5a1784cadc16bc495dded2e1" - integrity sha512-WZ901nh7uG75342lMukJvuxFF/w3W5JDyWElY8KDhXfaDLbMKqhRqaWRIiEEX4YvciO5ACSzqKt+957/y1yUUQ== +apollo-server-testing@~2.4.8: + version "2.4.8" + resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.4.8.tgz#eb929a431e059723c298919688355434d53e3ea8" + integrity sha512-AmNn5pDn9FJ9AJbmc7gwsUFaUt4uf44IFHaCfZow/jkAeY2JZnIozt8LYC8Koidy+Lfb+i/HsjkgbBodElbGMQ== dependencies: - apollo-server-core "2.4.2" + apollo-server-core "2.4.8" apollo-server@~2.4.8: version "2.4.8" @@ -1231,14 +1175,6 @@ apollo-server@~2.4.8: graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" -apollo-tracing@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.5.1.tgz#16be201bc276120f0f8b7aa180201ee89d57e3bd" - integrity sha512-5gb8OWzkGaJFsmQdyMyZnOjcq6weMTkqJSGj0hfR7uX99X4SBFAzZV4nTeK4z0XkXO2I12xSTJoS4gxbFjgeaA== - dependencies: - apollo-server-env "2.2.0" - graphql-extensions "0.5.2" - apollo-tracing@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.5.2.tgz#cc49936fb435fa98d19c841514cfe05237c85b33" @@ -3164,13 +3100,6 @@ graphql-deduplicator@^2.0.1: resolved "https://registry.yarnpkg.com/graphql-deduplicator/-/graphql-deduplicator-2.0.2.tgz#d8608161cf6be97725e178df0c41f6a1f9f778f3" integrity sha512-0CGmTmQh4UvJfsaTPppJAcHwHln8Ayat7yXXxdnuWT+Mb1dBzkbErabCWzjXyKh/RefqlGTTA7EQOZHofMaKJA== -graphql-extensions@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.5.2.tgz#cdced94c1931c9983fffcc144e336d6cd4d8b02b" - integrity sha512-D/FAvjYEZ8GM3vfALxRvItozy5iLUfzyoauE2lli+0OuUBCAZDLP0fgqeTFK93NnQX/XSjBVGhcuDWBB7JesEw== - dependencies: - "@apollographql/apollo-tools" "^0.3.3" - graphql-extensions@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.5.4.tgz#18a9674f9adb11aa6c0737485887ea8877914cff" From 57c24f62d19b99174026ec6158817186a390579c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Thu, 28 Feb 2019 09:25:48 +0000 Subject: [PATCH 18/66] Bump @babel/preset-env from 7.3.1 to 7.3.4 Bumps [@babel/preset-env](https://github.com/babel/babel) from 7.3.1 to 7.3.4. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/master/CHANGELOG.md) - [Commits](https://github.com/babel/babel/compare/v7.3.1...v7.3.4) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 132 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 92 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 50b2eddf3..c276a09d7 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@babel/cli": "~7.2.3", "@babel/core": "~7.3.3", "@babel/node": "~7.2.2", - "@babel/preset-env": "~7.3.1", + "@babel/preset-env": "~7.3.4", "@babel/register": "~7.0.0", "apollo-server-testing": "~2.4.8", "babel-core": "~7.0.0-0", diff --git a/yarn.lock b/yarn.lock index 70a337743..c36e11172 100644 --- a/yarn.lock +++ b/yarn.lock @@ -69,6 +69,17 @@ source-map "^0.5.0" trim-right "^1.0.1" +"@babel/generator@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e" + integrity sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg== + dependencies: + "@babel/types" "^7.3.4" + jsesc "^2.5.1" + lodash "^4.17.11" + source-map "^0.5.0" + trim-right "^1.0.1" + "@babel/helper-annotate-as-pure@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" @@ -199,6 +210,16 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" +"@babel/helper-replace-supers@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.3.4.tgz#a795208e9b911a6eeb08e5891faacf06e7013e13" + integrity sha512-pvObL9WVf2ADs+ePg0jrqlhHoxRXlOa+SHRHzAXIz2xkYuOHfGl+fKxPMaS4Fq+uje8JQPobnertBBvyrWnQ1A== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.0.0" + "@babel/helper-optimise-call-expression" "^7.0.0" + "@babel/traverse" "^7.3.4" + "@babel/types" "^7.3.4" + "@babel/helper-simple-access@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz#65eeb954c8c245beaa4e859da6188f39d71e585c" @@ -258,6 +279,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.3.tgz#092d450db02bdb6ccb1ca8ffd47d8774a91aef87" integrity sha512-xsH1CJoln2r74hR+y7cg2B5JCPaTh+Hd+EbBRk9nWGSNspuo6krjhX0Om6RnRQuIvFq8wVXCLKH3kwKDYhanSg== +"@babel/parser@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c" + integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ== + "@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" @@ -275,10 +301,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-json-strings" "^7.2.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.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.4.tgz#47f73cf7f2a721aad5c0261205405c642e424654" + integrity sha512-j7VQmbbkA+qrzNqbKHrBsW3ddFnOeva6wzSe/zB7T+xaxGc+RCpwo44wCmRixAIGRoIpmVgvzFzNJqQcO3/9RA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" @@ -335,10 +361,10 @@ dependencies: "@babel/helper-plugin-utils" "^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.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.3.4.tgz#4e45408d3c3da231c0e7b823f407a53a7eb3048c" + integrity sha512-Y7nCzv2fw/jEZ9f678MuKdMo99MFDJMT/PvD9LisrR5JDFcJH6vYeH6RnjVt3p5tceyGRvTtEN0VOlU+rgHZjA== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -351,25 +377,25 @@ dependencies: "@babel/helper-plugin-utils" "^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.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.3.4.tgz#5c22c339de234076eee96c8783b2fed61202c5c4" + integrity sha512-blRr2O8IOZLAOJklXLV4WhcEzpYafYQKSGT3+R26lWG41u/FODJuBggehtOwilVAcFu393v3OFj+HmaE6tVjhA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - lodash "^4.17.10" + lodash "^4.17.11" -"@babel/plugin-transform-classes@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.2.0.tgz#374f8876075d7d21fea55aeb5c53561259163f96" - integrity sha512-aPCEkrhJYebDXcGTAP+cdUENkH7zqOlgbKwLbghjjHpJRJBWM/FSlCjMoPGA8oUdiMfOrk3+8EFPLLb5r7zj2w== +"@babel/plugin-transform-classes@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.3.4.tgz#dc173cb999c6c5297e0b5f2277fdaaec3739d0cc" + integrity sha512-J9fAvCFBkXEvBimgYxCjvaVDzL6thk0j0dBvCeZmIUDBwyt+nv6HfbImsSrWsYXfDNDivyANgJlFXDUWRTZBuA== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-define-map" "^7.1.0" "@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-replace-supers" "^7.3.4" "@babel/helper-split-export-declaration" "^7.0.0" globals "^11.1.0" @@ -450,10 +476,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-simple-access" "^7.1.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.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.3.4.tgz#813b34cd9acb6ba70a84939f3680be0eb2e58861" + integrity sha512-VZ4+jlGOF36S7TjKs8g4ojp4MEI+ebCQZdswWb/T9I4X84j8OtFAyjXjt/M16iIm5RIZn0UMQgg/VgIwo/87vw== dependencies: "@babel/helper-hoist-variables" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -497,12 +523,12 @@ "@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-regenerator@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.3.4.tgz#1601655c362f5b38eead6a52631f5106b29fa46a" + integrity sha512-hvJg8EReQvXT6G9H2MvNPXkv9zK36Vxa1+csAVTpE1J3j0zlHplw76uudEbJxgvqZzAq9Yh45FLD4pk5mKRFQA== dependencies: - regenerator-transform "^0.13.3" + regenerator-transform "^0.13.4" "@babel/plugin-transform-shorthand-properties@^7.2.0": version "7.2.0" @@ -558,16 +584,16 @@ core-js "^2.5.7" regenerator-runtime "^0.11.1" -"@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.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.4.tgz#887cf38b6d23c82f19b5135298bdb160062e33e1" + integrity sha512-2mwqfYMK8weA0g0uBKOt4FE3iEodiHy9/CW0b+nWXcbL+pGzLx8ESYc+j9IIxr6LTDHWKgPm71i9smo02bw+gA== 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.3.4" "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" "@babel/plugin-proposal-unicode-property-regex" "^7.2.0" "@babel/plugin-syntax-async-generators" "^7.2.0" @@ -575,10 +601,10 @@ "@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.3.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.3.4" + "@babel/plugin-transform-classes" "^7.3.4" "@babel/plugin-transform-computed-properties" "^7.2.0" "@babel/plugin-transform-destructuring" "^7.2.0" "@babel/plugin-transform-dotall-regex" "^7.2.0" @@ -589,13 +615,13 @@ "@babel/plugin-transform-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-systemjs" "^7.3.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-object-super" "^7.2.0" "@babel/plugin-transform-parameters" "^7.2.0" - "@babel/plugin-transform-regenerator" "^7.0.0" + "@babel/plugin-transform-regenerator" "^7.3.4" "@babel/plugin-transform-shorthand-properties" "^7.2.0" "@babel/plugin-transform-spread" "^7.2.0" "@babel/plugin-transform-sticky-regex" "^7.2.0" @@ -651,6 +677,21 @@ globals "^11.1.0" lodash "^4.17.10" +"@babel/traverse@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06" + integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.3.4" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.0.0" + "@babel/parser" "^7.3.4" + "@babel/types" "^7.3.4" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.11" + "@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.3": version "7.3.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.3.tgz#6c44d1cdac2a7625b624216657d5bc6c107ab436" @@ -660,6 +701,15 @@ lodash "^4.17.11" to-fast-properties "^2.0.0" +"@babel/types@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed" + integrity sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ== + dependencies: + esutils "^2.0.2" + lodash "^4.17.11" + to-fast-properties "^2.0.0" + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -5790,10 +5840,10 @@ regenerator-runtime@^0.12.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== -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.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb" + integrity sha512-T0QMBjK3J0MtxjPmdIMXm72Wvj2Abb0Bd4HADdfijwMdoIsyQZ6fWC7kDFhk2YinBBEMZDL7Y7wh0J1sGx3S4A== dependencies: private "^0.1.6" From ce28de893bd6fe3faaae3ac4701437b0a03f8e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 27 Feb 2019 16:41:18 +0100 Subject: [PATCH 19/66] Write a test for #27 Moderators are allowed to see disabled or deleted posts if they ask for it. --- src/middleware/softDeleteMiddleware.spec.js | 119 ++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/middleware/softDeleteMiddleware.spec.js diff --git a/src/middleware/softDeleteMiddleware.spec.js b/src/middleware/softDeleteMiddleware.spec.js new file mode 100644 index 000000000..2e5dbe054 --- /dev/null +++ b/src/middleware/softDeleteMiddleware.spec.js @@ -0,0 +1,119 @@ +import Factory from '../seed/factories' +import { host, login } from '../jest/helpers' +import { GraphQLClient } from 'graphql-request' + +const factory = Factory() +let client +let query +let action + +beforeEach(async () => { + await Promise.all([ + factory.create('User', { role: 'user', email: 'user@example.org', password: '1234' }), + factory.create('User', { role: 'moderator', email: 'moderator@example.org', password: '1234' }) + ]) + await factory.authenticateAs({email: 'user@example.org', password: '1234'}) + await Promise.all([ + factory.create('Post', { title: 'Deleted post', deleted: true, disabled: false }), + factory.create('Post', { title: 'Disabled post', deleted: false, disabled: true}), + factory.create('Post', { title: 'Publicly visible post', deleted: false, disabled: false }) + ]) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('softDeleteMiddleware', () => { + describe('Post', () => { + action = () => { + return client.request(query) + } + + beforeEach(() => { + query = '{ Post { title } }' + }) + + describe('as user', () => { + beforeEach(async () => { + const headers = await login({ email: 'user@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('hides deleted or disabled posts', async () => { + const expected = {Post: [{title: 'Publicly visible post'}]} + await expect(action()).resolves.toEqual(expected) + }) + }) + + describe('as moderator', () => { + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('hides deleted or disabled posts', async () => { + const expected = {Post: [{title: 'Publicly visible post'}]} + await expect(action()).resolves.toEqual(expected) + }) + }) + + describe('filter (deleted: true)', () => { + beforeEach(() => { + query = '{ Post(deleted: true) { title } }' + }) + + describe('as user', () => { + beforeEach(async () => { + const headers = await login({ email: 'user@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorisation error', async () => { + await expect(action()).rejects.toThrow('Not Authorised!') + }) + }) + + describe('as moderator', () => { + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('shows deleted posts', async () => { + const expected = {Post: [{title: 'Deleted post'}]} + await expect(action()).resolves.toEqual(expected) + }) + }) + }) + + describe('filter (disabled: true)', () => { + beforeEach(() => { + query = '{ Post(disabled: true) { title } }' + }) + + describe('as user', () => { + beforeEach(async () => { + const headers = await login({ email: 'user@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorisation error', async () => { + await expect(action()).rejects.toThrow('Not Authorised!') + }) + }) + + describe('as moderator', () => { + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('shows disabled posts', async () => { + const expected = {Post: [{title: 'Disabled post'}]} + await expect(action()).resolves.toEqual(expected) + }) + }) + }) + }) +}) From 738ba4f51ca4dec9e61cb2372be64d767f4f3d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 27 Feb 2019 16:48:43 +0100 Subject: [PATCH 20/66] DRY softDeleteMiddleware --- src/middleware/softDeleteMiddleware.js | 43 +++++++++----------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/src/middleware/softDeleteMiddleware.js b/src/middleware/softDeleteMiddleware.js index 79e4a7d08..abc742bb3 100644 --- a/src/middleware/softDeleteMiddleware.js +++ b/src/middleware/softDeleteMiddleware.js @@ -1,38 +1,23 @@ +const normalize = (args) => { + if (typeof args.deleted !== 'boolean') { + args.deleted = false + } + if (typeof args.disabled !== 'boolean') { + args.disabled = false + } + return args +} + export default { Query: { - Post: async (resolve, root, args, context, info) => { - if (typeof args.deleted !== 'boolean') { - args.deleted = false - } - if (typeof args.disabled !== 'boolean') { - args.disabled = false - } - const result = await resolve(root, args, context, info) - return result + Post: (resolve, root, args, context, info) => { + return resolve(root, normalize(args), context, info) }, Comment: async (resolve, root, args, context, info) => { - if (typeof args.deleted !== 'boolean') { - args.deleted = false - } - if (typeof args.disabled !== 'boolean') { - args.disabled = false - } - const result = await resolve(root, args, context, info) - return result + return resolve(root, normalize(args), context, info) }, User: async (resolve, root, args, context, info) => { - if (typeof args.deleted !== 'boolean') { - args.deleted = false - } - if (typeof args.disabled !== 'boolean') { - args.disabled = false - } - // console.log('ROOT', root) - // console.log('ARGS', args) - // console.log('CONTEXT', context) - // console.log('info', info.fieldNodes[0].arguments) - const result = await resolve(root, args, context, info) - return result + return resolve(root, normalize(args), context, info) } } } From 911500a3bd3df99e9ca2cda3cddc2cd2743ad02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 27 Feb 2019 16:49:03 +0100 Subject: [PATCH 21/66] Don't override given { deleted, disabled } = args @appinteractive I guess this was done unintentionally? --- src/middleware/dateTimeMiddleware.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/middleware/dateTimeMiddleware.js b/src/middleware/dateTimeMiddleware.js index 97e6e2767..473dbf444 100644 --- a/src/middleware/dateTimeMiddleware.js +++ b/src/middleware/dateTimeMiddleware.js @@ -2,29 +2,21 @@ export default { Mutation: { CreateUser: async (resolve, root, args, context, info) => { args.createdAt = (new Date()).toISOString() - args.disabled = false - args.deleted = false const result = await resolve(root, args, context, info) return result }, CreatePost: async (resolve, root, args, context, info) => { args.createdAt = (new Date()).toISOString() - args.disabled = false - args.deleted = false const result = await resolve(root, args, context, info) return result }, CreateComment: async (resolve, root, args, context, info) => { args.createdAt = (new Date()).toISOString() - args.disabled = false - args.deleted = false const result = await resolve(root, args, context, info) return result }, CreateOrganization: async (resolve, root, args, context, info) => { args.createdAt = (new Date()).toISOString() - args.disabled = false - args.deleted = false const result = await resolve(root, args, context, info) return result }, From f3ab671f2146b4bf597a6f4f6dda523f07048592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Thu, 28 Feb 2019 01:19:39 +0100 Subject: [PATCH 22/66] Soft delete middleware test passes --- src/middleware/permissionsMiddleware.js | 22 ++++++++++++---------- src/middleware/softDeleteMiddleware.js | 8 ++++---- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 0c6723b4b..c070e536d 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -1,4 +1,4 @@ -import { rule, shield, allow } from 'graphql-shield' +import { rule, shield, allow, or } from 'graphql-shield' /* * TODO: implement @@ -11,36 +11,38 @@ const isAuthenticated = rule()(async (parent, args, ctx, info) => { const isAdmin = rule()(async (parent, args, ctx, info) => { return ctx.user.role === 'ADMIN' }) -const isModerator = rule()(async (parent, args, ctx, info) => { - return ctx.user.role === 'MODERATOR' -}) */ +const isModerator = rule()(async (parent, args, { user }, info) => { + return user && (user.role === 'moderator' || user.role === 'admin') +}) + const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, ctx, info) => { return ctx.user.id === parent.id }) +const onlyEnabledContent = rule({ cache: 'strict' })(async (parent, args, ctx, info) => { + const { disabled, deleted } = args + return !(disabled || deleted) +}) + // Permissions const permissions = shield({ Query: { statistics: allow, - currentUser: allow - // fruits: and(isAuthenticated, or(isAdmin, isModerator)), - // customers: and(isAuthenticated, isAdmin) + currentUser: allow, + Post: or(onlyEnabledContent, isModerator) }, Mutation: { CreatePost: isAuthenticated, // TODO UpdatePost: isOwner, // TODO DeletePost: isOwner, report: isAuthenticated - // addFruitToBasket: isAuthenticated - // CreateUser: allow, }, User: { email: isMyOwn, password: isMyOwn } - // Post: isAuthenticated }) export default permissions diff --git a/src/middleware/softDeleteMiddleware.js b/src/middleware/softDeleteMiddleware.js index abc742bb3..bed7b6ca0 100644 --- a/src/middleware/softDeleteMiddleware.js +++ b/src/middleware/softDeleteMiddleware.js @@ -1,4 +1,4 @@ -const normalize = (args) => { +const setDefaults = (args) => { if (typeof args.deleted !== 'boolean') { args.deleted = false } @@ -11,13 +11,13 @@ const normalize = (args) => { export default { Query: { Post: (resolve, root, args, context, info) => { - return resolve(root, normalize(args), context, info) + return resolve(root, setDefaults(args), context, info) }, Comment: async (resolve, root, args, context, info) => { - return resolve(root, normalize(args), context, info) + return resolve(root, setDefaults(args), context, info) }, User: async (resolve, root, args, context, info) => { - return resolve(root, normalize(args), context, info) + return resolve(root, setDefaults(args), context, info) } } } From 8febf147cef2dd3eccb3b3331f47add0c02d6988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Thu, 28 Feb 2019 02:53:11 +0100 Subject: [PATCH 23/66] Fix lint --- src/middleware/softDeleteMiddleware.spec.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/middleware/softDeleteMiddleware.spec.js b/src/middleware/softDeleteMiddleware.spec.js index 2e5dbe054..925a03ccc 100644 --- a/src/middleware/softDeleteMiddleware.spec.js +++ b/src/middleware/softDeleteMiddleware.spec.js @@ -9,13 +9,13 @@ let action beforeEach(async () => { await Promise.all([ - factory.create('User', { role: 'user', email: 'user@example.org', password: '1234' }), + factory.create('User', { role: 'user', email: 'user@example.org', password: '1234' }), factory.create('User', { role: 'moderator', email: 'moderator@example.org', password: '1234' }) ]) - await factory.authenticateAs({email: 'user@example.org', password: '1234'}) + await factory.authenticateAs({ email: 'user@example.org', password: '1234' }) await Promise.all([ - factory.create('Post', { title: 'Deleted post', deleted: true, disabled: false }), - factory.create('Post', { title: 'Disabled post', deleted: false, disabled: true}), + factory.create('Post', { title: 'Deleted post', deleted: true, disabled: false }), + factory.create('Post', { title: 'Disabled post', deleted: false, disabled: true }), factory.create('Post', { title: 'Publicly visible post', deleted: false, disabled: false }) ]) }) @@ -41,7 +41,7 @@ describe('softDeleteMiddleware', () => { }) it('hides deleted or disabled posts', async () => { - const expected = {Post: [{title: 'Publicly visible post'}]} + const expected = { Post: [{ title: 'Publicly visible post' }] } await expect(action()).resolves.toEqual(expected) }) }) @@ -53,7 +53,7 @@ describe('softDeleteMiddleware', () => { }) it('hides deleted or disabled posts', async () => { - const expected = {Post: [{title: 'Publicly visible post'}]} + const expected = { Post: [{ title: 'Publicly visible post' }] } await expect(action()).resolves.toEqual(expected) }) }) @@ -81,7 +81,7 @@ describe('softDeleteMiddleware', () => { }) it('shows deleted posts', async () => { - const expected = {Post: [{title: 'Deleted post'}]} + const expected = { Post: [{ title: 'Deleted post' }] } await expect(action()).resolves.toEqual(expected) }) }) @@ -110,7 +110,7 @@ describe('softDeleteMiddleware', () => { }) it('shows disabled posts', async () => { - const expected = {Post: [{title: 'Disabled post'}]} + const expected = { Post: [{ title: 'Disabled post' }] } await expect(action()).resolves.toEqual(expected) }) }) From 5e592f666b78ffe12581639c55635426a6d4bb85 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Thu, 28 Feb 2019 15:00:45 -0300 Subject: [PATCH 24/66] Write unit test badge creation - for unauthenticated - for authenticated admin Co-authored-by: Wolfgang Huss --- src/middleware/permissionsMiddleware.js | 3 +- src/resolvers/badges.spec.js | 85 +++++++++++++++++++++++++ src/seed/factories/badges.js | 18 +++--- 3 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 src/resolvers/badges.spec.js diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 0c6723b4b..928137847 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -32,7 +32,8 @@ const permissions = shield({ CreatePost: isAuthenticated, // TODO UpdatePost: isOwner, // TODO DeletePost: isOwner, - report: isAuthenticated + report: isAuthenticated, + CreateBadge: isAuthenticated // addFruitToBasket: isAuthenticated // CreateUser: allow, }, diff --git a/src/resolvers/badges.spec.js b/src/resolvers/badges.spec.js new file mode 100644 index 000000000..abaa85995 --- /dev/null +++ b/src/resolvers/badges.spec.js @@ -0,0 +1,85 @@ +import Factory from '../seed/factories' +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../jest/helpers' + +const factory = Factory() + +describe('report', () => { + beforeEach(async () => { + await factory.create('User', { + email: 'user@example.org', + password: '1234' + }) + await factory.create('User', { + id: 'u2', + name: 'moderator', + role: 'moderator', + email: 'moderator@example.org' + }) + await factory.create('User', { + id: 'u3', + name: 'admin', + role: 'moderator', + email: 'admin@example.org' + }) + }) + + afterEach(async () => { + await factory.cleanDatabase() + }) + + const params = { + id: 'b1', + key: 'indiegogo_en_racoon', + type: 'crowdfunding', + status: 'permanent', + icon: '/img/badges/indiegogo_en_racoon.svg' + } + + describe('unauthenticated', () => { + let client + + it('throws authorization error', async () => { + client = new GraphQLClient(host) + let { id, key, type, status, icon } = params + await expect( + client.request(`mutation { + CreateBadge( + id: "${id}", + key: "${key}", + type: ${type}, + status: ${status}, + icon: "${icon}" + ) { id } + }`) + ).rejects.toThrow('Not Authorised') + }) + + describe('authenticated admin', () => { + let headers + let response + let { id, key, type, status, icon } = params + beforeEach(async () => { + headers = await login({ email: 'admin@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + response = await client.request(`mutation { + CreateBadge( + id: "${id}", + key: "${key}", + type: ${type}, + status: ${status}, + icon: "${icon}" + ) { id } + }`, + { headers } + ) + }) + it('creates a badge', () => { + let { id } = response.CreateBadge + expect(response).toEqual({ + CreateBadge: { id } + }) + }) + }) + }) +}) diff --git a/src/seed/factories/badges.js b/src/seed/factories/badges.js index b34442521..e24a67c21 100644 --- a/src/seed/factories/badges.js +++ b/src/seed/factories/badges.js @@ -10,14 +10,14 @@ export default function (params) { } = params return ` - mutation { - CreateBadge( - id: "${id}", - key: "${key}", - type: ${type}, - status: ${status}, - icon: "${icon}" - ) { id } - } + mutation { + CreateBadge( + id: "${id}", + key: "${key}", + type: ${type}, + status: ${status}, + icon: "${icon}" + ) { id } + } ` } From ec648113798b91d8bf96ffcc41c164a468b1ff2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Fri, 1 Mar 2019 13:17:07 +0100 Subject: [PATCH 25/66] Add to .gitignore `.DS_Store` --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b909223f8..81a29c8e6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ coverage.lcov .nyc_output/ public/uploads/* !.gitkeep + +# Apple macOS folder attribute file +.DS_Store \ No newline at end of file From 6271c6e8a37fd0cd4067e575aeec803d77505f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Fri, 1 Mar 2019 13:38:14 +0100 Subject: [PATCH 26/66] Add to .gitignore `.DS_Store` (#196) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b909223f8..81a29c8e6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ coverage.lcov .nyc_output/ public/uploads/* !.gitkeep + +# Apple macOS folder attribute file +.DS_Store \ No newline at end of file From 6937c60ef8de53458a8ea09e0ec311594462b217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Fri, 1 Mar 2019 15:49:11 +0100 Subject: [PATCH 27/66] Only admins are allowed to create badges --- src/middleware/permissionsMiddleware.js | 10 +++- src/resolvers/badges.spec.js | 79 ++++++++++++++++--------- 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 928137847..d2dc45094 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -16,8 +16,12 @@ const isModerator = rule()(async (parent, args, ctx, info) => { }) */ -const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, ctx, info) => { - return ctx.user.id === parent.id +const isAdmin = rule()(async (parent, args, { user }, info) => { + return user && (user.role === 'admin') +}) + +const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, context, info) => { + return context.user.id === parent.id }) // Permissions @@ -33,7 +37,7 @@ const permissions = shield({ // TODO UpdatePost: isOwner, // TODO DeletePost: isOwner, report: isAuthenticated, - CreateBadge: isAuthenticated + CreateBadge: isAdmin // addFruitToBasket: isAuthenticated // CreateUser: allow, }, diff --git a/src/resolvers/badges.spec.js b/src/resolvers/badges.spec.js index abaa85995..3574dae6d 100644 --- a/src/resolvers/badges.spec.js +++ b/src/resolvers/badges.spec.js @@ -4,22 +4,21 @@ import { host, login } from '../jest/helpers' const factory = Factory() -describe('report', () => { +describe('Badge', () => { beforeEach(async () => { await factory.create('User', { email: 'user@example.org', + role: 'user', password: '1234' }) await factory.create('User', { id: 'u2', - name: 'moderator', role: 'moderator', email: 'moderator@example.org' }) await factory.create('User', { id: 'u3', - name: 'admin', - role: 'moderator', + role: 'admin', email: 'admin@example.org' }) }) @@ -54,32 +53,56 @@ describe('report', () => { }`) ).rejects.toThrow('Not Authorised') }) + }) - describe('authenticated admin', () => { - let headers - let response - let { id, key, type, status, icon } = params - beforeEach(async () => { - headers = await login({ email: 'admin@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - response = await client.request(`mutation { - CreateBadge( - id: "${id}", - key: "${key}", - type: ${type}, - status: ${status}, - icon: "${icon}" - ) { id } - }`, - { headers } - ) - }) - it('creates a badge', () => { - let { id } = response.CreateBadge - expect(response).toEqual({ - CreateBadge: { id } - }) + describe('authenticated admin', () => { + let client + let headers + let response + let { id, key, type, status, icon } = params + beforeEach(async () => { + headers = await login({ email: 'admin@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + response = await client.request(`mutation { + CreateBadge( + id: "${id}", + key: "${key}", + type: ${type}, + status: ${status}, + icon: "${icon}" + ) { id } + }`, + { headers } + ) + }) + it('creates a badge', () => { + let { id } = response.CreateBadge + expect(response).toEqual({ + CreateBadge: { id } }) }) }) + + describe('authenticated moderator', () => { + let client + let headers + let { id, key, type, status, icon } = params + beforeEach(async () => { + headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + it('throws authorization error', async () => { + await expect(client.request(`mutation { + CreateBadge( + id: "${id}", + key: "${key}", + type: ${type}, + status: ${status}, + icon: "${icon}" + ) { id } + }`, + { headers } + )).rejects.toThrow('Not Authorised') + }) + }) }) From f25708875a8c75f36f3b88f20f7e302ee53261ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Sun, 3 Mar 2019 14:01:50 +0100 Subject: [PATCH 28/66] Refactor badges test CC @Tirokk @grenzfrequence * the top level block should correspond with the name of the resolver * the block below should be `CreatePost` or `UpdatePost` * the arguments of client.request are `query/mutation`, `variables` but you passed in the `headers` which should go into `new GraphQlClient(host, options)` * re-use the very same mutation to avoid bugs in the tests * use `await expect(someAsyncMethod).resolves.toEqual(expected)` style for extra test assurance --- src/resolvers/badges.spec.js | 128 ++++++++++++++++------------------- 1 file changed, 57 insertions(+), 71 deletions(-) diff --git a/src/resolvers/badges.spec.js b/src/resolvers/badges.spec.js index 3574dae6d..f6d8c06dc 100644 --- a/src/resolvers/badges.spec.js +++ b/src/resolvers/badges.spec.js @@ -4,7 +4,7 @@ import { host, login } from '../jest/helpers' const factory = Factory() -describe('Badge', () => { +describe('badges', () => { beforeEach(async () => { await factory.create('User', { email: 'user@example.org', @@ -27,82 +27,68 @@ describe('Badge', () => { await factory.cleanDatabase() }) - const params = { - id: 'b1', - key: 'indiegogo_en_racoon', - type: 'crowdfunding', - status: 'permanent', - icon: '/img/badges/indiegogo_en_racoon.svg' - } + describe('CreateBadge', () => { + const params = { + id: 'b1', + key: 'indiegogo_en_racoon', + type: 'crowdfunding', + status: 'permanent', + icon: '/img/badges/indiegogo_en_racoon.svg' + } - describe('unauthenticated', () => { - let client + const mutation = ` + mutation( + $id: ID + $key: String! + $type: BadgeTypeEnum! + $status: BadgeStatusEnum! + $icon: String! + ) { + CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) { + id + } + } + ` - it('throws authorization error', async () => { - client = new GraphQLClient(host) - let { id, key, type, status, icon } = params - await expect( - client.request(`mutation { - CreateBadge( - id: "${id}", - key: "${key}", - type: ${type}, - status: ${status}, - icon: "${icon}" - ) { id } - }`) - ).rejects.toThrow('Not Authorised') + describe('unauthenticated', () => { + let client + + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect( + client.request(mutation, params) + ).rejects.toThrow('Not Authorised') + }) }) - }) - describe('authenticated admin', () => { - let client - let headers - let response - let { id, key, type, status, icon } = params - beforeEach(async () => { - headers = await login({ email: 'admin@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - response = await client.request(`mutation { - CreateBadge( - id: "${id}", - key: "${key}", - type: ${type}, - status: ${status}, - icon: "${icon}" - ) { id } - }`, - { headers } - ) + describe('authenticated admin', () => { + let client + beforeEach(async () => { + const headers = await login({ email: 'admin@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + it('creates a badge', async () => { + const expected = { + CreateBadge: { + id: 'b1' + } + } + await expect(client.request(mutation, params)).resolves.toEqual(expected) + }) }) - it('creates a badge', () => { - let { id } = response.CreateBadge - expect(response).toEqual({ - CreateBadge: { id } + + describe('authenticated moderator', () => { + let client + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorization error', async () => { + await expect( + client.request(mutation, params) + ).rejects.toThrow('Not Authorised') }) }) }) - - describe('authenticated moderator', () => { - let client - let headers - let { id, key, type, status, icon } = params - beforeEach(async () => { - headers = await login({ email: 'moderator@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - it('throws authorization error', async () => { - await expect(client.request(`mutation { - CreateBadge( - id: "${id}", - key: "${key}", - type: ${type}, - status: ${status}, - icon: "${icon}" - ) { id } - }`, - { headers } - )).rejects.toThrow('Not Authorised') - }) - }) }) From fb2b407be029f8e08cfaaf21be72cc6c088aff4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Sun, 3 Mar 2019 14:32:29 +0100 Subject: [PATCH 29/66] Extend @Tirokk 's test to Create and Update --- src/resolvers/badges.spec.js | 141 +++++++++++++++++++++++++++++++++-- 1 file changed, 135 insertions(+), 6 deletions(-) diff --git a/src/resolvers/badges.spec.js b/src/resolvers/badges.spec.js index f6d8c06dc..e38f54381 100644 --- a/src/resolvers/badges.spec.js +++ b/src/resolvers/badges.spec.js @@ -28,7 +28,7 @@ describe('badges', () => { }) describe('CreateBadge', () => { - const params = { + const variables = { id: 'b1', key: 'indiegogo_en_racoon', type: 'crowdfunding', @@ -45,7 +45,11 @@ describe('badges', () => { $icon: String! ) { CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) { - id + id, + key, + type, + status, + icon } } ` @@ -56,7 +60,7 @@ describe('badges', () => { it('throws authorization error', async () => { client = new GraphQLClient(host) await expect( - client.request(mutation, params) + client.request(mutation, variables) ).rejects.toThrow('Not Authorised') }) }) @@ -70,10 +74,14 @@ describe('badges', () => { it('creates a badge', async () => { const expected = { CreateBadge: { - id: 'b1' + icon: '/img/badges/indiegogo_en_racoon.svg', + id: 'b1', + key: 'indiegogo_en_racoon', + status: 'permanent', + type: 'crowdfunding' } } - await expect(client.request(mutation, params)).resolves.toEqual(expected) + await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) }) @@ -86,9 +94,130 @@ describe('badges', () => { it('throws authorization error', async () => { await expect( - client.request(mutation, params) + client.request(mutation, variables) ).rejects.toThrow('Not Authorised') }) }) }) + + describe('UpdateBadge', () => { + beforeEach(async () => { + await factory.authenticateAs({ email: 'admin@example.org', password: '1234' }) + await factory.create('Badge', { id: 'b1' }) + }) + const variables = { + id: 'b1', + key: 'whatever' + } + + const mutation = ` + mutation($id: ID!, $key: String!) { + UpdateBadge(id: $id, key: $key) { + id + key + } + } + ` + + describe('unauthenticated', () => { + let client + + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect( + client.request(mutation, variables) + ).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated moderator', () => { + let client + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorization error', async () => { + await expect( + client.request(mutation, variables) + ).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated admin', () => { + let client + beforeEach(async () => { + const headers = await login({ email: 'admin@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + it('updates a badge', async () => { + const expected = { + UpdateBadge: { + id: 'b1', + key: 'whatever' + } + } + await expect(client.request(mutation, variables)).resolves.toEqual(expected) + }) + }) + }) + + describe('DeleteBadge', () => { + beforeEach(async () => { + await factory.authenticateAs({ email: 'admin@example.org', password: '1234' }) + await factory.create('Badge', { id: 'b1' }) + }) + const variables = { + id: 'b1' + } + + const mutation = ` + mutation($id: ID!) { + DeleteBadge(id: $id) { + id + } + } + ` + + describe('unauthenticated', () => { + let client + + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect( + client.request(mutation, variables) + ).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated moderator', () => { + let client + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorization error', async () => { + await expect( + client.request(mutation, variables) + ).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated admin', () => { + let client + beforeEach(async () => { + const headers = await login({ email: 'admin@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + it('deletes a badge', async () => { + const expected = { + DeleteBadge: { + id: 'b1' + } + } + await expect(client.request(mutation, variables)).resolves.toEqual(expected) + }) + }) + }) }) From 8d1eb6026a8b34ad82c22f6ab8d0c29dae8b4c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Sun, 3 Mar 2019 14:35:08 +0100 Subject: [PATCH 30/66] Let all tests pass :green_heart: --- src/middleware/permissionsMiddleware.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index d2dc45094..f10b30cb6 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -37,7 +37,9 @@ const permissions = shield({ // TODO UpdatePost: isOwner, // TODO DeletePost: isOwner, report: isAuthenticated, - CreateBadge: isAdmin + CreateBadge: isAdmin, + UpdateBadge: isAdmin, + DeleteBadge: isAdmin // addFruitToBasket: isAuthenticated // CreateUser: allow, }, From 83b36e8e351a95f5be9276c18579e574c4e8edfb Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Sun, 3 Mar 2019 15:55:11 +0100 Subject: [PATCH 31/66] Output distinct counts --- src/schema.graphql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/schema.graphql b/src/schema.graphql index 1f9bcb477..20a0b3652 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -84,13 +84,13 @@ type User { updatedAt: String friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") - friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(r)") + friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)") following: [User]! @relation(name: "FOLLOWS", direction: "OUT") - followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(r)") + followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)") followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") - followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(r)") + followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)") #contributions: [WrittenPost]! #contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]! @@ -109,7 +109,7 @@ type User { commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) WHERE NOT r.deleted = true RETURN COUNT(r)") shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT") - shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)") + shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT") organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT") @@ -149,7 +149,7 @@ type Post { commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) RETURN COUNT(r)") shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN") - shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)") + shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") } type Comment { @@ -223,7 +223,7 @@ type Tag { name: String! taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN") taggedOrganizations: [Organization]! @relation(name: "TAGGED", direction: "IN") - taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(p)") + taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(DISTINCT p)") taggedCountUnique: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)") deleted: Boolean disabled: Boolean From 257183ac3f4e702f56063ebea206d7fb7795d84d Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Sun, 3 Mar 2019 19:29:25 +0100 Subject: [PATCH 32/66] added currentUserId to cypher params --- package.json | 2 +- src/server.js | 5 ++++- yarn.lock | 52 ++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 570841630..2d6696164 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "lodash": "~4.17.11", "ms": "~2.1.1", "neo4j-driver": "~1.7.2", - "neo4j-graphql-js": "~2.3.1", + "neo4j-graphql-js": "~2.4.1", "node-fetch": "~2.3.0", "npm-run-all": "~4.1.5", "sanitize-html": "~1.20.0", diff --git a/src/server.js b/src/server.js index 2fc3af871..8e1cd36da 100644 --- a/src/server.js +++ b/src/server.js @@ -45,7 +45,10 @@ const createServer = (options) => { return { driver, user, - req: request + req: request, + cypherParams: { + currentUserId: user ? user.id : null + } } }, schema: schema, diff --git a/yarn.lock b/yarn.lock index fd3c53297..5e59726cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1092,6 +1092,14 @@ apollo-env@0.3.3: core-js "3.0.0-beta.13" node-fetch "^2.2.0" +apollo-errors@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/apollo-errors/-/apollo-errors-1.9.0.tgz#f1ed0ca0a6be5cd2f24e2eaa7b0860a10146ff51" + integrity sha512-XVukHd0KLvgY6tNjsPS3/Re3U6RQlTKrTbIpqqeTMo2N34uQMr+H1UheV21o8hOZBAFosvBORVricJiP5vfmrw== + dependencies: + assert "^1.4.1" + extendable-error "^0.1.5" + apollo-graphql@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.1.1.tgz#dc5eac3062abf9f063ac9869f0ef5c54fdc136e5" @@ -1378,6 +1386,13 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= +assert@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + integrity sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE= + dependencies: + util "0.10.3" + assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" @@ -2800,6 +2815,11 @@ extend@^3.0.0, extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +extendable-error@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/extendable-error/-/extendable-error-0.1.5.tgz#122308a7097bc89a263b2c4fbf089c78140e3b6d" + integrity sha1-EiMIpwl7yJomOyxPvwiceBQOO20= + external-editor@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" @@ -3155,6 +3175,15 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== +graphql-auth-directives@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/graphql-auth-directives/-/graphql-auth-directives-2.1.0.tgz#85b83817844e2ec5fba8fe5de444287d6dd0f85a" + integrity sha512-mRVsjeMeMABPyjxyzl9mhkcW02YBwSj7dnu7C6wy2dIhiby6xTKy6Q54C8KeqXSYsy6ua4VmBH++d7GKqpvIoA== + dependencies: + apollo-errors "^1.9.0" + graphql-tools "^4.0.4" + jsonwebtoken "^8.3.0" + graphql-custom-directives@~0.2.14: version "0.2.14" resolved "https://registry.yarnpkg.com/graphql-custom-directives/-/graphql-custom-directives-0.2.14.tgz#88611b8cb074477020ad85af47bfe168c4c23992" @@ -3588,6 +3617,11 @@ inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, i resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= + ini@^1.3.4, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" @@ -4432,7 +4466,7 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= -jsonwebtoken@~8.5.0: +jsonwebtoken@^8.3.0, jsonwebtoken@~8.5.0: version "8.5.0" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#ebd0ca2a69797816e1c5af65b6c759787252947e" integrity sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA== @@ -4941,12 +4975,13 @@ neo4j-driver@^1.7.2, neo4j-driver@~1.7.2: text-encoding "^0.6.4" uri-js "^4.2.1" -neo4j-graphql-js@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.3.1.tgz#9a5de7e312594d63481e947a0cbe4e08b05ffed3" - integrity sha512-9DExWXD2vFdDJOmqorT1ygFOUEos7KF8KyF8Wt3jYxejWUuq+a5fAFBu7+YDH8QbvA23paKPEX0Pn1nS+Q5C1A== +neo4j-graphql-js@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.4.1.tgz#a1de65340fb6f1ad0ae6aadab90a0bb78b490b32" + integrity sha512-Py6RJuMT7A03Hzoo6qfKyo6DUnHQgbZlBcgucnhgQjbeysAzvM3F02UAVn/HxEtOgowAeGWjyjJvwozoNtiiqQ== dependencies: graphql "^14.0.2" + graphql-auth-directives "^2.0.0" lodash "^4.17.11" neo4j-driver "^1.7.2" @@ -6962,6 +6997,13 @@ util.promisify@^1.0.0: define-properties "^1.1.2" object.getownpropertydescriptors "^2.0.3" +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= + dependencies: + inherits "2.0.1" + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" From f39e27c47c2a90fe72a0edd287d93f34d54d6de3 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Sun, 3 Mar 2019 19:29:40 +0100 Subject: [PATCH 33/66] Added followedByCurrentUser and shoutedByCurrentUser --- src/schema.graphql | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/schema.graphql b/src/schema.graphql index 20a0b3652..dd2bfde59 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -92,6 +92,12 @@ type User { followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)") + "Is the currently logged in user following that user?" + followedByCurrentUser: Boolean! @cypher(statement: """ + MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) + RETURN COUNT(u) >= 1 + """) + #contributions: [WrittenPost]! #contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]! # @cypher( @@ -150,6 +156,12 @@ type Post { shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN") shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") + + "Has the currently logged in user shouted that post?" + shoutedByCurrentUser: Boolean! @cypher(statement: """ + MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) + RETURN COUNT(u) >= 1 + """) } type Comment { From b23380d5938369f2f6cead131e2b6dc7519e1ecf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 4 Mar 2019 04:21:10 +0000 Subject: [PATCH 34/66] Bump eslint from 5.14.1 to 5.15.0 Bumps [eslint](https://github.com/eslint/eslint) from 5.14.1 to 5.15.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v5.14.1...v5.15.0) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 570841630..cefa46953 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "babel-eslint": "~10.0.1", "babel-jest": "~24.1.0", "chai": "~4.2.0", - "eslint": "~5.14.1", + "eslint": "~5.15.0", "eslint-config-standard": "~12.0.0", "eslint-plugin-import": "~2.16.0", "eslint-plugin-jest": "~22.3.0", diff --git a/yarn.lock b/yarn.lock index fd3c53297..eb0f3efd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2564,10 +2564,10 @@ eslint-scope@3.7.1: esrecurse "^4.1.0" estraverse "^4.1.1" -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.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.2.tgz#5f10cd6cabb1965bf479fa65745673439e21cb0e" + integrity sha512-5q1+B/ogmHl8+paxtOKx38Z8LtWkVGuNt3+GQNErqwLl6ViNp/gdJGMCjZNxZ8j/VYjDNZ2Fo+eQc1TAVPIzbg== dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" @@ -2582,10 +2582,10 @@ 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.14.1: - version "5.14.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.14.1.tgz#490a28906be313685c55ccd43a39e8d22efc04ba" - integrity sha512-CyUMbmsjxedx8B0mr79mNOqetvkbij/zrXnFeK2zc3pGRn3/tibjiNAv/3UxFEyfMDjh+ZqTrJrEGBFiGfD5Og== +eslint@~5.15.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.15.0.tgz#f313a2f7c7628d39adeefdba4a9c41f842012c9e" + integrity sha512-xwG7SS5JLeqkiR3iOmVgtF8Y6xPdtr6AAsN6ph7Q6R/fv+3UlKYoika8SmNzmb35qdRF+RfTY35kMEdtbi+9wg== dependencies: "@babel/code-frame" "^7.0.0" ajv "^6.9.1" @@ -2593,7 +2593,7 @@ eslint@~5.14.1: cross-spawn "^6.0.5" debug "^4.0.1" doctrine "^3.0.0" - eslint-scope "^4.0.0" + eslint-scope "^4.0.2" eslint-utils "^1.3.1" eslint-visitor-keys "^1.0.0" espree "^5.0.1" From b2520258a3c7454fdab8facdb261f4913e60b9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 4 Mar 2019 18:35:02 +0100 Subject: [PATCH 35/66] Improve specification of posts resolver * only authors are alllowed to update/delete their own posts * set disabled+deleted to false if not provided --- src/resolvers/posts.spec.js | 195 +++++++++++++++++++++++++++++++----- 1 file changed, 168 insertions(+), 27 deletions(-) diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index a6c1d7e3e..5603683eb 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -3,6 +3,7 @@ import { GraphQLClient } from 'graphql-request' import { host, login } from '../jest/helpers' const factory = Factory() +let client beforeEach(async () => { await factory.create('User', { @@ -16,46 +17,186 @@ afterEach(async () => { }) describe('CreatePost', () => { + const mutation = ` + mutation { + CreatePost(title: "I am a title", content: "Some content") { + title + content + slug + disabled + deleted + } + } + ` + describe('unauthenticated', () => { - let client it('throws authorization error', async () => { client = new GraphQLClient(host) - await expect(client.request(`mutation { - CreatePost( - title: "I am a post", - content: "Some content" - ) { slug } - }`)).rejects.toThrow('Not Authorised') + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated', () => { + let headers + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) }) - describe('authenticated', () => { - let headers - let response - beforeEach(async () => { - headers = await login({ email: 'test@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - response = await client.request(`mutation { - CreatePost( - title: "A title", - content: "Some content" - ) { title, content } - }`, { headers }) - }) + it('creates a post', async () => { + const expected = { + CreatePost: { + title: 'I am a title', + content: 'Some content' + } + } + await expect(client.request(mutation)).resolves.toMatchObject(expected) + }) - it('creates a post', () => { - expect(response).toEqual({ CreatePost: { title: 'A title', content: 'Some content' } }) - }) - - it('assigns the authenticated user as author', async () => { - const { User } = await client.request(`{ + it('assigns the authenticated user as author', async () => { + await client.request(mutation) + const { User } = await client.request(`{ User(email:"test@example.org") { contributions { title } } }`, { headers }) - expect(User).toEqual([ { contributions: [ { title: 'A title' } ] } ]) + expect(User).toEqual([ { contributions: [ { title: 'I am a title' } ] } ]) + }) + + describe('disabled and deleted', () => { + it('initially false', async () => { + const expected = { CreatePost: { disabled: false, deleted: false } } + await expect(client.request(mutation)).resolves.toMatchObject(expected) }) }) }) }) + +describe('UpdatePost', () => { + const mutation = ` + mutation($id: ID!, $content: String) { + UpdatePost(id: $id, content: $content) { + id + content + } + } + ` + + let variables = { + id: 'p1', + content: 'New content' + } + + beforeEach(async () => { + const asAuthor = Factory() + await asAuthor.create('User', { + email: 'author@example.org', + password: '1234' + }) + await asAuthor.authenticateAs({ + email: 'author@example.org', + password: '1234' + }) + await asAuthor.create('Post', { + id: 'p1', + content: 'Old content' + }) + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated but not the author', () => { + let headers + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorization error', async () => { + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated as author', () => { + let headers + beforeEach(async () => { + headers = await login({ email: 'author@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('updates a post', async () => { + const expected = { UpdatePost: { id: 'p1', content: 'New content' } } + await expect(client.request(mutation, variables)).resolves.toEqual(expected) + }) + }) +}) + +describe('DeletePost', () => { + const mutation = ` + mutation($id: ID!) { + DeletePost(id: $id) { + id + content + } + } + ` + + let variables = { + id: 'p1' + } + + beforeEach(async () => { + const asAuthor = Factory() + await asAuthor.create('User', { + email: 'author@example.org', + password: '1234' + }) + await asAuthor.authenticateAs({ + email: 'author@example.org', + password: '1234' + }) + await asAuthor.create('Post', { + id: 'p1', + content: 'To be deleted' + }) + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated but not the author', () => { + let headers + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorization error', async () => { + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated as author', () => { + let headers + beforeEach(async () => { + headers = await login({ email: 'author@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('deletes a post', async () => { + const expected = { DeletePost: { id: 'p1', content: 'To be deleted' } } + await expect(client.request(mutation, variables)).resolves.toEqual(expected) + }) + }) +}) From c869724d29784ed9af282c4a590ddd7b231dacc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 4 Mar 2019 18:36:56 +0100 Subject: [PATCH 36/66] Let all tests pass :green_heart: --- src/middleware/permissionsMiddleware.js | 2 -- src/middleware/softDeleteMiddleware.js | 3 +++ src/resolvers/posts.js | 33 +++++++++++++++++++++---- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 5515d5b7a..b755f3bab 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -34,8 +34,6 @@ const permissions = shield({ }, Mutation: { CreatePost: isAuthenticated, - // TODO UpdatePost: isOwner, - // TODO DeletePost: isOwner, report: isAuthenticated, CreateBadge: isAdmin, UpdateBadge: isAdmin, diff --git a/src/middleware/softDeleteMiddleware.js b/src/middleware/softDeleteMiddleware.js index bed7b6ca0..0c12e7a72 100644 --- a/src/middleware/softDeleteMiddleware.js +++ b/src/middleware/softDeleteMiddleware.js @@ -19,5 +19,8 @@ export default { User: async (resolve, root, args, context, info) => { return resolve(root, setDefaults(args), context, info) } + }, + Mutation: async (resolve, root, args, context, info) => { + return resolve(root, setDefaults(args), context, info) } } diff --git a/src/resolvers/posts.js b/src/resolvers/posts.js index 6a8a0c25f..f59050b5f 100644 --- a/src/resolvers/posts.js +++ b/src/resolvers/posts.js @@ -1,22 +1,45 @@ import { neo4jgraphql } from 'neo4j-graphql-js' +const isAuthor = async (params, { user, driver }) => { + if (!user) return false + const session = driver.session() + const { id: postId } = params + const result = await session.run(` + MATCH (post:Post {id: $postId})<-[:WROTE]-(author) + RETURN author + `, { postId }) + const [author] = result.records.map((record) => { + return record.get('author') + }) + const { properties: { id: authorId } } = author + session.close() + return authorId === user.id +} + export default { Mutation: { - CreatePost: async (object, params, ctx, resolveInfo) => { - const result = await neo4jgraphql(object, params, ctx, resolveInfo, false) + CreatePost: async (object, params, context, resolveInfo) => { + const result = await neo4jgraphql(object, params, context, resolveInfo, false) - const session = ctx.driver.session() + const session = context.driver.session() await session.run( 'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' + 'MERGE (post)<-[:WROTE]-(author) ' + 'RETURN author', { - userId: ctx.user.id, + userId: context.user.id, postId: result.id }) session.close() return result + }, + UpdatePost: async (object, params, context, resolveInfo) => { + if (!await isAuthor(params, context)) return Error('Not Authorised!') + return neo4jgraphql(object, params, context, resolveInfo, false) + }, + DeletePost: async (object, params, context, resolveInfo) => { + if (!await isAuthor(params, context)) return Error('Not Authorised!') + return neo4jgraphql(object, params, context, resolveInfo, false) } - } } From b64ea750115670aeead8441ba25f8b770eae0611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 4 Mar 2019 18:38:36 +0100 Subject: [PATCH 37/66] Add a deleted post and a disabled post to seeds --- src/seed/seed-db.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/seed/seed-db.js b/src/seed/seed-db.js index b2ee8fbdb..ed46a5716 100644 --- a/src/seed/seed-db.js +++ b/src/seed/seed-db.js @@ -82,12 +82,12 @@ import Factory from './factories' await Promise.all([ asAdmin.create('Post', { id: 'p0' }), asModerator.create('Post', { id: 'p1' }), - asUser.create('Post', { id: 'p2' }), + asUser.create('Post', { id: 'p2', deleted: true }), asTick.create('Post', { id: 'p3' }), asTrick.create('Post', { id: 'p4' }), asTrack.create('Post', { id: 'p5' }), asAdmin.create('Post', { id: 'p6' }), - asModerator.create('Post', { id: 'p7' }), + asModerator.create('Post', { id: 'p7', disabled: true }), asUser.create('Post', { id: 'p8' }), asTick.create('Post', { id: 'p9' }), asTrick.create('Post', { id: 'p10' }), From 180491c08caef423c48ee413faed7b6bb9fbd3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 4 Mar 2019 19:40:49 +0100 Subject: [PATCH 38/66] Put `isAuthor` in permissions middleware I find it dirty to access the database in a middleware, ie. I would like to put all access on the database as close to the resolver as possible. However, in this case that would mean to put the authorization check in the resolver, where nobody expects it to be. CC @appinteractive --- src/middleware/permissionsMiddleware.js | 18 ++++++++++++++++++ src/resolvers/posts.js | 24 ------------------------ 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index b755f3bab..c40803e00 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -25,6 +25,22 @@ const onlyEnabledContent = rule({ cache: 'strict' })(async (parent, args, ctx, i return !(disabled || deleted) }) +const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) => { + if (!user) return false + const session = driver.session() + const { id: postId } = args + const result = await session.run(` + MATCH (post:Post {id: $postId})<-[:WROTE]-(author) + RETURN author + `, { postId }) + const [author] = result.records.map((record) => { + return record.get('author') + }) + const { properties: { id: authorId } } = author + session.close() + return authorId === user.id +}) + // Permissions const permissions = shield({ Query: { @@ -34,6 +50,8 @@ const permissions = shield({ }, Mutation: { CreatePost: isAuthenticated, + UpdatePost: isAuthor, + DeletePost: isAuthor, report: isAuthenticated, CreateBadge: isAdmin, UpdateBadge: isAdmin, diff --git a/src/resolvers/posts.js b/src/resolvers/posts.js index f59050b5f..abf91e047 100644 --- a/src/resolvers/posts.js +++ b/src/resolvers/posts.js @@ -1,21 +1,5 @@ import { neo4jgraphql } from 'neo4j-graphql-js' -const isAuthor = async (params, { user, driver }) => { - if (!user) return false - const session = driver.session() - const { id: postId } = params - const result = await session.run(` - MATCH (post:Post {id: $postId})<-[:WROTE]-(author) - RETURN author - `, { postId }) - const [author] = result.records.map((record) => { - return record.get('author') - }) - const { properties: { id: authorId } } = author - session.close() - return authorId === user.id -} - export default { Mutation: { CreatePost: async (object, params, context, resolveInfo) => { @@ -32,14 +16,6 @@ export default { session.close() return result - }, - UpdatePost: async (object, params, context, resolveInfo) => { - if (!await isAuthor(params, context)) return Error('Not Authorised!') - return neo4jgraphql(object, params, context, resolveInfo, false) - }, - DeletePost: async (object, params, context, resolveInfo) => { - if (!await isAuthor(params, context)) return Error('Not Authorised!') - return neo4jgraphql(object, params, context, resolveInfo, false) } } } From 0db91530fea70010d1f90983f743f0c71d20e940 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Tue, 5 Mar 2019 04:18:26 +0000 Subject: [PATCH 39/66] Bump eslint from 5.15.0 to 5.15.1 Bumps [eslint](https://github.com/eslint/eslint) from 5.15.0 to 5.15.1. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v5.15.0...v5.15.1) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index cefa46953..1c6e8f2ce 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "babel-eslint": "~10.0.1", "babel-jest": "~24.1.0", "chai": "~4.2.0", - "eslint": "~5.15.0", + "eslint": "~5.15.1", "eslint-config-standard": "~12.0.0", "eslint-plugin-import": "~2.16.0", "eslint-plugin-jest": "~22.3.0", diff --git a/yarn.lock b/yarn.lock index eb0f3efd9..18d336c7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2582,10 +2582,10 @@ 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.15.0: - version "5.15.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.15.0.tgz#f313a2f7c7628d39adeefdba4a9c41f842012c9e" - integrity sha512-xwG7SS5JLeqkiR3iOmVgtF8Y6xPdtr6AAsN6ph7Q6R/fv+3UlKYoika8SmNzmb35qdRF+RfTY35kMEdtbi+9wg== +eslint@~5.15.1: + version "5.15.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.15.1.tgz#8266b089fd5391e0009a047050795b1d73664524" + integrity sha512-NTcm6vQ+PTgN3UBsALw5BMhgO6i5EpIjQF/Xb5tIh3sk9QhrFafujUOczGz4J24JBlzWclSB9Vmx8d+9Z6bFCg== dependencies: "@babel/code-frame" "^7.0.0" ajv "^6.9.1" From e2add5a730ceafa6dff84ecce41db488549dcfe0 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Tue, 5 Mar 2019 10:56:47 +0100 Subject: [PATCH 40/66] Added (un)shout and (un)follow mutations --- src/schema.graphql | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/schema.graphql b/src/schema.graphql index dd2bfde59..e5a077f2d 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -1,12 +1,45 @@ type Query { isLoggedIn: Boolean! + "Get the currently logged in User based on the given JWT Token" currentUser: User + "Get the latest Network Statistics" statistics: Statistics! } type Mutation { + "Get a JWT Token for the given Email and password" login(email: String!, password: String!): String! signup(email: String!, password: String!): Boolean! report(resource: Resource!, description: String): Report + + "Shout the given Type and ID" + shout(id: ID!, type: ShoutTypeEnum): String! @cypher(statement: """ + MATCH (n {id: $id}) + WHERE $type IN labels(n) + MERGE (:User {id: $cypherParams.currentUserId})-[r:SHOUTED]->(n) + RETURN COUNT(r) > 0 + """) + "Unshout the given Type and ID" + unshout(id: ID!, type: ShoutTypeEnum): Boolean! @cypher(statement: """ + MATCH (:User {id: $cypherParams.currentUserId})-[r:SHOUTED]->(n {id: $id}) + WHERE $type IN labels(n) + DELETE r + RETURN COUNT(r) > 0 + """) + + "Follow the given Type and ID" + follow(id: ID!, type: FollowTypeEnum): Boolean! @cypher(statement: """ + MATCH (n {id: $id}) + WHERE $type IN labels(n) AND NOT $id = $cypherParams.currentUserId + MERGE (:User {id: $cypherParams.currentUserId})-[r:FOLLOWS]->(n) + RETURN COUNT(r) > 0 + """) + "Unfollow the given Type and ID" + unfollow(id: ID!, type: FollowTypeEnum): Boolean! @cypher(statement: """ + MATCH (:User {id: $cypherParams.currentUserId})-[r:FOLLOWS]->(n {id: $id}) + WHERE $type IN labels(n) + DELETE r + RETURN COUNT(r) > 0 + """) } type Statistics { @@ -214,6 +247,16 @@ enum BadgeStatusEnum { permanent temporary } +enum ShoutTypeEnum { + Post + Organization + Project +} +enum FollowTypeEnum { + User + Organization + Project +} type Organization { id: ID! From d7b1ea88c4dddd9de6df7cefdfb68e0ff2418038 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Tue, 5 Mar 2019 11:18:59 +0100 Subject: [PATCH 41/66] Fixed shouts and follows --- src/schema.graphql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/schema.graphql b/src/schema.graphql index e5a077f2d..f95bbb18a 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -13,9 +13,9 @@ type Mutation { "Shout the given Type and ID" shout(id: ID!, type: ShoutTypeEnum): String! @cypher(statement: """ - MATCH (n {id: $id}) + MATCH (n {id: $id}), (u:User {id: $cypherParams.currentUserId}) WHERE $type IN labels(n) - MERGE (:User {id: $cypherParams.currentUserId})-[r:SHOUTED]->(n) + MERGE (u)-[r:SHOUTED]->(n) RETURN COUNT(r) > 0 """) "Unshout the given Type and ID" @@ -28,9 +28,9 @@ type Mutation { "Follow the given Type and ID" follow(id: ID!, type: FollowTypeEnum): Boolean! @cypher(statement: """ - MATCH (n {id: $id}) + MATCH (n {id: $id}), (u:User {id: $cypherParams.currentUserId}) WHERE $type IN labels(n) AND NOT $id = $cypherParams.currentUserId - MERGE (:User {id: $cypherParams.currentUserId})-[r:FOLLOWS]->(n) + MERGE (u)-[r:FOLLOWS]->(n) RETURN COUNT(r) > 0 """) "Unfollow the given Type and ID" From 45a004662f1fad9d206fc3ddf4fd7b5c3b0c614f Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Tue, 5 Mar 2019 14:18:22 +0100 Subject: [PATCH 42/66] Disable shouting of own content --- src/schema.graphql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/schema.graphql b/src/schema.graphql index f95bbb18a..e6e2088f1 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -13,8 +13,8 @@ type Mutation { "Shout the given Type and ID" shout(id: ID!, type: ShoutTypeEnum): String! @cypher(statement: """ - MATCH (n {id: $id}), (u:User {id: $cypherParams.currentUserId}) - WHERE $type IN labels(n) + MATCH (n {id: $id})<-[:WROTE]-(wu:User), (u:User {id: $cypherParams.currentUserId}) + WHERE $type IN labels(n) AND NOT wu.id = $cypherParams.currentUserId MERGE (u)-[r:SHOUTED]->(n) RETURN COUNT(r) > 0 """) From f644507e4fc496a4e4ba0f6d2948d56d8a483bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 15:47:03 +0100 Subject: [PATCH 43/66] Intermediate commit --- src/middleware/softDeleteMiddleware.spec.js | 11 +++++++---- src/seed/factories/posts.js | 2 -- src/seed/seed-db.js | 7 ++++++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/middleware/softDeleteMiddleware.spec.js b/src/middleware/softDeleteMiddleware.spec.js index 925a03ccc..c37e7d426 100644 --- a/src/middleware/softDeleteMiddleware.spec.js +++ b/src/middleware/softDeleteMiddleware.spec.js @@ -10,14 +10,17 @@ let action beforeEach(async () => { await Promise.all([ factory.create('User', { role: 'user', email: 'user@example.org', password: '1234' }), - factory.create('User', { role: 'moderator', email: 'moderator@example.org', password: '1234' }) + factory.create('User', { id: 'm1', role: 'moderator', email: 'moderator@example.org', password: '1234' }) ]) await factory.authenticateAs({ email: 'user@example.org', password: '1234' }) await Promise.all([ - factory.create('Post', { title: 'Deleted post', deleted: true, disabled: false }), - factory.create('Post', { title: 'Disabled post', deleted: false, disabled: true }), - factory.create('Post', { title: 'Publicly visible post', deleted: false, disabled: false }) + factory.create('Post', { title: 'Deleted post', deleted: true }), + factory.create('Post', { id: 'p2', title: 'Disabled post', deleted: false }), + factory.create('Post', { title: 'Publicly visible post', deleted: false }) ]) + const moderatorFactory = Factory() + await moderatorFactory.authenticateAs({ email: 'moderator@example.org', password: '1234'}) + await moderatorFactory.relate('Post', 'DisabledBy', { from: 'm1', to: 'p2'}) }) afterEach(async () => { diff --git a/src/seed/factories/posts.js b/src/seed/factories/posts.js index d96cf4f73..e2bc2ab66 100644 --- a/src/seed/factories/posts.js +++ b/src/seed/factories/posts.js @@ -14,7 +14,6 @@ export default function (params) { ].join('. '), image = faker.image.image(), visibility = 'public', - disabled = false, deleted = false } = params @@ -26,7 +25,6 @@ export default function (params) { content: "${content}", image: "${image}", visibility: ${visibility}, - disabled: ${disabled}, deleted: ${deleted} ) { title, content } } diff --git a/src/seed/seed-db.js b/src/seed/seed-db.js index ed46a5716..c20d524ef 100644 --- a/src/seed/seed-db.js +++ b/src/seed/seed-db.js @@ -87,7 +87,7 @@ import Factory from './factories' asTrick.create('Post', { id: 'p4' }), asTrack.create('Post', { id: 'p5' }), asAdmin.create('Post', { id: 'p6' }), - asModerator.create('Post', { id: 'p7', disabled: true }), + asModerator.create('Post', { id: 'p7' }), asUser.create('Post', { id: 'p8' }), asTick.create('Post', { id: 'p9' }), asTrick.create('Post', { id: 'p10' }), @@ -98,6 +98,11 @@ import Factory from './factories' asTick.create('Post', { id: 'p15' }) ]) + await asModerator.relate('Post', 'DisabledBy', { + from: 'u2', + to: 'p15' + }) + await Promise.all([ f.relate('Post', 'Categories', { from: 'p0', to: 'cat16' }), f.relate('Post', 'Categories', { from: 'p1', to: 'cat1' }), From 420ea8a4d60747b77e0c0da8b0c70f1dfb28a9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 16:15:31 +0100 Subject: [PATCH 44/66] Scaffold some tests for disabledBy relation --- src/resolvers/posts.spec.js | 46 +++++++++++++++++++++++++++++++++++++ src/schema.graphql | 1 + 2 files changed, 47 insertions(+) diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index 5603683eb..1601e3348 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -200,3 +200,49 @@ describe('DeletePost', () => { }) }) }) + +describe('AddPostDisabledBy', () => { + const mutation = ` + mutation { + AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { + from { + id + } + to { + id + } + } + } + ` + it.todo('throws authorization error') + + describe('authenticated', () => { + it.todo('throws authorization error') + + describe('as moderator', () => { + it.todo('throws authorization error') + + describe('current user matches provided user', () => { + it.todo('sets current user') + it.todo('updates .disabled on post') + }) + }) + }) +}) + +describe('RemovePostDisabledBy', () => { + it.todo('throws authorization error') + + describe('authenticated', () => { + it.todo('throws authorization error') + + describe('as moderator', () => { + it.todo('throws authorization error') + + describe('current user matches provided user', () => { + it.todo('sets current user') + it.todo('updates .disabled on post') + }) + }) + }) +}) diff --git a/src/schema.graphql b/src/schema.graphql index 1f9bcb477..4c2a58505 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -133,6 +133,7 @@ type Post { visibility: VisibilityEnum deleted: Boolean disabled: Boolean + disabledBy: User! @relation(name: "DISABLED", direction: "IN") createdAt: String updatedAt: String From 85d9d7043eef6080673f52db806892e11e2881e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 16:19:51 +0100 Subject: [PATCH 45/66] Setup isModerator permission for disable relation --- src/middleware/permissionsMiddleware.js | 5 ++- src/resolvers/posts.spec.js | 51 +++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index c40803e00..ec2261c5a 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -55,7 +55,10 @@ const permissions = shield({ report: isAuthenticated, CreateBadge: isAdmin, UpdateBadge: isAdmin, - DeleteBadge: isAdmin + DeleteBadge: isAdmin, + + AddPostDisabledBy: isModerator, + RemovePostDisabledBy: isModerator, // addFruitToBasket: isAuthenticated // CreateUser: allow, }, diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index 1601e3348..cbe836b21 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -214,10 +214,25 @@ describe('AddPostDisabledBy', () => { } } ` - it.todo('throws authorization error') + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) describe('authenticated', () => { - it.todo('throws authorization error') + let headers + beforeEach(async () => { + await factory.create('User', { + email: 'someUser@example.org', + password: '1234' + }) + headers = await login({ email: 'someUser@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorization error', async () => { + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) describe('as moderator', () => { it.todo('throws authorization error') @@ -231,10 +246,38 @@ describe('AddPostDisabledBy', () => { }) describe('RemovePostDisabledBy', () => { - it.todo('throws authorization error') + const mutation = ` + mutation { + AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { + from { + id + } + to { + id + } + } + } + ` + + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) describe('authenticated', () => { - it.todo('throws authorization error') + let headers + beforeEach(async () => { + await factory.create('User', { + email: 'someUser@example.org', + password: '1234' + }) + headers = await login({ email: 'someUser@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorization error', async () => { + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) describe('as moderator', () => { it.todo('throws authorization error') From f2e7e515a4c1874bcb7df289b2d24f1c797a92f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 16:46:39 +0100 Subject: [PATCH 46/66] Check from: User! matches the authenticated user --- src/middleware/permissionsMiddleware.js | 11 +- src/resolvers/posts.spec.js | 166 ++++++++++++++---------- 2 files changed, 109 insertions(+), 68 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index ec2261c5a..8cf35c2a3 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -1,4 +1,4 @@ -import { rule, shield, allow, or } from 'graphql-shield' +import { rule, shield, allow, and, or } from 'graphql-shield' /* * TODO: implement @@ -41,6 +41,11 @@ const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver return authorId === user.id }) +const fromUserMatchesCurrentUser = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) => { + const { from: { id: fromId } } = args + return user.id === fromId +}) + // Permissions const permissions = shield({ Query: { @@ -57,8 +62,8 @@ const permissions = shield({ UpdateBadge: isAdmin, DeleteBadge: isAdmin, - AddPostDisabledBy: isModerator, - RemovePostDisabledBy: isModerator, + AddPostDisabledBy: and(isModerator, fromUserMatchesCurrentUser), + RemovePostDisabledBy: and(isModerator, fromUserMatchesCurrentUser), // addFruitToBasket: isAuthenticated // CreateUser: allow, }, diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index cbe836b21..515216f34 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -201,52 +201,22 @@ describe('DeletePost', () => { }) }) -describe('AddPostDisabledBy', () => { - const mutation = ` - mutation { - AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { - from { - id - } - to { - id - } - } + + + +describe('disabledBy relation', () => { + const setup = async (params = {}) => { + let headers = {} + const { email, password } = params + if (email && password) { + await factory.create('User', params) + headers = await login({email, password}) } - ` - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) + client = new GraphQLClient(host, { headers }) + } - describe('authenticated', () => { - let headers - beforeEach(async () => { - await factory.create('User', { - email: 'someUser@example.org', - password: '1234' - }) - headers = await login({ email: 'someUser@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - - it('throws authorization error', async () => { - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('as moderator', () => { - it.todo('throws authorization error') - - describe('current user matches provided user', () => { - it.todo('sets current user') - it.todo('updates .disabled on post') - }) - }) - }) -}) - -describe('RemovePostDisabledBy', () => { - const mutation = ` + describe('AddPostDisabledBy', () => { + const mutation = ` mutation { AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { from { @@ -259,32 +229,98 @@ describe('RemovePostDisabledBy', () => { } ` - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('authenticated', () => { - let headers - beforeEach(async () => { - await factory.create('User', { - email: 'someUser@example.org', - password: '1234' - }) - headers = await login({ email: 'someUser@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - it('throws authorization error', async () => { + await setup() await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('as moderator', () => { - it.todo('throws authorization error') + describe('authenticated', () => { + it('throws authorization error', async () => { + await setup({ + email: 'someUser@example.org', + password: '1234' + }) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) - describe('current user matches provided user', () => { - it.todo('sets current user') - it.todo('updates .disabled on post') + describe('as moderator', () => { + it('throws authorization error', async () => { + await setup({ + email: 'attributedUserMismatch@example.org', + password: '1234', + role: 'moderator' + }) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('current user matches provided user', () => { + beforeEach(async () => { + await setup({ + id: 'u7', + email: 'moderator@example.org', + password: '1234', + role: 'moderator' + }) + }) + + it.todo('sets current user') + it.todo('updates .disabled on post') + }) + }) + }) + }) + + describe('RemovePostDisabledBy', () => { + const mutation = ` + mutation { + AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { + from { + id + } + to { + id + } + } + } + ` + + it('throws authorization error', async () => { + await setup() + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('authenticated', () => { + it('throws authorization error', async () => { + await setup({ + email: 'someUser@example.org', + password: '1234' + }) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('as moderator', () => { + it('throws authorization error', async () => { + await setup({ + role: 'moderator', + email: 'someUser@example.org', + password: '1234' + }) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('current user matches provided user', () => { + beforeEach(async () => { + await setup({ + id: 'u7', + role: 'moderator', + email: 'someUser@example.org', + password: '1234' + }) + }) + + it.todo('sets current user') + it.todo('updates .disabled on post') + }) }) }) }) From 99cebc8d64b46766a7ff618afc1f30669f59adc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 17:45:10 +0100 Subject: [PATCH 47/66] Implementation ready except disabled attr. --- src/middleware/permissionsMiddleware.js | 1 + src/resolvers/posts.spec.js | 64 ++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 8cf35c2a3..1c6e8310b 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -42,6 +42,7 @@ const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) const fromUserMatchesCurrentUser = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) => { + if (!user) return false const { from: { id: fromId } } = args return user.id === fromId }) diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index 515216f34..0715f221d 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -206,10 +206,16 @@ describe('DeletePost', () => { describe('disabledBy relation', () => { const setup = async (params = {}) => { + await factory.create('User', {email: 'author@example.org', password: '1234'}) + await factory.authenticateAs({email: 'author@example.org', password: '1234'}) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for + }) + let headers = {} const { email, password } = params if (email && password) { - await factory.create('User', params) + const user = await factory.create('User', params) headers = await login({email, password}) } client = new GraphQLClient(host, { headers }) @@ -218,7 +224,7 @@ describe('disabledBy relation', () => { describe('AddPostDisabledBy', () => { const mutation = ` mutation { - AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { + AddPostDisabledBy(from: { id: "u7" }, to: { id: "p9" }) { from { id } @@ -263,17 +269,47 @@ describe('disabledBy relation', () => { }) }) - it.todo('sets current user') - it.todo('updates .disabled on post') + it('returns created relation', async () => { + const expected = { + AddPostDisabledBy: { + from: { id: 'u7' }, + to: { id: 'p9' } + } + } + await expect(client.request(mutation)).resolves.toEqual(expected) + }) + + it('sets current user', async () => { + await client.request(mutation) + const query = `{ Post { id, disabledBy { id } } }` + const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } + await expect(client.request(query)).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + await client.request(mutation) + const query = `{ Post { id disabled } }` + const expected = { Post: [ { id: 'p9', disabled: true } ] } + await expect(client.request(query)).resolves.toEqual(expected) + }) }) }) }) }) describe('RemovePostDisabledBy', () => { + beforeEach(async () => { + await factory.create('User', {email: 'anotherModerator@example.org', password: '1234', role: 'moderator'}) + await factory.authenticateAs({email: 'anotherModerator@example.org', password: '1234'}) + await factory.relate('Post', 'DisabledBy', { + from: 'u7', + to: 'p9' + }) + }) + const mutation = ` mutation { - AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { + RemovePostDisabledBy(from: { id: "u7" }, to: { id: "p9" }) { from { id } @@ -318,8 +354,22 @@ describe('disabledBy relation', () => { }) }) - it.todo('sets current user') - it.todo('updates .disabled on post') + it('returns deleted relation', async () => { + const expected = { + RemovePostDisabledBy: { + from: { id: 'u7' }, + to: { id: 'p9' } + } + } + await expect(client.request(mutation)).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + await client.request(mutation) + const query = `{ Post { id disabled } }` + const expected = { Post: [ { id: 'p9', disabled: false } ] } + await expect(client.request(query)).resolves.toEqual(expected) + }) }) }) }) From 592f25b978d4a6e38e4ec89504f4d59b73e6917c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 18:14:25 +0100 Subject: [PATCH 48/66] Implement update of .disabled field --- src/resolvers/posts.js | 20 ++++ src/resolvers/posts.spec.js | 179 ++++++++++++++++++++---------------- 2 files changed, 120 insertions(+), 79 deletions(-) diff --git a/src/resolvers/posts.js b/src/resolvers/posts.js index abf91e047..33934699b 100644 --- a/src/resolvers/posts.js +++ b/src/resolvers/posts.js @@ -2,6 +2,26 @@ import { neo4jgraphql } from 'neo4j-graphql-js' export default { Mutation: { + AddPostDisabledBy: async (object, params, context, resolveInfo) => { + const { to: { id: postId } } = params + const session = context.driver.session() + await session.run(` + MATCH (p:Post {id: $postId}) + SET p.disabled = true`, { postId }) + session.close() + return neo4jgraphql(object, params, context, resolveInfo, false) + }, + + RemovePostDisabledBy: async (object, params, context, resolveInfo) => { + const { to: { id: postId } } = params + const session = context.driver.session() + await session.run(` + MATCH (p:Post {id: $postId}) + SET p.disabled = false`, { postId }) + session.close() + return neo4jgraphql(object, params, context, resolveInfo, false) + }, + CreatePost: async (object, params, context, resolveInfo) => { const result = await neo4jgraphql(object, params, context, resolveInfo, false) diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index 0715f221d..427a5d925 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -204,7 +204,7 @@ describe('DeletePost', () => { -describe('disabledBy relation', () => { +describe('AddPostDisabledBy', () => { const setup = async (params = {}) => { await factory.create('User', {email: 'author@example.org', password: '1234'}) await factory.authenticateAs({email: 'author@example.org', password: '1234'}) @@ -221,8 +221,7 @@ describe('disabledBy relation', () => { client = new GraphQLClient(host, { headers }) } - describe('AddPostDisabledBy', () => { - const mutation = ` + const mutation = ` mutation { AddPostDisabledBy(from: { id: "u7" }, to: { id: "p9" }) { from { @@ -235,79 +234,96 @@ describe('disabledBy relation', () => { } ` + it('throws authorization error', async () => { + await setup() + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('authenticated', () => { it('throws authorization error', async () => { - await setup() + await setup({ + email: 'someUser@example.org', + password: '1234' + }) await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('authenticated', () => { + describe('as moderator', () => { it('throws authorization error', async () => { await setup({ - email: 'someUser@example.org', - password: '1234' + email: 'attributedUserMismatch@example.org', + password: '1234', + role: 'moderator' }) await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('as moderator', () => { - it('throws authorization error', async () => { + describe('current user matches provided user', () => { + beforeEach(async () => { await setup({ - email: 'attributedUserMismatch@example.org', + id: 'u7', + email: 'moderator@example.org', password: '1234', role: 'moderator' }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('current user matches provided user', () => { - beforeEach(async () => { - await setup({ - id: 'u7', - email: 'moderator@example.org', - password: '1234', - role: 'moderator' - }) - }) - - it('returns created relation', async () => { - const expected = { - AddPostDisabledBy: { - from: { id: 'u7' }, - to: { id: 'p9' } - } + it('returns created relation', async () => { + const expected = { + AddPostDisabledBy: { + from: { id: 'u7' }, + to: { id: 'p9' } } - await expect(client.request(mutation)).resolves.toEqual(expected) - }) + } + await expect(client.request(mutation)).resolves.toEqual(expected) + }) - it('sets current user', async () => { - await client.request(mutation) - const query = `{ Post { id, disabledBy { id } } }` - const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } - await expect(client.request(query)).resolves.toEqual(expected) - }) + it('sets current user', async () => { + await client.request(mutation) + const query = `{ Post(disabled: true) { id, disabledBy { id } } }` + const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } + await expect(client.request(query)).resolves.toEqual(expected) + }) - it('updates .disabled on post', async () => { - await client.request(mutation) - const query = `{ Post { id disabled } }` - const expected = { Post: [ { id: 'p9', disabled: true } ] } - await expect(client.request(query)).resolves.toEqual(expected) - }) + it('updates .disabled on post', async () => { + const before = { Post: [ { id: 'p9', disabled: false } ] } + const expected = { Post: [ { id: 'p9', disabled: true } ] } + + await expect(client.request( + `{ Post { id disabled } }` + )).resolves.toEqual(before) + await client.request(mutation) // this updates .disabled + await expect(client.request( + `{ Post(disabled: true) { id disabled } }` + )).resolves.toEqual(expected) }) }) }) }) +}) - describe('RemovePostDisabledBy', () => { - beforeEach(async () => { - await factory.create('User', {email: 'anotherModerator@example.org', password: '1234', role: 'moderator'}) - await factory.authenticateAs({email: 'anotherModerator@example.org', password: '1234'}) - await factory.relate('Post', 'DisabledBy', { - from: 'u7', - to: 'p9' - }) +describe('RemovePostDisabledBy', () => { + const setup = async (params = {}) => { + await factory.create('User', {email: 'anotherModerator@example.org', password: '1234', id: 'u123', role: 'moderator'}) + await factory.authenticateAs({email: 'anotherModerator@example.org', password: '1234'}) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for }) + await factory.relate('Post', 'DisabledBy', { + from: 'u123', + to: 'p9' + }) // that's we want to delete - const mutation = ` + let headers = {} + const { email, password } = params + if (email && password) { + const user = await factory.create('User', params) + headers = await login({email, password}) + } + client = new GraphQLClient(host, { headers }) + } + + const mutation = ` mutation { RemovePostDisabledBy(from: { id: "u7" }, to: { id: "p9" }) { from { @@ -320,56 +336,61 @@ describe('disabledBy relation', () => { } ` + it('throws authorization error', async () => { + await setup() + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('authenticated', () => { it('throws authorization error', async () => { - await setup() + await setup({ + email: 'someUser@example.org', + password: '1234' + }) await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('authenticated', () => { + describe('as moderator', () => { it('throws authorization error', async () => { await setup({ + role: 'moderator', email: 'someUser@example.org', password: '1234' }) await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('as moderator', () => { - it('throws authorization error', async () => { + describe('current user matches provided user', () => { + beforeEach(async () => { await setup({ + id: 'u7', role: 'moderator', email: 'someUser@example.org', password: '1234' }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('current user matches provided user', () => { - beforeEach(async () => { - await setup({ - id: 'u7', - role: 'moderator', - email: 'someUser@example.org', - password: '1234' - }) - }) - - it('returns deleted relation', async () => { - const expected = { - RemovePostDisabledBy: { - from: { id: 'u7' }, - to: { id: 'p9' } - } + it('returns deleted relation', async () => { + const expected = { + RemovePostDisabledBy: { + from: { id: 'u7' }, + to: { id: 'p9' } } - await expect(client.request(mutation)).resolves.toEqual(expected) - }) + } + await expect(client.request(mutation)).resolves.toEqual(expected) + }) - it('updates .disabled on post', async () => { - await client.request(mutation) - const query = `{ Post { id disabled } }` - const expected = { Post: [ { id: 'p9', disabled: false } ] } - await expect(client.request(query)).resolves.toEqual(expected) - }) + it('updates .disabled on post', async () => { + const before = { Post: [ { id: 'p9', disabled: true } ] } + const expected = { Post: [ { id: 'p9', disabled: false } ] } + + await expect(client.request( + `{ Post(disabled: true) { id disabled } }` + )).resolves.toEqual(before) + await client.request(mutation) // this updates .disabled + await expect(client.request( + `{ Post { id disabled } }` + )).resolves.toEqual(expected) }) }) }) From 2b7576521cf73fd247884e1f0338c4cb19004639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 18:15:05 +0100 Subject: [PATCH 49/66] Fix lint + return more attributes in post factory for convenience --- src/middleware/permissionsMiddleware.js | 2 +- src/middleware/softDeleteMiddleware.spec.js | 4 +-- src/resolvers/posts.spec.js | 29 +++++++++------------ src/seed/factories/users.js | 3 +++ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 1c6e8310b..44ed2ed34 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -64,7 +64,7 @@ const permissions = shield({ DeleteBadge: isAdmin, AddPostDisabledBy: and(isModerator, fromUserMatchesCurrentUser), - RemovePostDisabledBy: and(isModerator, fromUserMatchesCurrentUser), + RemovePostDisabledBy: and(isModerator, fromUserMatchesCurrentUser) // addFruitToBasket: isAuthenticated // CreateUser: allow, }, diff --git a/src/middleware/softDeleteMiddleware.spec.js b/src/middleware/softDeleteMiddleware.spec.js index c37e7d426..283e16eb0 100644 --- a/src/middleware/softDeleteMiddleware.spec.js +++ b/src/middleware/softDeleteMiddleware.spec.js @@ -19,8 +19,8 @@ beforeEach(async () => { factory.create('Post', { title: 'Publicly visible post', deleted: false }) ]) const moderatorFactory = Factory() - await moderatorFactory.authenticateAs({ email: 'moderator@example.org', password: '1234'}) - await moderatorFactory.relate('Post', 'DisabledBy', { from: 'm1', to: 'p2'}) + await moderatorFactory.authenticateAs({ email: 'moderator@example.org', password: '1234' }) + await moderatorFactory.relate('Post', 'DisabledBy', { from: 'm1', to: 'p2' }) }) afterEach(async () => { diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index 427a5d925..89fb1c8e4 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -201,13 +201,10 @@ describe('DeletePost', () => { }) }) - - - describe('AddPostDisabledBy', () => { const setup = async (params = {}) => { - await factory.create('User', {email: 'author@example.org', password: '1234'}) - await factory.authenticateAs({email: 'author@example.org', password: '1234'}) + await factory.create('User', { email: 'author@example.org', password: '1234' }) + await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) await factory.create('Post', { id: 'p9' // that's the ID we will look for }) @@ -215,8 +212,8 @@ describe('AddPostDisabledBy', () => { let headers = {} const { email, password } = params if (email && password) { - const user = await factory.create('User', params) - headers = await login({email, password}) + await factory.create('User', params) + headers = await login({ email, password }) } client = new GraphQLClient(host, { headers }) } @@ -280,7 +277,7 @@ describe('AddPostDisabledBy', () => { it('sets current user', async () => { await client.request(mutation) - const query = `{ Post(disabled: true) { id, disabledBy { id } } }` + const query = '{ Post(disabled: true) { id, disabledBy { id } } }' const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } await expect(client.request(query)).resolves.toEqual(expected) }) @@ -290,11 +287,11 @@ describe('AddPostDisabledBy', () => { const expected = { Post: [ { id: 'p9', disabled: true } ] } await expect(client.request( - `{ Post { id disabled } }` + '{ Post { id disabled } }' )).resolves.toEqual(before) await client.request(mutation) // this updates .disabled await expect(client.request( - `{ Post(disabled: true) { id disabled } }` + '{ Post(disabled: true) { id disabled } }' )).resolves.toEqual(expected) }) }) @@ -304,8 +301,8 @@ describe('AddPostDisabledBy', () => { describe('RemovePostDisabledBy', () => { const setup = async (params = {}) => { - await factory.create('User', {email: 'anotherModerator@example.org', password: '1234', id: 'u123', role: 'moderator'}) - await factory.authenticateAs({email: 'anotherModerator@example.org', password: '1234'}) + await factory.create('User', { email: 'anotherModerator@example.org', password: '1234', id: 'u123', role: 'moderator' }) + await factory.authenticateAs({ email: 'anotherModerator@example.org', password: '1234' }) await factory.create('Post', { id: 'p9' // that's the ID we will look for }) @@ -317,8 +314,8 @@ describe('RemovePostDisabledBy', () => { let headers = {} const { email, password } = params if (email && password) { - const user = await factory.create('User', params) - headers = await login({email, password}) + await factory.create('User', params) + headers = await login({ email, password }) } client = new GraphQLClient(host, { headers }) } @@ -385,11 +382,11 @@ describe('RemovePostDisabledBy', () => { const expected = { Post: [ { id: 'p9', disabled: false } ] } await expect(client.request( - `{ Post(disabled: true) { id disabled } }` + '{ Post(disabled: true) { id disabled } }' )).resolves.toEqual(before) await client.request(mutation) // this updates .disabled await expect(client.request( - `{ Post { id disabled } }` + '{ Post { id disabled } }' )).resolves.toEqual(expected) }) }) diff --git a/src/seed/factories/users.js b/src/seed/factories/users.js index 8e0ee693c..c27b2b1ce 100644 --- a/src/seed/factories/users.js +++ b/src/seed/factories/users.js @@ -25,10 +25,13 @@ export default function create (params) { disabled: ${disabled}, deleted: ${deleted} ) { + id name email avatar role + deleted + disabled } } ` From a292a522e9524f949f32fbd90fe913d0ce34fb60 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Tue, 5 Mar 2019 18:30:28 +0100 Subject: [PATCH 50/66] Fixed an issue and added basic testing for (un)shout --- src/resolvers/shout.spec.js | 97 +++++++++++++++++++++++++++++++++++++ src/schema.graphql | 2 +- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/resolvers/shout.spec.js diff --git a/src/resolvers/shout.spec.js b/src/resolvers/shout.spec.js new file mode 100644 index 000000000..4f5ea6ed6 --- /dev/null +++ b/src/resolvers/shout.spec.js @@ -0,0 +1,97 @@ +import Factory from '../seed/factories' +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../jest/helpers' + +const factory = Factory() +let clientUser1, clientUser2 + +const mutationShoutPost = (id) => ` + mutation { + shout(id: "${id}", type: Post) + } +` +const mutationUnshoutPost = (id) => ` + mutation { + unshout(id: "${id}", type: Post) + } +` + +beforeEach(async () => { + await factory.create('User', { + id: 'u1', + email: 'test@example.org', + password: '1234' + }) + await factory.create('User', { + id: 'u2', + email: 'test2@example.org', + password: '1234' + }) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + + +describe('shout ', () => { + describe('(un)shout foreign post', () => { + let headersUser1, headersUser2 + beforeEach(async () => { + headersUser1 = await login({ email: 'test@example.org', password: '1234' }) + headersUser2 = await login({ email: 'test2@example.org', password: '1234' }) + clientUser1 = new GraphQLClient(host, { headers: headersUser1 }) + clientUser2 = new GraphQLClient(host, { headers: headersUser2 }) + + await clientUser1.request(` + mutation { + CreatePost(id: "p1", title: "Post Title 1", content: "Some Post Content 1") { + id + title + } + } + `) + await clientUser2.request(` + mutation { + CreatePost(id: "p2", title: "Post Title 2", content: "Some Post Content 2") { + id + title + } + } + `) + }) + + it('I shout a post of another user', async () => { + const res = await clientUser1.request( + mutationShoutPost('p2') + ) + const expected = { + shout: true + } + expect(res).toMatchObject(expected) + }) + + it('I unshout a post of another user', async () => { + // shout + await clientUser1.request( + mutationShoutPost('p2') + ) + const expected = { + unshout: true + } + // unshout + const res = await clientUser1.request(mutationUnshoutPost('p2')) + expect(res).toMatchObject(expected) + }) + + it('I can`t shout my own post', async () => { + const res = await clientUser1.request( + mutationShoutPost('p1') + ) + const expected = { + shout: false + } + expect(res).toMatchObject(expected) + }) + }) +}) diff --git a/src/schema.graphql b/src/schema.graphql index e6e2088f1..b21773c00 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -12,7 +12,7 @@ type Mutation { report(resource: Resource!, description: String): Report "Shout the given Type and ID" - shout(id: ID!, type: ShoutTypeEnum): String! @cypher(statement: """ + shout(id: ID!, type: ShoutTypeEnum): Boolean! @cypher(statement: """ MATCH (n {id: $id})<-[:WROTE]-(wu:User), (u:User {id: $cypherParams.currentUserId}) WHERE $type IN labels(n) AND NOT wu.id = $cypherParams.currentUserId MERGE (u)-[r:SHOUTED]->(n) From 4fdb1562f9c58b63e58fc46a69f6fd06e84aba4c Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Tue, 5 Mar 2019 18:34:35 +0100 Subject: [PATCH 51/66] Added test for shoutedByCurrentUser flag on posts --- src/resolvers/shout.spec.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/resolvers/shout.spec.js b/src/resolvers/shout.spec.js index 4f5ea6ed6..440210720 100644 --- a/src/resolvers/shout.spec.js +++ b/src/resolvers/shout.spec.js @@ -69,6 +69,16 @@ describe('shout ', () => { shout: true } expect(res).toMatchObject(expected) + + const { Post } = await clientUser1.request(`{ + Post(id: "p2") { + shoutedByCurrentUser + } + }`) + const expected2 = { + shoutedByCurrentUser: true + } + expect(Post[0]).toMatchObject(expected2) }) it('I unshout a post of another user', async () => { @@ -82,6 +92,16 @@ describe('shout ', () => { // unshout const res = await clientUser1.request(mutationUnshoutPost('p2')) expect(res).toMatchObject(expected) + + const { Post } = await clientUser1.request(`{ + Post(id: "p2") { + shoutedByCurrentUser + } + }`) + const expected2 = { + shoutedByCurrentUser: false + } + expect(Post[0]).toMatchObject(expected2) }) it('I can`t shout my own post', async () => { @@ -92,6 +112,16 @@ describe('shout ', () => { shout: false } expect(res).toMatchObject(expected) + + const { Post } = await clientUser1.request(`{ + Post(id: "p1") { + shoutedByCurrentUser + } + }`) + const expected2 = { + shoutedByCurrentUser: false + } + expect(Post[0]).toMatchObject(expected2) }) }) }) From c2aea104f4ed3a9b4d9e1c5ed5ba3a195fece02d Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Tue, 5 Mar 2019 18:44:39 +0100 Subject: [PATCH 52/66] Added basic tests for follow mutation --- src/resolvers/follow.spec.js | 118 +++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/resolvers/follow.spec.js diff --git a/src/resolvers/follow.spec.js b/src/resolvers/follow.spec.js new file mode 100644 index 000000000..a7b0d3284 --- /dev/null +++ b/src/resolvers/follow.spec.js @@ -0,0 +1,118 @@ +import Factory from '../seed/factories' +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../jest/helpers' + +const factory = Factory() +let clientUser1, clientUser2 + +const mutationFollowUser = (id) => ` + mutation { + follow(id: "${id}", type: User) + } +` +const mutationUnfollowUser = (id) => ` + mutation { + unfollow(id: "${id}", type: User) + } +` + +beforeEach(async () => { + await factory.create('User', { + id: 'u1', + email: 'test@example.org', + password: '1234' + }) + await factory.create('User', { + id: 'u2', + email: 'test2@example.org', + password: '1234' + }) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + + +describe('follow ', () => { + describe('(un)follow user', () => { + let headersUser1, headersUser2 + beforeEach(async () => { + headersUser1 = await login({ email: 'test@example.org', password: '1234' }) + headersUser2 = await login({ email: 'test2@example.org', password: '1234' }) + clientUser1 = new GraphQLClient(host, { headers: headersUser1 }) + clientUser2 = new GraphQLClient(host, { headers: headersUser2 }) + }) + + it('I can follow another user', async () => { + const res = await clientUser1.request( + mutationFollowUser('u2') + ) + const expected = { + follow: true + } + expect(res).toMatchObject(expected) + + const { User } = await clientUser1.request(`{ + User(id: "u2") { + followedBy { id } + followedByCurrentUser + } + }`) + const expected2 = { + followedBy: [ + { id: 'u1' } + ], + followedByCurrentUser: true + } + expect(User[0]).toMatchObject(expected2) + }) + + it('I can unfollow a user', async () => { + // follow + await clientUser1.request( + mutationFollowUser('u2') + ) + const expected = { + unfollow: true + } + // unfollow + const res = await clientUser1.request(mutationUnfollowUser('u2')) + expect(res).toMatchObject(expected) + + const { User } = await clientUser1.request(`{ + User(id: "u2") { + followedBy { id } + followedByCurrentUser + } + }`) + const expected2 = { + followedBy: [], + followedByCurrentUser: false + } + expect(User[0]).toMatchObject(expected2) + }) + + it('I can`t follow myself', async () => { + const res = await clientUser1.request( + mutationFollowUser('u1') + ) + const expected = { + follow: false + } + expect(res).toMatchObject(expected) + + const { User } = await clientUser1.request(`{ + User(id: "u1") { + followedBy { id } + followedByCurrentUser + } + }`) + const expected2 = { + followedBy: [], + followedByCurrentUser: false + } + expect(User[0]).toMatchObject(expected2) + }) + }) +}) From 865fdebc5f06257646631965d4793df9514b1222 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Tue, 5 Mar 2019 18:58:17 +0100 Subject: [PATCH 53/66] Fixed linting issues inside tests --- src/resolvers/follow.spec.js | 7 ++----- src/resolvers/shout.spec.js | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/resolvers/follow.spec.js b/src/resolvers/follow.spec.js index a7b0d3284..3c16560e5 100644 --- a/src/resolvers/follow.spec.js +++ b/src/resolvers/follow.spec.js @@ -3,7 +3,7 @@ import { GraphQLClient } from 'graphql-request' import { host, login } from '../jest/helpers' const factory = Factory() -let clientUser1, clientUser2 +let clientUser1 const mutationFollowUser = (id) => ` mutation { @@ -33,15 +33,12 @@ afterEach(async () => { await factory.cleanDatabase() }) - describe('follow ', () => { describe('(un)follow user', () => { - let headersUser1, headersUser2 + let headersUser1 beforeEach(async () => { headersUser1 = await login({ email: 'test@example.org', password: '1234' }) - headersUser2 = await login({ email: 'test2@example.org', password: '1234' }) clientUser1 = new GraphQLClient(host, { headers: headersUser1 }) - clientUser2 = new GraphQLClient(host, { headers: headersUser2 }) }) it('I can follow another user', async () => { diff --git a/src/resolvers/shout.spec.js b/src/resolvers/shout.spec.js index 440210720..490191c7a 100644 --- a/src/resolvers/shout.spec.js +++ b/src/resolvers/shout.spec.js @@ -33,7 +33,6 @@ afterEach(async () => { await factory.cleanDatabase() }) - describe('shout ', () => { describe('(un)shout foreign post', () => { let headersUser1, headersUser2 From 0a73ddd46d1aceeb982c1d427839845615c3065a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 23:59:54 +0100 Subject: [PATCH 54/66] Refactor: custom resolvers for moderation --- src/graphql-schema.js | 2 + src/middleware/permissionsMiddleware.js | 4 +- src/resolvers/moderation.js | 10 ++ src/resolvers/moderation.spec.js | 170 +++++++++++++++++++++ src/resolvers/posts.spec.js | 193 ------------------------ src/schema.graphql | 4 +- 6 files changed, 187 insertions(+), 196 deletions(-) create mode 100644 src/resolvers/moderation.js create mode 100644 src/resolvers/moderation.spec.js diff --git a/src/graphql-schema.js b/src/graphql-schema.js index 6d10183c9..c2d96ce16 100644 --- a/src/graphql-schema.js +++ b/src/graphql-schema.js @@ -4,6 +4,7 @@ import userManagement from './resolvers/user_management.js' import statistics from './resolvers/statistics.js' import reports from './resolvers/reports.js' import posts from './resolvers/posts.js' +import moderation from './resolvers/moderation.js' export const typeDefs = fs.readFileSync(process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql')) @@ -17,6 +18,7 @@ export const resolvers = { Mutation: { ...userManagement.Mutation, ...reports.Mutation, + ...moderation.Mutation, ...posts.Mutation } } diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 44ed2ed34..dff11b888 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -63,8 +63,8 @@ const permissions = shield({ UpdateBadge: isAdmin, DeleteBadge: isAdmin, - AddPostDisabledBy: and(isModerator, fromUserMatchesCurrentUser), - RemovePostDisabledBy: and(isModerator, fromUserMatchesCurrentUser) + enable: isModerator, + disable: isModerator, // addFruitToBasket: isAuthenticated // CreateUser: allow, }, diff --git a/src/resolvers/moderation.js b/src/resolvers/moderation.js new file mode 100644 index 000000000..15fd291e9 --- /dev/null +++ b/src/resolvers/moderation.js @@ -0,0 +1,10 @@ +export default { + Mutation: { + disable: async (object, params, {user, driver}) => { + return true + }, + enable: async (object, params, {user, driver}) => { + return true + } + } +} diff --git a/src/resolvers/moderation.spec.js b/src/resolvers/moderation.spec.js new file mode 100644 index 000000000..b6249a8b7 --- /dev/null +++ b/src/resolvers/moderation.spec.js @@ -0,0 +1,170 @@ +import Factory from '../seed/factories' +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../jest/helpers' + +const factory = Factory() +let client + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('disable', () => { + const setup = async (params = {}) => { + await factory.create('User', { email: 'author@example.org', password: '1234' }) + await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for + }) + + // create the user we use in the scenario below + let headers = {} + const { email, password } = params + if (email && password) { + await factory.create('User', params) + headers = await login({ email, password }) + } + client = new GraphQLClient(host, { headers }) + } + + const mutation = ` + mutation { + disable(resource: { + id: "p9" + type: contribution + }) + } + ` + + it('throws authorization error', async () => { + await setup() + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('authenticated', () => { + it('throws authorization error', async () => { + await setup({ + email: 'someUser@example.org', + password: '1234' + }) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('as moderator', () => { + beforeEach(async () => { + await setup({ + id: 'u7', + email: 'moderator@example.org', + password: '1234', + role: 'moderator' + }) + }) + + it('returns true', async () => { + const expected = { disable: true } + await expect(client.request(mutation)).resolves.toEqual(expected) + }) + + it('sets current user', async () => { + const before = { Post: [{ id: 'p9', disabledBy: null }] } + const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } + + await expect(client.request( + '{ Post { id, disabledBy { id } } }' + )).resolves.toEqual(before) + await client.request(mutation) + await expect(client.request( + '{ Post(disabled: true) { id, disabledBy { id } } }' + )).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + const before = { Post: [ { id: 'p9', disabled: false } ] } + const expected = { Post: [ { id: 'p9', disabled: true } ] } + + await expect(client.request( + '{ Post { id disabled } }' + )).resolves.toEqual(before) + await client.request(mutation) // this updates .disabled + await expect(client.request( + '{ Post(disabled: true) { id disabled } }' + )).resolves.toEqual(expected) + }) + }) + }) +}) + +describe('enable', () => { + const setup = async (params = {}) => { + await factory.create('User', { email: 'anotherModerator@example.org', password: '1234', id: 'u123', role: 'moderator' }) + await factory.authenticateAs({ email: 'anotherModerator@example.org', password: '1234' }) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for + }) + await factory.relate('Post', 'DisabledBy', { + from: 'u123', + to: 'p9' + }) // that's we want to delete + + let headers = {} + const { email, password } = params + if (email && password) { + await factory.create('User', params) + headers = await login({ email, password }) + } + client = new GraphQLClient(host, { headers }) + } + + + const mutation = ` + mutation { + enable(resource: { + id: "p9" + type: contribution + }) + } + ` + + it('throws authorization error', async () => { + await setup() + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('authenticated', () => { + it('throws authorization error', async () => { + await setup({ + email: 'someUser@example.org', + password: '1234' + }) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('as moderator', () => { + beforeEach(async () => { + await setup({ + role: 'moderator', + email: 'someUser@example.org', + password: '1234' + }) + }) + + it('returns true', async () => { + const expected = { enable: true } + await expect(client.request(mutation)).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + const before = { Post: [ { id: 'p9', disabled: true } ] } + const expected = { Post: [ { id: 'p9', disabled: false } ] } + + await expect(client.request( + '{ Post(disabled: true) { id disabled } }' + )).resolves.toEqual(before) + await client.request(mutation) // this updates .disabled + await expect(client.request( + '{ Post { id disabled } }' + )).resolves.toEqual(expected) + }) + }) + }) +}) diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index 89fb1c8e4..5603683eb 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -200,196 +200,3 @@ describe('DeletePost', () => { }) }) }) - -describe('AddPostDisabledBy', () => { - const setup = async (params = {}) => { - await factory.create('User', { email: 'author@example.org', password: '1234' }) - await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) - await factory.create('Post', { - id: 'p9' // that's the ID we will look for - }) - - let headers = {} - const { email, password } = params - if (email && password) { - await factory.create('User', params) - headers = await login({ email, password }) - } - client = new GraphQLClient(host, { headers }) - } - - const mutation = ` - mutation { - AddPostDisabledBy(from: { id: "u7" }, to: { id: "p9" }) { - from { - id - } - to { - id - } - } - } - ` - - it('throws authorization error', async () => { - await setup() - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('authenticated', () => { - it('throws authorization error', async () => { - await setup({ - email: 'someUser@example.org', - password: '1234' - }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('as moderator', () => { - it('throws authorization error', async () => { - await setup({ - email: 'attributedUserMismatch@example.org', - password: '1234', - role: 'moderator' - }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('current user matches provided user', () => { - beforeEach(async () => { - await setup({ - id: 'u7', - email: 'moderator@example.org', - password: '1234', - role: 'moderator' - }) - }) - - it('returns created relation', async () => { - const expected = { - AddPostDisabledBy: { - from: { id: 'u7' }, - to: { id: 'p9' } - } - } - await expect(client.request(mutation)).resolves.toEqual(expected) - }) - - it('sets current user', async () => { - await client.request(mutation) - const query = '{ Post(disabled: true) { id, disabledBy { id } } }' - const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } - await expect(client.request(query)).resolves.toEqual(expected) - }) - - it('updates .disabled on post', async () => { - const before = { Post: [ { id: 'p9', disabled: false } ] } - const expected = { Post: [ { id: 'p9', disabled: true } ] } - - await expect(client.request( - '{ Post { id disabled } }' - )).resolves.toEqual(before) - await client.request(mutation) // this updates .disabled - await expect(client.request( - '{ Post(disabled: true) { id disabled } }' - )).resolves.toEqual(expected) - }) - }) - }) - }) -}) - -describe('RemovePostDisabledBy', () => { - const setup = async (params = {}) => { - await factory.create('User', { email: 'anotherModerator@example.org', password: '1234', id: 'u123', role: 'moderator' }) - await factory.authenticateAs({ email: 'anotherModerator@example.org', password: '1234' }) - await factory.create('Post', { - id: 'p9' // that's the ID we will look for - }) - await factory.relate('Post', 'DisabledBy', { - from: 'u123', - to: 'p9' - }) // that's we want to delete - - let headers = {} - const { email, password } = params - if (email && password) { - await factory.create('User', params) - headers = await login({ email, password }) - } - client = new GraphQLClient(host, { headers }) - } - - const mutation = ` - mutation { - RemovePostDisabledBy(from: { id: "u7" }, to: { id: "p9" }) { - from { - id - } - to { - id - } - } - } - ` - - it('throws authorization error', async () => { - await setup() - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('authenticated', () => { - it('throws authorization error', async () => { - await setup({ - email: 'someUser@example.org', - password: '1234' - }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('as moderator', () => { - it('throws authorization error', async () => { - await setup({ - role: 'moderator', - email: 'someUser@example.org', - password: '1234' - }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('current user matches provided user', () => { - beforeEach(async () => { - await setup({ - id: 'u7', - role: 'moderator', - email: 'someUser@example.org', - password: '1234' - }) - }) - - it('returns deleted relation', async () => { - const expected = { - RemovePostDisabledBy: { - from: { id: 'u7' }, - to: { id: 'p9' } - } - } - await expect(client.request(mutation)).resolves.toEqual(expected) - }) - - it('updates .disabled on post', async () => { - const before = { Post: [ { id: 'p9', disabled: true } ] } - const expected = { Post: [ { id: 'p9', disabled: false } ] } - - await expect(client.request( - '{ Post(disabled: true) { id disabled } }' - )).resolves.toEqual(before) - await client.request(mutation) // this updates .disabled - await expect(client.request( - '{ Post { id disabled } }' - )).resolves.toEqual(expected) - }) - }) - }) - }) -}) diff --git a/src/schema.graphql b/src/schema.graphql index 4c2a58505..ddafd880e 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -7,6 +7,8 @@ type Mutation { login(email: String!, password: String!): String! signup(email: String!, password: String!): Boolean! report(resource: Resource!, description: String): Report + disable(resource: Resource!): Boolean! + enable(resource: Resource!): Boolean! } type Statistics { @@ -133,7 +135,7 @@ type Post { visibility: VisibilityEnum deleted: Boolean disabled: Boolean - disabledBy: User! @relation(name: "DISABLED", direction: "IN") + disabledBy: User @relation(name: "DISABLED", direction: "IN") createdAt: String updatedAt: String From 5cff508bd6d9b553e0bf8d9fcc5e29143062583b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 6 Mar 2019 00:55:26 +0100 Subject: [PATCH 55/66] Disable/enable fullfills tests --- src/middleware/permissionsMiddleware.js | 10 ++----- src/middleware/softDeleteMiddleware.spec.js | 10 ++++++- src/resolvers/moderation.js | 28 ++++++++++++++++--- src/resolvers/moderation.spec.js | 31 ++++++++++++++++----- src/resolvers/posts.js | 20 ------------- src/seed/factories/index.js | 5 ++++ src/seed/seed-db.js | 13 ++++++--- 7 files changed, 73 insertions(+), 44 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index dff11b888..7fb6e75b8 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -1,4 +1,4 @@ -import { rule, shield, allow, and, or } from 'graphql-shield' +import { rule, shield, allow, or } from 'graphql-shield' /* * TODO: implement @@ -41,12 +41,6 @@ const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver return authorId === user.id }) -const fromUserMatchesCurrentUser = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) => { - if (!user) return false - const { from: { id: fromId } } = args - return user.id === fromId -}) - // Permissions const permissions = shield({ Query: { @@ -64,7 +58,7 @@ const permissions = shield({ DeleteBadge: isAdmin, enable: isModerator, - disable: isModerator, + disable: isModerator // addFruitToBasket: isAuthenticated // CreateUser: allow, }, diff --git a/src/middleware/softDeleteMiddleware.spec.js b/src/middleware/softDeleteMiddleware.spec.js index 283e16eb0..e9bc461f1 100644 --- a/src/middleware/softDeleteMiddleware.spec.js +++ b/src/middleware/softDeleteMiddleware.spec.js @@ -20,7 +20,15 @@ beforeEach(async () => { ]) const moderatorFactory = Factory() await moderatorFactory.authenticateAs({ email: 'moderator@example.org', password: '1234' }) - await moderatorFactory.relate('Post', 'DisabledBy', { from: 'm1', to: 'p2' }) + const disableMutation = ` + mutation { + disable(resource: { + id: "p2" + type: contribution + }) + } + ` + await moderatorFactory.mutate(disableMutation) }) afterEach(async () => { diff --git a/src/resolvers/moderation.js b/src/resolvers/moderation.js index 15fd291e9..8e171dbf6 100644 --- a/src/resolvers/moderation.js +++ b/src/resolvers/moderation.js @@ -1,10 +1,30 @@ export default { Mutation: { - disable: async (object, params, {user, driver}) => { - return true + disable: async (object, params, { user, driver }) => { + const { resource: { id: postId } } = params + const { id: userId } = user + const cypher = ` + MATCH (u:User {id: $userId}) + MATCH (p:Post {id: $postId}) + SET p.disabled = true + MERGE (p)<-[:DISABLED]-(u) + ` + const session = driver.session() + const res = await session.run(cypher, { postId, userId }) + session.close() + return Boolean(res) }, - enable: async (object, params, {user, driver}) => { - return true + enable: async (object, params, { user, driver }) => { + const { resource: { id: postId } } = params + const cypher = ` + MATCH (p:Post {id: $postId})<-[d:DISABLED]-() + SET p.disabled = false + DELETE d + ` + const session = driver.session() + const res = await session.run(cypher, { postId }) + session.close() + return Boolean(res) } } } diff --git a/src/resolvers/moderation.spec.js b/src/resolvers/moderation.spec.js index b6249a8b7..081d7c38d 100644 --- a/src/resolvers/moderation.spec.js +++ b/src/resolvers/moderation.spec.js @@ -65,8 +65,8 @@ describe('disable', () => { await expect(client.request(mutation)).resolves.toEqual(expected) }) - it('sets current user', async () => { - const before = { Post: [{ id: 'p9', disabledBy: null }] } + it('changes .disabledBy', async () => { + const before = { Post: [{ id: 'p9', disabledBy: null }] } const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } await expect(client.request( @@ -101,10 +101,15 @@ describe('enable', () => { await factory.create('Post', { id: 'p9' // that's the ID we will look for }) - await factory.relate('Post', 'DisabledBy', { - from: 'u123', - to: 'p9' - }) // that's we want to delete + const disableMutation = ` + mutation { + disable(resource: { + id: "p9" + type: contribution + }) + } + ` + await factory.mutate(disableMutation) // that's we want to delete let headers = {} const { email, password } = params @@ -115,7 +120,6 @@ describe('enable', () => { client = new GraphQLClient(host, { headers }) } - const mutation = ` mutation { enable(resource: { @@ -153,6 +157,19 @@ describe('enable', () => { await expect(client.request(mutation)).resolves.toEqual(expected) }) + it('changes .disabledBy', async () => { + const before = { Post: [{ id: 'p9', disabledBy: { id: 'u123' } }] } + const expected = { Post: [{ id: 'p9', disabledBy: null }] } + + await expect(client.request( + '{ Post(disabled: true) { id, disabledBy { id } } }' + )).resolves.toEqual(before) + await client.request(mutation) + await expect(client.request( + '{ Post { id, disabledBy { id } } }' + )).resolves.toEqual(expected) + }) + it('updates .disabled on post', async () => { const before = { Post: [ { id: 'p9', disabled: true } ] } const expected = { Post: [ { id: 'p9', disabled: false } ] } diff --git a/src/resolvers/posts.js b/src/resolvers/posts.js index 33934699b..abf91e047 100644 --- a/src/resolvers/posts.js +++ b/src/resolvers/posts.js @@ -2,26 +2,6 @@ import { neo4jgraphql } from 'neo4j-graphql-js' export default { Mutation: { - AddPostDisabledBy: async (object, params, context, resolveInfo) => { - const { to: { id: postId } } = params - const session = context.driver.session() - await session.run(` - MATCH (p:Post {id: $postId}) - SET p.disabled = true`, { postId }) - session.close() - return neo4jgraphql(object, params, context, resolveInfo, false) - }, - - RemovePostDisabledBy: async (object, params, context, resolveInfo) => { - const { to: { id: postId } } = params - const session = context.driver.session() - await session.run(` - MATCH (p:Post {id: $postId}) - SET p.disabled = false`, { postId }) - session.close() - return neo4jgraphql(object, params, context, resolveInfo, false) - }, - CreatePost: async (object, params, context, resolveInfo) => { const result = await neo4jgraphql(object, params, context, resolveInfo, false) diff --git a/src/seed/factories/index.js b/src/seed/factories/index.js index ed35d2c3b..68dd99200 100644 --- a/src/seed/factories/index.js +++ b/src/seed/factories/index.js @@ -86,6 +86,10 @@ export default function Factory (options = {}) { this.lastResponse = await this.graphQLClient.request(mutation) return this }, + async mutate (mutation, variables) { + this.lastResponse = await this.graphQLClient.request(mutation, variables) + return this + }, async cleanDatabase () { this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }) return this @@ -94,6 +98,7 @@ export default function Factory (options = {}) { result.authenticateAs.bind(result) result.create.bind(result) result.relate.bind(result) + result.mutate.bind(result) result.cleanDatabase.bind(result) return result } diff --git a/src/seed/seed-db.js b/src/seed/seed-db.js index c20d524ef..310089ef7 100644 --- a/src/seed/seed-db.js +++ b/src/seed/seed-db.js @@ -98,10 +98,15 @@ import Factory from './factories' asTick.create('Post', { id: 'p15' }) ]) - await asModerator.relate('Post', 'DisabledBy', { - from: 'u2', - to: 'p15' - }) + const disableMutation = ` + mutation { + disable(resource: { + id: "p11" + type: contribution + }) + } + ` + await asModerator.mutate(disableMutation) await Promise.all([ f.relate('Post', 'Categories', { from: 'p0', to: 'cat16' }), From 09ec8c3602337b5ae0e5337acd99100c3bd7dc6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Wed, 6 Mar 2019 04:31:31 +0000 Subject: [PATCH 56/66] Bump apollo-link-http from 1.5.11 to 1.5.12 Bumps [apollo-link-http](https://github.com/apollographql/apollo-link) from 1.5.11 to 1.5.12. - [Release notes](https://github.com/apollographql/apollo-link/releases) - [Changelog](https://github.com/apollographql/apollo-link/blob/master/CHANGELOG.md) - [Commits](https://github.com/apollographql/apollo-link/compare/apollo-link-http@1.5.11...apollo-link-http@1.5.12) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 54 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 1c6e8f2ce..a0f745bf7 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "dependencies": { "apollo-cache-inmemory": "~1.5.1", "apollo-client": "~2.5.1", - "apollo-link-http": "~1.5.11", + "apollo-link-http": "~1.5.12", "apollo-server": "~2.4.8", "bcryptjs": "~2.4.3", "cheerio": "~1.0.0-rc.2", diff --git a/yarn.lock b/yarn.lock index 18d336c7a..215a608ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1106,27 +1106,33 @@ apollo-link-dedup@^1.0.0: dependencies: apollo-link "^1.2.4" -apollo-link-http-common@^0.2.10: - version "0.2.10" - resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.10.tgz#b5bbf502ff40a81cc00281ba3b8543b7ad866dfe" - integrity sha512-KY9nhpAurw3z48OIYV0sCZFXrzWp/wjECsveK+Q9GUhhSe1kEbbUjFfmi+qigg+iELgdp5V8ioRJhinl1vPojw== +apollo-link-http-common@^0.2.11: + version "0.2.11" + resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.11.tgz#d4e494ed1e45ea0e0c0ed60f3df64541d0de682d" + integrity sha512-FjtzEDiG6blH/2MR4fpVNoxdZUFmddP0sez34qnoLaYz6ABFbTDlmRE/dVN79nPExM4Spfs/DtW7KRqyjJ3tOg== dependencies: - apollo-link "^1.2.8" + apollo-link "^1.2.9" + ts-invariant "^0.3.2" + tslib "^1.9.3" -apollo-link-http@~1.5.11: - version "1.5.11" - resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.11.tgz#1f72a377d03e874a08bc9eadb1ce7ecb166f1e56" - integrity sha512-wDG+I9UmpfaZRPIvTYBgkvqiCgmz6yWgvuzW/S24Q4r4Xrfe6sLpg2FmarhtdP+hdN+IXTLbFNCZ+Trgfpifow== +apollo-link-http@~1.5.12: + version "1.5.12" + resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.12.tgz#878d48bf9d8ae091752710529a222c4a5548118e" + integrity sha512-2tS36RIU6OdxzoWYTPrjvDTF2sCrnlaJ6SL7j0ILPn1Lmw4y6YLwKDsv/SWLwtodtVe9v1dLCGKIGMRMM/SdyA== dependencies: - apollo-link "^1.2.8" - apollo-link-http-common "^0.2.10" + apollo-link "^1.2.9" + apollo-link-http-common "^0.2.11" + tslib "^1.9.3" -apollo-link@^1.0.0, apollo-link@^1.2.3, apollo-link@^1.2.4, apollo-link@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.8.tgz#0f252adefd5047ac1a9f35ba9439d216587dcd84" - integrity sha512-lfzGRxhK9RmiH3HPFi7TIEBhhDY9M5a2ZDnllcfy5QDk7cCQHQ1WQArcw1FK0g1B+mV4Kl72DSrlvZHZJEolrA== +apollo-link@^1.0.0, apollo-link@^1.2.3, apollo-link@^1.2.4, apollo-link@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.9.tgz#40a8f0b90716ce3fd6beb27b7eae1108b92e0054" + integrity sha512-ZLUwthOFZq4lxchQ2jeBfVqS/UDdcVmmh8aUw6Ar9awZH4r+RgkcDeu2ooFLUfodWE3mZr7wIZuYsBas/MaNVA== dependencies: - zen-observable-ts "^0.8.15" + apollo-utilities "^1.2.1" + ts-invariant "^0.3.2" + tslib "^1.9.3" + zen-observable-ts "^0.8.16" apollo-server-caching@0.3.1: version "0.3.1" @@ -6781,6 +6787,13 @@ ts-invariant@^0.2.1: dependencies: tslib "^1.9.3" +ts-invariant@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.3.2.tgz#89a2ffeb70879b777258df1df1c59383c35209b0" + integrity sha512-QsY8BCaRnHiB5T6iE4DPlJMAKEG3gzMiUco9FEt1jUXQf0XP6zi0idT0i0rMTu8A326JqNSDsmlkA9dRSh1TRg== + dependencies: + tslib "^1.9.3" + tslib@^1.9.0, tslib@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" @@ -7257,11 +7270,12 @@ yup@^0.26.10: synchronous-promise "^2.0.5" toposort "^2.0.2" -zen-observable-ts@^0.8.15: - version "0.8.15" - resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.15.tgz#6cf7df6aa619076e4af2f707ccf8a6290d26699b" - integrity sha512-sXKPWiw6JszNEkRv5dQ+lQCttyjHM2Iks74QU5NP8mMPS/NrzTlHDr780gf/wOBqmHkPO6NCLMlsa+fAQ8VE8w== +zen-observable-ts@^0.8.16: + version "0.8.16" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.16.tgz#969367299074fe17422fe2f46ee417e9a30cf3fa" + integrity sha512-pQl75N7qwgybKVsh6WFO+WwPRijeQ52Gn1vSf4uvPFXald9CbVQXLa5QrOPEJhdZiC+CD4quqOVqSG+Ptz5XLA== dependencies: + tslib "^1.9.3" zen-observable "^0.8.0" zen-observable@^0.8.0: From 2813de4f8b79968b6c8d032295c4edb125df66ca Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Wed, 6 Mar 2019 13:20:33 +0100 Subject: [PATCH 57/66] Fixed organization seeder --- src/seed/factories/organizations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seed/factories/organizations.js b/src/seed/factories/organizations.js index 783edac26..e0b2e52d4 100644 --- a/src/seed/factories/organizations.js +++ b/src/seed/factories/organizations.js @@ -4,7 +4,7 @@ import uuid from 'uuid/v4' export default function create (params) { const { id = uuid(), - name = faker.comany.companyName(), + name = faker.company.companyName(), description = faker.company.catchPhrase(), disabled = false, deleted = false From 1c34f10f967280ebc69f28dba48341dca80e02b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 6 Mar 2019 15:14:08 +0100 Subject: [PATCH 58/66] Test refactoring: Check comments + posts --- src/resolvers/moderation.spec.js | 418 ++++++++++++++++++++++--------- 1 file changed, 303 insertions(+), 115 deletions(-) diff --git a/src/resolvers/moderation.spec.js b/src/resolvers/moderation.spec.js index 081d7c38d..6107074ef 100644 --- a/src/resolvers/moderation.spec.js +++ b/src/resolvers/moderation.spec.js @@ -5,54 +5,76 @@ import { host, login } from '../jest/helpers' const factory = Factory() let client +const setupAuthenticateClient = (params) => { + const authenticateClient = async () => { + await factory.create('User', params) + const headers = await login(params) + client = new GraphQLClient(host, { headers }) + } + return authenticateClient +} + +let setup +const runSetup = async () => { + await setup.createResource() + await setup.authenticateClient() +} + +beforeEach(() => { + setup = { + createResource: () => { + }, + authenticateClient: () => { + client = new GraphQLClient(host) + } + } +}) + afterEach(async () => { await factory.cleanDatabase() }) describe('disable', () => { - const setup = async (params = {}) => { - await factory.create('User', { email: 'author@example.org', password: '1234' }) - await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) - await factory.create('Post', { - id: 'p9' // that's the ID we will look for - }) - - // create the user we use in the scenario below - let headers = {} - const { email, password } = params - if (email && password) { - await factory.create('User', params) - headers = await login({ email, password }) - } - client = new GraphQLClient(host, { headers }) - } - const mutation = ` - mutation { - disable(resource: { - id: "p9" - type: contribution - }) + mutation($id: ID!, $type: ResourceEnum!) { + disable(resource: { id: $id, type: $type }) } ` + let variables + + beforeEach(() => { + // our defaul set of variables + variables = { + id: 'blabla', + type: 'contribution' + } + }) + + const action = async () => { + return client.request(mutation, variables) + } it('throws authorization error', async () => { - await setup() - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + await runSetup() + await expect(action()).rejects.toThrow('Not Authorised') }) describe('authenticated', () => { - it('throws authorization error', async () => { - await setup({ - email: 'someUser@example.org', + beforeEach(() => { + setup.authenticateClient = setupAuthenticateClient({ + email: 'user@example.org', password: '1234' }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + it('throws authorization error', async () => { + await runSetup() + await expect(action()).rejects.toThrow('Not Authorised') }) describe('as moderator', () => { - beforeEach(async () => { - await setup({ + beforeEach(() => { + setup.authenticateClient = setupAuthenticateClient({ id: 'u7', email: 'moderator@example.org', password: '1234', @@ -60,127 +82,293 @@ describe('disable', () => { }) }) - it('returns true', async () => { - const expected = { disable: true } - await expect(client.request(mutation)).resolves.toEqual(expected) + describe('on a comment', () => { + beforeEach(async () => { + variables = { + id: 'c47', + type: 'comment' + } + + setup.createResource = async () => { + await factory.create('User', { id: 'u45', email: 'commenter@example.org', password: '1234' }) + await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' }) + await factory.create('Post', { + id: 'p3' + }) + await factory.create('Comment', { + id: 'c47' + }) + await Promise.all([ + factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' }), + factory.relate('Comment', 'Post', { from: 'c47', to: 'p3' }) + ]) + } + }) + + it('returns true', async () => { + const expected = { disable: true } + await runSetup() + await expect(action()).resolves.toEqual(expected) + }) + + it('changes .disabledBy', async () => { + const before = { Comment: [{ id: 'c47', disabledBy: null }] } + const expected = { Comment: [{ id: 'c47', disabledBy: { id: 'u7' } }] } + + await runSetup() + await expect(client.request( + '{ Comment { id, disabledBy { id } } }' + )).resolves.toEqual(before) + await action() + await expect(client.request( + '{ Comment(disabled: true) { id, disabledBy { id } } }' + )).resolves.toEqual(expected) + }) + + it('updates .disabled on comment', async () => { + const before = { Comment: [ { id: 'c47', disabled: false } ] } + const expected = { Comment: [ { id: 'c47', disabled: true } ] } + + await runSetup() + await expect(client.request( + '{ Comment { id disabled } }' + )).resolves.toEqual(before) + await action() + await expect(client.request( + '{ Comment(disabled: true) { id disabled } }' + )).resolves.toEqual(expected) + }) }) - it('changes .disabledBy', async () => { - const before = { Post: [{ id: 'p9', disabledBy: null }] } - const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } + describe('on a post', () => { + beforeEach(async () => { + variables = { + id: 'p9', + type: 'contribution' + } - await expect(client.request( - '{ Post { id, disabledBy { id } } }' - )).resolves.toEqual(before) - await client.request(mutation) - await expect(client.request( - '{ Post(disabled: true) { id, disabledBy { id } } }' - )).resolves.toEqual(expected) - }) + setup.createResource = async () => { + await factory.create('User', { email: 'author@example.org', password: '1234' }) + await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for + }) + } + }) - it('updates .disabled on post', async () => { - const before = { Post: [ { id: 'p9', disabled: false } ] } - const expected = { Post: [ { id: 'p9', disabled: true } ] } + it('returns true', async () => { + const expected = { disable: true } + await runSetup() + await expect(action()).resolves.toEqual(expected) + }) - await expect(client.request( - '{ Post { id disabled } }' - )).resolves.toEqual(before) - await client.request(mutation) // this updates .disabled - await expect(client.request( - '{ Post(disabled: true) { id disabled } }' - )).resolves.toEqual(expected) + it('changes .disabledBy', async () => { + const before = { Post: [{ id: 'p9', disabledBy: null }] } + const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } + + await runSetup() + await expect(client.request( + '{ Post { id, disabledBy { id } } }' + )).resolves.toEqual(before) + await action() + await expect(client.request( + '{ Post(disabled: true) { id, disabledBy { id } } }' + )).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + const before = { Post: [ { id: 'p9', disabled: false } ] } + const expected = { Post: [ { id: 'p9', disabled: true } ] } + + await runSetup() + await expect(client.request( + '{ Post { id disabled } }' + )).resolves.toEqual(before) + await action() + await expect(client.request( + '{ Post(disabled: true) { id disabled } }' + )).resolves.toEqual(expected) + }) }) }) }) }) describe('enable', () => { - const setup = async (params = {}) => { - await factory.create('User', { email: 'anotherModerator@example.org', password: '1234', id: 'u123', role: 'moderator' }) - await factory.authenticateAs({ email: 'anotherModerator@example.org', password: '1234' }) - await factory.create('Post', { - id: 'p9' // that's the ID we will look for - }) - const disableMutation = ` - mutation { - disable(resource: { - id: "p9" - type: contribution - }) - } - ` - await factory.mutate(disableMutation) // that's we want to delete - - let headers = {} - const { email, password } = params - if (email && password) { - await factory.create('User', params) - headers = await login({ email, password }) - } - client = new GraphQLClient(host, { headers }) - } - const mutation = ` - mutation { - enable(resource: { - id: "p9" - type: contribution - }) + mutation($id: ID!, $type: ResourceEnum!) { + enable(resource: { id: $id, type: $type }) } ` + let variables + + const action = async () => { + return client.request(mutation, variables) + } + + beforeEach(() => { + // our defaul set of variables + variables = { + id: 'blabla', + type: 'contribution' + } + }) it('throws authorization error', async () => { - await setup() - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + await runSetup() + await expect(action()).rejects.toThrow('Not Authorised') }) describe('authenticated', () => { - it('throws authorization error', async () => { - await setup({ - email: 'someUser@example.org', + beforeEach(() => { + setup.authenticateClient = setupAuthenticateClient({ + email: 'user@example.org', password: '1234' }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + it('throws authorization error', async () => { + await runSetup() + await expect(action()).rejects.toThrow('Not Authorised') }) describe('as moderator', () => { beforeEach(async () => { - await setup({ + setup.authenticateClient = setupAuthenticateClient({ role: 'moderator', email: 'someUser@example.org', password: '1234' }) }) - it('returns true', async () => { - const expected = { enable: true } - await expect(client.request(mutation)).resolves.toEqual(expected) + describe('on a comment', () => { + beforeEach(async () => { + variables = { + id: 'c456', + type: 'comment' + } + + setup.createResource = async () => { + await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' }) + await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for + }) + + await factory.create('Comment', { + id: 'c456' + }) + await Promise.all([ + factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' }), + factory.relate('Comment', 'Post', { from: 'c456', to: 'p9' }) + ]) + + const disableMutation = ` + mutation { + disable(resource: { + id: "c456" + type: comment + }) + } + ` + await factory.mutate(disableMutation) // that's we want to delete + } + }) + + it('returns true', async () => { + const expected = { enable: true } + await runSetup() + await expect(action()).resolves.toEqual(expected) + }) + + it('changes .disabledBy', async () => { + const before = { Comment: [{ id: 'c456', disabledBy: { id: 'u123' } }] } + const expected = { Comment: [{ id: 'c456', disabledBy: null }] } + + await runSetup() + await expect(client.request( + '{ Comment(disabled: true) { id, disabledBy { id } } }' + )).resolves.toEqual(before) + await action() + await expect(client.request( + '{ Comment { id, disabledBy { id } } }' + )).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + const before = { Comment: [ { id: 'c456', disabled: true } ] } + const expected = { Comment: [ { id: 'c456', disabled: false } ] } + + await runSetup() + await expect(client.request( + '{ Comment(disabled: true) { id disabled } }' + )).resolves.toEqual(before) + await action() // this updates .disabled + await expect(client.request( + '{ Comment { id disabled } }' + )).resolves.toEqual(expected) + }) }) - it('changes .disabledBy', async () => { - const before = { Post: [{ id: 'p9', disabledBy: { id: 'u123' } }] } - const expected = { Post: [{ id: 'p9', disabledBy: null }] } + describe('on a post', () => { + beforeEach(async () => { + variables = { + id: 'p9', + type: 'contribution' + } - await expect(client.request( - '{ Post(disabled: true) { id, disabledBy { id } } }' - )).resolves.toEqual(before) - await client.request(mutation) - await expect(client.request( - '{ Post { id, disabledBy { id } } }' - )).resolves.toEqual(expected) - }) + setup.createResource = async () => { + await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' }) + await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for + }) - it('updates .disabled on post', async () => { - const before = { Post: [ { id: 'p9', disabled: true } ] } - const expected = { Post: [ { id: 'p9', disabled: false } ] } + const disableMutation = ` + mutation { + disable(resource: { + id: "p9" + type: contribution + }) + } + ` + await factory.mutate(disableMutation) // that's we want to delete + } + }) - await expect(client.request( - '{ Post(disabled: true) { id disabled } }' - )).resolves.toEqual(before) - await client.request(mutation) // this updates .disabled - await expect(client.request( - '{ Post { id disabled } }' - )).resolves.toEqual(expected) + it('returns true', async () => { + const expected = { enable: true } + await runSetup() + await expect(action()).resolves.toEqual(expected) + }) + + it('changes .disabledBy', async () => { + const before = { Post: [{ id: 'p9', disabledBy: { id: 'u123' } }] } + const expected = { Post: [{ id: 'p9', disabledBy: null }] } + + await runSetup() + await expect(client.request( + '{ Post(disabled: true) { id, disabledBy { id } } }' + )).resolves.toEqual(before) + await action() + await expect(client.request( + '{ Post { id, disabledBy { id } } }' + )).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + const before = { Post: [ { id: 'p9', disabled: true } ] } + const expected = { Post: [ { id: 'p9', disabled: false } ] } + + await runSetup() + await expect(client.request( + '{ Post(disabled: true) { id disabled } }' + )).resolves.toEqual(before) + await action() // this updates .disabled + await expect(client.request( + '{ Post { id disabled } }' + )).resolves.toEqual(expected) + }) }) }) }) From f40a67b7a85c6aa24ea7d005e62cf6ce29fb82ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 6 Mar 2019 17:12:30 +0100 Subject: [PATCH 59/66] Implement disabling of comments+users+posts --- src/resolvers/moderation.js | 16 ++++++++-------- src/schema.graphql | 2 ++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/resolvers/moderation.js b/src/resolvers/moderation.js index 8e171dbf6..10046dd03 100644 --- a/src/resolvers/moderation.js +++ b/src/resolvers/moderation.js @@ -1,28 +1,28 @@ export default { Mutation: { disable: async (object, params, { user, driver }) => { - const { resource: { id: postId } } = params + const { resource: { id } } = params const { id: userId } = user const cypher = ` MATCH (u:User {id: $userId}) - MATCH (p:Post {id: $postId}) - SET p.disabled = true - MERGE (p)<-[:DISABLED]-(u) + MATCH (r {id: $id}) + SET r.disabled = true + MERGE (r)<-[:DISABLED]-(u) ` const session = driver.session() - const res = await session.run(cypher, { postId, userId }) + const res = await session.run(cypher, { id, userId }) session.close() return Boolean(res) }, enable: async (object, params, { user, driver }) => { - const { resource: { id: postId } } = params + const { resource: { id } } = params const cypher = ` - MATCH (p:Post {id: $postId})<-[d:DISABLED]-() + MATCH (p {id: $id})<-[d:DISABLED]-() SET p.disabled = false DELETE d ` const session = driver.session() - const res = await session.run(cypher, { postId }) + const res = await session.run(cypher, { id }) session.close() return Boolean(res) } diff --git a/src/schema.graphql b/src/schema.graphql index ddafd880e..5e58d5f1d 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -76,6 +76,7 @@ type User { avatar: String deleted: Boolean disabled: Boolean + disabledBy: User @relation(name: "DISABLED", direction: "IN") role: UserGroupEnum location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") @@ -165,6 +166,7 @@ type Comment { updatedAt: String deleted: Boolean disabled: Boolean + disabledBy: User @relation(name: "DISABLED", direction: "IN") } type Report { From 80729394586e6923cde23f8480e75dd4d5dcedad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 6 Mar 2019 17:21:49 +0100 Subject: [PATCH 60/66] Tiny performance improvement --- src/resolvers/moderation.js | 4 ++-- src/resolvers/moderation.spec.js | 21 ++++++++------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/resolvers/moderation.js b/src/resolvers/moderation.js index 10046dd03..db44790b9 100644 --- a/src/resolvers/moderation.js +++ b/src/resolvers/moderation.js @@ -17,8 +17,8 @@ export default { enable: async (object, params, { user, driver }) => { const { resource: { id } } = params const cypher = ` - MATCH (p {id: $id})<-[d:DISABLED]-() - SET p.disabled = false + MATCH (r {id: $id})<-[d:DISABLED]-() + SET r.disabled = false DELETE d ` const session = driver.session() diff --git a/src/resolvers/moderation.spec.js b/src/resolvers/moderation.spec.js index 6107074ef..c1d4a75fe 100644 --- a/src/resolvers/moderation.spec.js +++ b/src/resolvers/moderation.spec.js @@ -92,12 +92,10 @@ describe('disable', () => { setup.createResource = async () => { await factory.create('User', { id: 'u45', email: 'commenter@example.org', password: '1234' }) await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' }) - await factory.create('Post', { - id: 'p3' - }) - await factory.create('Comment', { - id: 'c47' - }) + await Promise.all([ + factory.create('Post', { id: 'p3' }), + factory.create('Comment', { id: 'c47' }) + ]) await Promise.all([ factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' }), factory.relate('Comment', 'Post', { from: 'c47', to: 'p3' }) @@ -251,13 +249,10 @@ describe('enable', () => { setup.createResource = async () => { await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' }) await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) - await factory.create('Post', { - id: 'p9' // that's the ID we will look for - }) - - await factory.create('Comment', { - id: 'c456' - }) + await Promise.all([ + factory.create('Post', { id: 'p9' }), + factory.create('Comment', { id: 'c456' }) + ]) await Promise.all([ factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' }), factory.relate('Comment', 'Post', { from: 'c456', to: 'p9' }) From 13d7f677e81cd4da5c4a5c9f35614b9aa1b31250 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Fri, 8 Mar 2019 04:23:50 +0000 Subject: [PATCH 61/66] Bump babel-jest from 24.1.0 to 24.3.1 Bumps [babel-jest](https://github.com/facebook/jest/tree/HEAD/packages/babel-jest) from 24.1.0 to 24.3.1. - [Release notes](https://github.com/facebook/jest/releases) - [Changelog](https://github.com/facebook/jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/facebook/jest/commits/v24.3.1/packages/babel-jest) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 261 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 246 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index a0f745bf7..a182fb2b7 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "apollo-server-testing": "~2.4.8", "babel-core": "~7.0.0-0", "babel-eslint": "~10.0.1", - "babel-jest": "~24.1.0", + "babel-jest": "~24.3.1", "chai": "~4.2.0", "eslint": "~5.15.1", "eslint-config-standard": "~12.0.0", diff --git a/yarn.lock b/yarn.lock index 215a608ab..1b0ec799d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -299,7 +299,7 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.3.tgz#092d450db02bdb6ccb1ca8ffd47d8774a91aef87" integrity sha512-xsH1CJoln2r74hR+y7cg2B5JCPaTh+Hd+EbBRk9nWGSNspuo6krjhX0Om6RnRQuIvFq8wVXCLKH3kwKDYhanSg== -"@babel/parser@^7.3.4": +"@babel/parser@^7.1.0", "@babel/parser@^7.3.4": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c" integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ== @@ -721,7 +721,7 @@ lodash "^4.17.11" to-fast-properties "^2.0.0" -"@babel/types@^7.3.4": +"@babel/types@^7.3.0", "@babel/types@^7.3.4": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed" integrity sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ== @@ -730,6 +730,81 @@ 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.3.0": + version "24.3.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.3.0.tgz#7bd920d250988ba0bf1352c4493a48e1cb97671e" + integrity sha512-NaCty/OOei6rSDcbPdMiCbYCI0KGFGPgGO6B09lwWt5QTxnkuhKYET9El5u5z1GAcSxkQmSMtM63e24YabCWqA== + dependencies: + "@jest/source-map" "^24.3.0" + "@types/node" "*" + chalk "^2.0.1" + slash "^2.0.0" + +"@jest/fake-timers@^24.3.0": + version "24.3.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.3.0.tgz#0a7f8b877b78780c3fa5c3f8683cc0aaf9488331" + integrity sha512-rHwVI17dGMHxHzfAhnZ04+wFznjFfZ246QugeBnbiYr7/bDosPD2P1qeNjWnJUUcfl0HpS6kkr+OB/mqSJxQFg== + dependencies: + "@jest/types" "^24.3.0" + "@types/node" "*" + jest-message-util "^24.3.0" + jest-mock "^24.3.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.3.0": + version "24.3.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.3.0.tgz#4c0b1c9716212111920f7cf8c4329c69bc81924a" + integrity sha512-j7UZ49T8C4CVipEY99nLttnczVTtLyVzFfN20OiBVn7awOs0U3endXSTq7ouPrLR5y4YjI5GDcbcvDUjgeamzg== + dependencies: + "@jest/console" "^24.3.0" + "@jest/types" "^24.3.0" + "@types/istanbul-lib-coverage" "^1.1.0" + +"@jest/transform@^24.3.1": + version "24.3.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.3.1.tgz#ce9e1329eb5e640f493bcd5c8eb9970770959bfc" + integrity sha512-PpjylI5goT4Si69+qUjEeHuKjex0LjjrqJzrMYzlOZn/+SCumGKuGC0UQFeEPThyGsFvWH1Q4gj0R66eOHnIpw== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^24.3.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.3.1" + jest-regex-util "^24.3.0" + jest-util "^24.3.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.3.0": + version "24.3.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.3.0.tgz#3f6e117e47248a9a6b5f1357ec645bd364f7ad23" + integrity sha512-VoO1F5tU2n/93QN/zaZ7Q8SeV/Rj+9JJOgbvKbBwy4lenvmdj1iDaQEPXGTKrO6OSvDeb2drTFipZJYxgo6kIQ== + dependencies: + "@types/istanbul-lib-coverage" "^1.1.0" + "@types/yargs" "^12.0.9" + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -790,6 +865,39 @@ dependencies: "@types/node" "*" +"@types/babel__core@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.0.tgz#710f2487dda4dcfd010ca6abb2b4dc7394365c51" + integrity sha512-wJTeJRt7BToFx3USrCDs2BhEi4ijBInTQjOIukj6a/5tEkwpFMVZ+1ppgmE+Q/FQyc5P/VWUbx7I9NELrKruHA== + 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.6" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.6.tgz#328dd1a8fc4cfe3c8458be9477b219ea158fd7b2" + integrity sha512-XYVgHF2sQ0YblLRMLNPB3CkFMewzFmlDsH/TneZFHUXDlABQgh88uOxuez7ZcXxayLFrqLwtDH1t+FmlFwNZxw== + dependencies: + "@babel/types" "^7.3.0" + "@types/body-parser@*", "@types/body-parser@1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c" @@ -854,6 +962,11 @@ resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.0.3.tgz#389e2e5b83ecdb376d9f98fae2094297bc112c1c" integrity sha512-TcFkpEjcQK7w8OcrQcd7iIBPjU0rdyi3ldj6d0iJ4PPSzbWqPBvXj9KSwO14hTOX2dm9RoiH7VuxksJLNYdXUQ== +"@types/istanbul-lib-coverage@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.0.tgz#2cc2ca41051498382b43157c8227fea60363f94a" + integrity sha512-ohkhb9LehJy+PA40rDtGAji61NCgdtKLAlFoYp4cnuuQEswwdK3vz9SOIkkyc3wrk8dzjphQApNs56yyXLStaQ== + "@types/long@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef" @@ -882,6 +995,11 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@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/ws@^6.0.0": version "6.0.1" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.1.tgz#ca7a3f3756aa12f62a0a62145ed14c6db25d5a28" @@ -890,6 +1008,11 @@ "@types/events" "*" "@types/node" "*" +"@types/yargs@^12.0.9": + version "12.0.9" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0" + integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA== + "@types/zen-observable@^0.5.3": version "0.5.4" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.5.4.tgz#b863a4191e525206819e008097ebf0fb2e3a1cdc" @@ -1494,13 +1617,16 @@ babel-eslint@~10.0.1: eslint-scope "3.7.1" eslint-visitor-keys "^1.0.0" -babel-jest@^24.1.0, 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.1.0, babel-jest@~24.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.3.1.tgz#168468a37e90426520c5293da4f55e1a512063b0" + integrity sha512-6KaXyUevY0KAxD5Ba+EBhyfwvc+R2f7JV7BpBZ5T8yJGgj0M1hYDfRhDq35oD5MzprMf/ggT81nEuLtMyxfDIg== dependencies: + "@jest/transform" "^24.3.1" + "@jest/types" "^24.3.0" + "@types/babel__core" "^7.1.0" babel-plugin-istanbul "^5.1.0" - babel-preset-jest "^24.1.0" + babel-preset-jest "^24.3.0" chalk "^2.4.2" slash "^2.0.0" @@ -1513,18 +1639,20 @@ babel-plugin-istanbul@^5.1.0: istanbul-lib-instrument "^3.0.0" test-exclude "^5.0.0" -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.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.3.0.tgz#f2e82952946f6e40bb0a75d266a3790d854c8b5b" + integrity sha512-nWh4N1mVH55Tzhx2isvUN5ebM5CDUvIpXPZYMRazQughie/EqGnbR+czzoQlhUmJG9pPJmYDRhvocotb2THl1w== + 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.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.3.0.tgz#db88497e18869f15b24d9c0e547d8e0ab950796d" + integrity sha512-VGTV2QYBa/Kn3WCOKdfS31j9qomaXSgJqi65B6o05/1GsJyj9LVhSljM9ro4S+IBGj/ENhNBuH9bpqzztKAQSw== dependencies: "@babel/plugin-syntax-object-rest-spread" "^7.0.0" - babel-plugin-jest-hoist "^24.1.0" + babel-plugin-jest-hoist "^24.3.0" babel-runtime@^6.26.0: version "6.26.0" @@ -2695,6 +2823,11 @@ exec-sh@^0.2.0: 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@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" @@ -4124,6 +4257,21 @@ jest-haste-map@^24.0.0: micromatch "^3.1.10" sane "^3.0.0" +jest-haste-map@^24.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.3.1.tgz#b4a66dbe1e6bc45afb9cd19c083bff81cdd535a1" + integrity sha512-OTMQle+astr1lWKi62Ccmk2YWn6OtUoU/8JpJdg8zdsnpFIry/k0S4sQ4nWocdM07PFdvqcthWc78CkCE6sXvA== + dependencies: + "@jest/types" "^24.3.0" + fb-watchman "^2.0.0" + graceful-fs "^4.1.15" + invariant "^2.2.4" + jest-serializer "^24.3.0" + jest-util "^24.3.0" + jest-worker "^24.3.1" + micromatch "^3.1.10" + sane "^4.0.3" + jest-jasmine2@^24.1.0: version "24.1.0" resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.1.0.tgz#8377324b967037c440f0a549ee0bbd9912055db6" @@ -4170,16 +4318,42 @@ jest-message-util@^24.0.0: slash "^2.0.0" stack-utils "^1.0.1" +jest-message-util@^24.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.3.0.tgz#e8f64b63ebc75b1a9c67ee35553752596e70d4a9" + integrity sha512-lXM0YgKYGqN5/eH1NGw4Ix+Pk2I9Y77beyRas7xM24n+XTTK3TbT0VkT3L/qiyS7WkW0YwyxoXnnAaGw4hsEDA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/test-result" "^24.3.0" + "@jest/types" "^24.3.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.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.3.0.tgz#95a86b6ad474e3e33227e6dd7c4ff6b07e18d3cb" + integrity sha512-AhAo0qjbVWWGvcbW5nChFjR0ObQImvGtU6DodprNziDOt+pP0CBdht/sYcNIOXeim8083QUi9bC8QdKB8PTK4Q== + dependencies: + "@jest/types" "^24.3.0" + 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.1.0: version "24.1.0" resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.1.0.tgz#78f738a2ec59ff4d00751d9da56f176e3f589f6c" @@ -4250,6 +4424,11 @@ jest-serializer@^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.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.3.0.tgz#074e307300d1451617cf2630d11543ee4f74a1c8" + integrity sha512-RiSpqo2OFbVLJN/PgAOwQIUeHDfss6NBUDTLhjiJM8Bb5rMrwRqHfkaqahIsOf9cXXB5UjcqDCzbQ7AIoMqWkg== + jest-snapshot@^24.1.0: version "24.1.0" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.1.0.tgz#85e22f810357aa5994ab61f236617dc2205f2f5b" @@ -4280,6 +4459,25 @@ jest-util@^24.0.0: slash "^2.0.0" source-map "^0.6.0" +jest-util@^24.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.3.0.tgz#a549ae9910fedbd4c5912b204bb1bcc122ea0057" + integrity sha512-eKIAC+MTKWZthUUVOwZ3Tc5a0cKMnxalQHr6qZ4kPzKn6k09sKvsmjCygqZ1SxVVfUKoa8Sfn6XDv9uTJ1iXTg== + dependencies: + "@jest/console" "^24.3.0" + "@jest/fake-timers" "^24.3.0" + "@jest/source-map" "^24.3.0" + "@jest/test-result" "^24.3.0" + "@jest/types" "^24.3.0" + "@types/node" "*" + callsites "^3.0.0" + chalk "^2.0.1" + graceful-fs "^4.1.15" + is-ci "^2.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" @@ -4309,6 +4507,15 @@ jest-worker@^24.0.0: merge-stream "^1.0.1" supports-color "^6.1.0" +jest-worker@^24.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.3.1.tgz#c1759dd2b1d5541b09a2e5e1bc3288de6c9d8632" + integrity sha512-ZCoAe/iGLzTJvWHrO8fyx3bmEQhpL16SILJmWHKe8joHhyF3z00psF1sCRT54DoHw5GJG0ZpUtGy+ylvwA4haA== + dependencies: + "@types/node" "*" + merge-stream "^1.0.1" + supports-color "^6.1.0" + jest@~24.1.0: version "24.1.0" resolved "https://registry.yarnpkg.com/jest/-/jest-24.1.0.tgz#b1e1135caefcf2397950ecf7f90e395fde866fd2" @@ -5818,6 +6025,13 @@ realpath-native@^1.0.0, realpath-native@^1.0.2: dependencies: util.promisify "^1.0.0" +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" + 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" @@ -6099,6 +6313,21 @@ sane@^3.0.0: optionalDependencies: fsevents "^1.2.3" +sane@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.0.3.tgz#e878c3f19e25cc57fbb734602f48f8a97818b181" + integrity sha512-hSLkC+cPHiBQs7LSyXkotC3UUtyn8C4FMn50TNaacRyvBlI+3ebcxMpqckmTdtXVtel87YS7GXN3UIOj7NiGVQ== + dependencies: + "@cnakazawa/watch" "^1.0.3" + anymatch "^2.0.0" + capture-exit "^1.2.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" + sanitize-html@~1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.20.0.tgz#9a602beb1c9faf960fb31f9890f61911cc4d9156" From dfc28acfe5ac854ac9dfe14c4601a025a0bc931d Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Fri, 8 Mar 2019 11:56:15 +0100 Subject: [PATCH 62/66] Do nodemon restart on .js and .graphql files --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a182fb2b7..f18eed87a 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "scripts": { "build": "babel src/ -d dist/ --copy-files", "start": "node dist/", - "dev": "nodemon --exec babel-node src/", - "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js", + "dev": "nodemon --exec babel-node src/ -e js,graphql", + "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,graphql", "lint": "eslint src --config .eslintrc.js", "test": "nyc --reporter=text-lcov yarn run test:jest", "test:cypress": "run-p --race test:before:*", From c8ba2f6558956464f7eb8b278e2933830d0e2f21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Fri, 8 Mar 2019 10:59:31 +0000 Subject: [PATCH 63/66] Bump jest from 24.1.0 to 24.3.1 Bumps [jest](https://github.com/facebook/jest) from 24.1.0 to 24.3.1. - [Release notes](https://github.com/facebook/jest/releases) - [Changelog](https://github.com/facebook/jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/facebook/jest/compare/v24.1.0...v24.3.1) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 612 ++++++++++++++++++++++++--------------------------- 2 files changed, 294 insertions(+), 320 deletions(-) diff --git a/package.json b/package.json index f18eed87a..671e3406b 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "eslint-plugin-promise": "~4.0.1", "eslint-plugin-standard": "~4.0.0", "graphql-request": "~1.8.2", - "jest": "~24.1.0", + "jest": "~24.3.1", "nodemon": "~1.18.10", "nyc": "~13.3.0", "supertest": "~3.4.2" diff --git a/yarn.lock b/yarn.lock index 1b0ec799d..3ee900990 100644 --- a/yarn.lock +++ b/yarn.lock @@ -748,6 +748,50 @@ chalk "^2.0.1" slash "^2.0.0" +"@jest/core@^24.3.1": + version "24.3.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.3.1.tgz#9811596d9fcc6dbb3d4062c67e4c4867bc061585" + integrity sha512-orucOIBKfXgm1IJirtPT0ToprqDVGYKUNJKNc9a6v1Lww6qLPq+xj5OfxyhpJb2rWOgzEkATW1bfZzg3oqV70w== + dependencies: + "@jest/console" "^24.3.0" + "@jest/reporters" "^24.3.1" + "@jest/test-result" "^24.3.0" + "@jest/transform" "^24.3.1" + "@jest/types" "^24.3.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-changed-files "^24.3.0" + jest-config "^24.3.1" + jest-haste-map "^24.3.1" + jest-message-util "^24.3.0" + jest-regex-util "^24.3.0" + jest-resolve-dependencies "^24.3.1" + jest-runner "^24.3.1" + jest-runtime "^24.3.1" + jest-snapshot "^24.3.1" + jest-util "^24.3.0" + jest-validate "^24.3.1" + jest-watcher "^24.3.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.3.1": + version "24.3.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.3.1.tgz#1fbda3ec8fb8ffbaee665d314da91d662227e11e" + integrity sha512-M8bqEkQqPwZVhMMFMqqCnzqIZtuM5vDMfFQ9ZvnEfRT+2T1zTA4UAOH/V4HagEi6S3BCd/mdxFdYmPgXf7GKCA== + dependencies: + "@jest/fake-timers" "^24.3.0" + "@jest/transform" "^24.3.1" + "@jest/types" "^24.3.0" + "@types/node" "*" + jest-mock "^24.3.0" + "@jest/fake-timers@^24.3.0": version "24.3.0" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.3.0.tgz#0a7f8b877b78780c3fa5c3f8683cc0aaf9488331" @@ -758,6 +802,32 @@ jest-message-util "^24.3.0" jest-mock "^24.3.0" +"@jest/reporters@^24.3.1": + version "24.3.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.3.1.tgz#68e4abc8d4233acd0dd87287f3bd270d81066248" + integrity sha512-jEIDJcvk20ReUW1Iqb+prlAcFV+kfFhQ/01poCq8X9As7/l/2y1GqVwJ3+6SaPTZuCXh0d0LVDy86zDAa8zlVA== + dependencies: + "@jest/environment" "^24.3.1" + "@jest/test-result" "^24.3.0" + "@jest/transform" "^24.3.1" + "@jest/types" "^24.3.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.2" + istanbul-api "^2.1.1" + istanbul-lib-coverage "^2.0.2" + istanbul-lib-instrument "^3.0.1" + istanbul-lib-source-maps "^3.0.1" + jest-haste-map "^24.3.1" + jest-resolve "^24.3.1" + jest-runtime "^24.3.1" + jest-util "^24.3.0" + jest-worker "^24.3.1" + 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" @@ -1008,7 +1078,7 @@ "@types/events" "*" "@types/node" "*" -"@types/yargs@^12.0.9": +"@types/yargs@^12.0.2", "@types/yargs@^12.0.9": version "12.0.9" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0" integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA== @@ -1617,7 +1687,7 @@ babel-eslint@~10.0.1: eslint-scope "3.7.1" eslint-visitor-keys "^1.0.0" -babel-jest@^24.1.0, babel-jest@~24.3.1: +babel-jest@^24.3.1, babel-jest@~24.3.1: version "24.3.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.3.1.tgz#168468a37e90426520c5293da4f55e1a512063b0" integrity sha512-6KaXyUevY0KAxD5Ba+EBhyfwvc+R2f7JV7BpBZ5T8yJGgj0M1hYDfRhDq35oD5MzprMf/ggT81nEuLtMyxfDIg== @@ -2415,10 +2485,10 @@ 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== doctrine@1.5.0: version "1.5.0" @@ -2816,13 +2886,6 @@ events@1.1.1: resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= -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" @@ -2872,16 +2935,17 @@ 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.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-24.3.1.tgz#7c42507da231a91a8099d065bc8dc9322dc85fc0" + integrity sha512-xnmobSlaqhg4FKqjb5REk4AobQzFMJoctDdREKfSGqrtzRfCWYbfqt3WmikAvQz/J8mCNQhORgYdEjPMJbMQPQ== dependencies: + "@jest/types" "^24.3.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.3.0" + jest-matcher-utils "^24.3.1" + jest-message-util "^24.3.0" + jest-regex-util "^24.3.0" express@^4.0.0, express@^4.16.3: version "4.16.4" @@ -3166,14 +3230,6 @@ 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.4" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" - integrity sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg== - dependencies: - nan "^2.9.2" - node-pre-gyp "^0.10.0" - fsevents@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4" @@ -4045,10 +4101,10 @@ 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== +istanbul-api@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-2.1.1.tgz#194b773f6d9cbc99a9258446848b0f988951c4d0" + integrity sha512-kVmYrehiwyeBAk/wE71tW6emzLiHGjYIiDrc8sfyty4F8M02/lrgXSm+R1kXysmF20zArvmZXjlE/mg24TVPJw== dependencies: async "^2.6.1" compare-versions "^3.2.1" @@ -4058,7 +4114,7 @@ istanbul-api@^2.0.8: istanbul-lib-instrument "^3.1.0" istanbul-lib-report "^2.0.4" istanbul-lib-source-maps "^3.0.2" - istanbul-reports "^2.1.0" + istanbul-reports "^2.1.1" js-yaml "^3.12.0" make-dir "^1.3.0" minimatch "^3.0.4" @@ -4109,7 +4165,7 @@ istanbul-lib-source-maps@^3.0.1, istanbul-lib-source-maps@^3.0.2: rimraf "^2.6.2" source-map "^0.6.1" -istanbul-reports@^2.1.0, istanbul-reports@^2.1.1: +istanbul-reports@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.1.1.tgz#72ef16b4ecb9a4a7bd0e2001e00f95d1eec8afa9" integrity sha512-FzNahnidyEPBCI0HcufJoSEoKykesRlFcSzQqjH9x0+LC8tnnE/p/90PBLu8iZTxr8yYZNyTtiAujUqyN+CIxw== @@ -4121,141 +4177,111 @@ iterall@^1.1.3, iterall@^1.2.1, iterall@^1.2.2: resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== -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.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.3.0.tgz#7050ae29aaf1d59437c80f21d5b3cd354e88a499" + integrity sha512-fTq0YAUR6644fgsqLC7Zi2gXA/bAplMRvfXQdutmkwgrCKK6upkj+sgXqsUfUZRm15CVr3YSojr/GRNn71IMvg== dependencies: + "@jest/types" "^24.3.0" execa "^1.0.0" throat "^4.0.0" -jest-cli@^24.1.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== +jest-cli@^24.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.3.1.tgz#52e4ae5f11044b41e06ca39fc7a7302fbbcb1661" + integrity sha512-HdwMgigvDQdlWX7gwM2QMkJJRqSk7tTYKq7kVplblK28RarqquJMWV/lOCN8CukuG9u3DZTeXpCDXR7kpGfB3w== dependencies: - ansi-escapes "^3.0.0" + "@jest/core" "^24.3.1" + "@jest/test-result" "^24.3.0" + "@jest/types" "^24.3.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" + jest-config "^24.3.1" + jest-util "^24.3.0" + jest-validate "^24.3.1" 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-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.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.3.1.tgz#271aff2d3aeabf1ff92512024eeca3323cd31a07" + integrity sha512-ujHQywsM//vKFvJwEC02KNZgKAGOzGz1bFPezmTQtuj8XdfsAVq8p6N/dw4yodXV11gSf6TJ075i4ehM+mKatA== dependencies: "@babel/core" "^7.1.0" - babel-jest "^24.1.0" + "@jest/types" "^24.3.0" + babel-jest "^24.3.1" 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.3.1" + jest-environment-node "^24.3.1" + jest-get-type "^24.3.0" + jest-jasmine2 "^24.3.1" + jest-regex-util "^24.3.0" + jest-resolve "^24.3.1" + jest-util "^24.3.0" + jest-validate "^24.3.1" micromatch "^3.1.10" - pretty-format "^24.0.0" - realpath-native "^1.0.2" + pretty-format "^24.3.1" + 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.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.3.1.tgz#87952e5ea1548567da91df398fa7bf7977d3f96a" + integrity sha512-YRVzDguyzShP3Pb9wP/ykBkV7Z+O4wltrMZ2P4LBtNxrHNpxwI2DECrpD9XevxWubRy5jcE8sSkxyX3bS7W+rA== 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.3.0" + pretty-format "^24.3.1" -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.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.3.1.tgz#ed8fe8b9f92a835a6625ca8c7ee06bc904440316" + integrity sha512-GTi+nxDaWwSgOPLiiqb/p4LURy0mv3usoqsA2eoTYSmRsLgjgZ6VUyRpUBH5JY9EMBx33suNFXk0iyUm29WRpw== dependencies: + "@jest/types" "^24.3.0" chalk "^2.0.1" - jest-get-type "^24.0.0" - jest-util "^24.0.0" - pretty-format "^24.0.0" + jest-get-type "^24.3.0" + jest-util "^24.3.0" + pretty-format "^24.3.1" -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== +jest-environment-jsdom@^24.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.3.1.tgz#49826bcf12fb3e38895f1e2aaeb52bde603cc2e4" + integrity sha512-rz2OSYJiQerDqWDwjisqRwhVNpwkqFXdtyMzEuJ47Ip9NRpRQ+qy7/+zFujPUy/Z+zjWRO5seHLB/dOD4VpEVg== dependencies: - jest-mock "^24.0.0" - jest-util "^24.0.0" + "@jest/environment" "^24.3.1" + "@jest/fake-timers" "^24.3.0" + "@jest/types" "^24.3.0" + jest-mock "^24.3.0" + jest-util "^24.3.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.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.3.1.tgz#333d864c569b27658a96bb3b10e02e7172125415" + integrity sha512-Xy+/yFem/yUs9OkzbcawQT237vwDjBhAVLjac1KYAMYVjGb0Vb/Ovw4g61PunVdrEIpfcXNtRUltM4+9c7lARQ== dependencies: - jest-mock "^24.0.0" - jest-util "^24.0.0" + "@jest/environment" "^24.3.1" + "@jest/fake-timers" "^24.3.0" + "@jest/types" "^24.3.0" + jest-mock "^24.3.0" + jest-util "^24.3.0" -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-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== - dependencies: - 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" - micromatch "^3.1.10" - sane "^3.0.0" +jest-get-type@^24.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.3.0.tgz#582cfd1a4f91b5cdad1d43d2932f816d543c65da" + integrity sha512-HYF6pry72YUlVcvUx3sEpMRwXEWGEPlJ0bSPVnB3b3n++j4phUEoSPcS6GC0pPJ9rpyPSe4cb5muFo6D39cXow== jest-haste-map@^24.3.1: version "24.3.1" @@ -4272,51 +4298,44 @@ jest-haste-map@^24.3.1: micromatch "^3.1.10" sane "^4.0.3" -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.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.3.1.tgz#127d628d3ac0829bd3c0fccacb87193e543b420b" + integrity sha512-STo6ar1IyPlIPq9jPxDQhM7lC0dAX7KKN0LmCLMlgJeXwX+1XiVdtZDv1a4zyg6qhNdpo1arOBGY0BcovUK7ug== dependencies: "@babel/traverse" "^7.1.0" + "@jest/environment" "^24.3.1" + "@jest/test-result" "^24.3.0" + "@jest/types" "^24.3.0" chalk "^2.0.1" co "^4.6.0" - expect "^24.1.0" + expect "^24.3.1" 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.3.1" + jest-matcher-utils "^24.3.1" + jest-message-util "^24.3.0" + jest-runtime "^24.3.1" + jest-snapshot "^24.3.1" + jest-util "^24.3.0" + pretty-format "^24.3.1" 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.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.3.1.tgz#ed89d05ca07e91b2b51dac1f676ab354663aa8da" + integrity sha512-GncRwEtAw/SohdSyY4bk2RE06Ac1dZrtQGZQ2j35hSuN4gAAAKSYMszJS2WDixsAEaFN+GHBHG+d8pjVGklKyw== dependencies: - pretty-format "^24.0.0" + pretty-format "^24.3.1" -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.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.3.1.tgz#025e1cd9c54a5fde68e74b12428775d06d123aa8" + integrity sha512-P5VIsUTJeI0FYvWVMwEHjxK1L83vEkDiKMV0XFPIrT2jzWaWPB2+dPCHkP2ID9z4eUKElaHqynZnJiOdNVHfXQ== dependencies: chalk "^2.0.1" - jest-diff "^24.0.0" - jest-get-type "^24.0.0" - pretty-format "^24.0.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== - dependencies: - "@babel/code-frame" "^7.0.0" - chalk "^2.0.1" - micromatch "^3.1.10" - slash "^2.0.0" - stack-utils "^1.0.1" + jest-diff "^24.3.1" + jest-get-type "^24.3.0" + pretty-format "^24.3.1" jest-message-util@^24.3.0: version "24.3.0" @@ -4332,11 +4351,6 @@ jest-message-util@^24.3.0: 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.3.0: version "24.3.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.3.0.tgz#95a86b6ad474e3e33227e6dd7c4ff6b07e18d3cb" @@ -4344,121 +4358,107 @@ jest-mock@^24.3.0: dependencies: "@jest/types" "^24.3.0" -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.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.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.3.1.tgz#a22839d611ba529a74594ee274ce2b77d046bea9" + integrity sha512-9JUejNImGnJjbNR/ttnod+zQIWANpsrYMPt18s2tYGK6rP191qFsyEQ2BhAQMdYDRkTmi8At+Co9tL+jTPqdpw== dependencies: - jest-regex-util "^24.0.0" - jest-snapshot "^24.1.0" + "@jest/types" "^24.3.0" + jest-regex-util "^24.3.0" + jest-snapshot "^24.3.1" -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.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.3.1.tgz#103dbd438b59618ea428ec4acbd65c56495ba397" + integrity sha512-N+Q3AcVuKxpn/kjQMxUVLwBk32ZE1diP4MPcHyjVwcKpCUuKrktfRR3Mqe/T2HoD25wyccstaqcPUKIudl41bg== dependencies: + "@jest/types" "^24.3.0" browser-resolve "^1.11.3" chalk "^2.0.1" - realpath-native "^1.0.0" + realpath-native "^1.1.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== +jest-runner@^24.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.3.1.tgz#5488566fa60cdb4b00a89c734ad6b54b9561415d" + integrity sha512-Etc9hQ5ruwg+q7DChm+E8qzHHdNTLeUdlo+whPQRSpNSgl0AEgc2r2mT4lxODREqmnHg9A8JHA44pIG4GE0Gzg== dependencies: + "@jest/console" "^24.3.0" + "@jest/environment" "^24.3.1" + "@jest/test-result" "^24.3.0" + "@jest/types" "^24.3.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.3.1" + jest-docblock "^24.3.0" + jest-haste-map "^24.3.1" + jest-jasmine2 "^24.3.1" + jest-leak-detector "^24.3.1" + jest-message-util "^24.3.0" + jest-resolve "^24.3.1" + jest-runtime "^24.3.1" + jest-util "^24.3.0" + jest-worker "^24.3.1" source-map-support "^0.5.6" throat "^4.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== +jest-runtime@^24.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.3.1.tgz#2798230b4fbed594b375a13e395278694d4751e2" + integrity sha512-Qz/tJWbZ2naFJ2Kvy1p+RhhRgsPYh4e6wddVRy6aHBr32FTt3Ja33bfV7pkMFWXFbVuAsJMJVdengbvdhWzq4A== dependencies: - "@babel/core" "^7.1.0" - babel-plugin-istanbul "^5.1.0" + "@jest/console" "^24.3.0" + "@jest/environment" "^24.3.1" + "@jest/source-map" "^24.3.0" + "@jest/transform" "^24.3.1" + "@jest/types" "^24.3.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.3.1" + jest-haste-map "^24.3.1" + jest-message-util "^24.3.0" + jest-mock "^24.3.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.3.1" + jest-snapshot "^24.3.1" + jest-util "^24.3.0" + jest-validate "^24.3.1" + 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.3.0: version "24.3.0" resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.3.0.tgz#074e307300d1451617cf2630d11543ee4f74a1c8" integrity sha512-RiSpqo2OFbVLJN/PgAOwQIUeHDfss6NBUDTLhjiJM8Bb5rMrwRqHfkaqahIsOf9cXXB5UjcqDCzbQ7AIoMqWkg== -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.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.3.1.tgz#0f22a86c1b8c87e823f5ad095e82c19d9ed93d72" + integrity sha512-7wbNJWh0sBjmoaexTOWqS7nleTQME7o2W9XKU6CHCxG49Thjct4aVPC/QPNF5NHnvf4M/VDmudIDbwz6noJTRA== dependencies: "@babel/types" "^7.0.0" + "@jest/types" "^24.3.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.3.1" + jest-diff "^24.3.1" + jest-matcher-utils "^24.3.1" + jest-message-util "^24.3.0" + jest-resolve "^24.3.1" mkdirp "^0.5.1" natural-compare "^1.4.0" - pretty-format "^24.0.0" + pretty-format "^24.3.1" 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== - dependencies: - 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-util@^24.3.0: version "24.3.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.3.0.tgz#a549ae9910fedbd4c5912b204bb1bcc122ea0057" @@ -4478,35 +4478,32 @@ jest-util@^24.3.0: 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.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.3.1.tgz#9359eea5a767a3d20b4fa7a5764fd78330ba8312" + integrity sha512-ww3+IPNCOEMi1oKlrHdSnBXetXtdrrdSh0bqLNTVkWglduhORf94RJWd1ko9oEPU2TcEQS5QIPacYziQIUzc4A== dependencies: + "@jest/types" "^24.3.0" camelcase "^5.0.0" chalk "^2.0.1" - jest-get-type "^24.0.0" + jest-get-type "^24.3.0" leven "^2.1.0" - pretty-format "^24.0.0" + pretty-format "^24.3.1" -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.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.3.0.tgz#ee51c6afbe4b35a12fcf1107556db6756d7b9290" + integrity sha512-EpJS/aUG8D3DMuy9XNA4fnkKWy3DQdoWhY92ZUdlETIeEn1xya4Np/96MBSh4II5YvxwKe6JKwbu3Bnzfwa7vA== dependencies: + "@jest/test-result" "^24.3.0" + "@jest/types" "^24.3.0" + "@types/node" "*" + "@types/yargs" "^12.0.9" ansi-escapes "^3.0.0" chalk "^2.0.1" - jest-util "^24.0.0" + jest-util "^24.3.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== - dependencies: - merge-stream "^1.0.1" - supports-color "^6.1.0" - jest-worker@^24.3.1: version "24.3.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.3.1.tgz#c1759dd2b1d5541b09a2e5e1bc3288de6c9d8632" @@ -4516,13 +4513,13 @@ jest-worker@^24.3.1: merge-stream "^1.0.1" supports-color "^6.1.0" -jest@~24.1.0: - version "24.1.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-24.1.0.tgz#b1e1135caefcf2397950ecf7f90e395fde866fd2" - integrity sha512-+q91L65kypqklvlRFfXfdzUKyngQLOcwGhXQaLmVHv+d09LkNXuBuGxlofTFW42XMzu3giIcChchTsCNUjQ78A== +jest@~24.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-24.3.1.tgz#81959de0d57b2df923510f4fafe266712d37dcca" + integrity sha512-SqZguEbYNcZ3r0KUUBN+IkKfyPS1VBbIUiK4Wrc0AiGUR52gJa0fmlWSOCL3x25908QrfoQwkVDu5jCsfXb2ig== dependencies: import-local "^2.0.0" - jest-cli "^24.1.0" + jest-cli "^24.3.1" jmespath@0.15.0: version "0.15.0" @@ -4976,11 +4973,6 @@ merge-stream@^1.0.1: dependencies: readable-stream "^2.0.1" -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.1, methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -5726,6 +5718,13 @@ pirates@^4.0.0: dependencies: node-modules-regexp "^1.0.0" +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" + pkg-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" @@ -5769,13 +5768,15 @@ prepend-http@^1.0.1: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= -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.3.1: + version "24.3.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.3.1.tgz#ae4a98e93d73d86913a8a7dd1a7c3c900f8fda59" + integrity sha512-NZGH1NWS6o4i9pvRWLsxIK00JB9pqOUzVrO7yWT6vjI2thdxwvxefBJO6O5T24UAhI8P5dMceZ7x5wphgVI7Mg== dependencies: + "@jest/types" "^24.3.0" ansi-regex "^4.0.0" ansi-styles "^3.2.0" + react-is "^16.8.4" private@^0.1.6: version "0.1.8" @@ -5933,6 +5934,11 @@ react-dom@^16.4.2: prop-types "^15.6.2" scheduler "^0.11.2" +react-is@^16.8.4: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" + integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== + react@^16.4.2: version "16.6.3" resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c" @@ -6018,13 +6024,6 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" -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== - dependencies: - util.promisify "^1.0.0" - realpath-native@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" @@ -6296,23 +6295,6 @@ 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== - dependencies: - anymatch "^2.0.0" - capture-exit "^1.2.0" - exec-sh "^0.2.0" - 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" - sane@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/sane/-/sane-4.0.3.tgz#e878c3f19e25cc57fbb734602f48f8a97818b181" @@ -7273,14 +7255,6 @@ walker@~1.0.5: 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@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -7326,7 +7300,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== From f86fc7507c1cf1f487f7ecce418076e153cf43de Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Fri, 8 Mar 2019 16:49:43 +0100 Subject: [PATCH 64/66] Fixed shout and follow seeding --- src/seed/factories/index.js | 26 ++++++++++++++++ src/seed/seed-db.js | 59 +++++++++++++++++++++++++++---------- 2 files changed, 70 insertions(+), 15 deletions(-) diff --git a/src/seed/factories/index.js b/src/seed/factories/index.js index 68dd99200..2629ce8b6 100644 --- a/src/seed/factories/index.js +++ b/src/seed/factories/index.js @@ -90,6 +90,32 @@ export default function Factory (options = {}) { this.lastResponse = await this.graphQLClient.request(mutation, variables) return this }, + async shout (properties) { + const { id, type } = properties + const mutation = ` + mutation { + shout( + id: "${id}", + type: ${type} + ) + } + ` + this.lastResponse = await this.graphQLClient.request(mutation) + return this + }, + async follow (properties) { + const { id, type } = properties + const mutation = ` + mutation { + follow( + id: "${id}", + type: ${type} + ) + } + ` + this.lastResponse = await this.graphQLClient.request(mutation) + return this + }, async cleanDatabase () { this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }) return this diff --git a/src/seed/seed-db.js b/src/seed/seed-db.js index 310089ef7..51a456126 100644 --- a/src/seed/seed-db.js +++ b/src/seed/seed-db.js @@ -23,6 +23,15 @@ import Factory from './factories' f.create('User', { id: 'u7', name: 'Dagobert', role: 'user', email: 'dagobert@example.org' }) ]) + const [ asAdmin, asModerator, asUser, asTick, asTrick, asTrack ] = await Promise.all([ + Factory().authenticateAs({ email: 'admin@example.org', password: '1234' }), + Factory().authenticateAs({ email: 'moderator@example.org', password: '1234' }), + Factory().authenticateAs({ email: 'user@example.org', password: '1234' }), + Factory().authenticateAs({ email: 'tick@example.org', password: '1234' }), + Factory().authenticateAs({ email: 'trick@example.org', password: '1234' }), + Factory().authenticateAs({ email: 'track@example.org', password: '1234' }) + ]) + await Promise.all([ f.relate('User', 'Badges', { from: 'b6', to: 'u1' }), f.relate('User', 'Badges', { from: 'b5', to: 'u2' }), @@ -30,12 +39,6 @@ import Factory from './factories' f.relate('User', 'Badges', { from: 'b3', to: 'u4' }), f.relate('User', 'Badges', { from: 'b2', to: 'u5' }), f.relate('User', 'Badges', { from: 'b1', to: 'u6' }), - f.relate('User', 'Following', { from: 'u1', to: 'u2' }), - f.relate('User', 'Following', { from: 'u2', to: 'u3' }), - f.relate('User', 'Following', { from: 'u3', to: 'u4' }), - f.relate('User', 'Following', { from: 'u4', to: 'u5' }), - f.relate('User', 'Following', { from: 'u5', to: 'u6' }), - f.relate('User', 'Following', { from: 'u6', to: 'u7' }), f.relate('User', 'Friends', { from: 'u1', to: 'u2' }), f.relate('User', 'Friends', { from: 'u1', to: 'u3' }), f.relate('User', 'Friends', { from: 'u2', to: 'u3' }), @@ -44,6 +47,21 @@ import Factory from './factories' f.relate('User', 'Blacklisted', { from: 'u7', to: 'u6' }) ]) + await Promise.all([ + asAdmin + .follow({ id: 'u3', type: 'User' }), + asModerator + .follow({ id: 'u4', type: 'User' }), + asUser + .follow({ id: 'u4', type: 'User' }), + asTick + .follow({ id: 'u6', type: 'User' }), + asTrick + .follow({ id: 'u4', type: 'User' }), + asTrack + .follow({ id: 'u3', type: 'User' }) + ]) + await Promise.all([ f.create('Category', { id: 'cat1', name: 'Just For Fun', slug: 'justforfun', icon: 'smile' }), f.create('Category', { id: 'cat2', name: 'Happyness & Values', slug: 'happyness-values', icon: 'heart-o' }), @@ -70,15 +88,6 @@ import Factory from './factories' f.create('Tag', { id: 't4', name: 'Freiheit' }) ]) - const [ asAdmin, asModerator, asUser, asTick, asTrick, asTrack ] = await Promise.all([ - Factory().authenticateAs({ email: 'admin@example.org', password: '1234' }), - Factory().authenticateAs({ email: 'moderator@example.org', password: '1234' }), - Factory().authenticateAs({ email: 'user@example.org', password: '1234' }), - Factory().authenticateAs({ email: 'tick@example.org', password: '1234' }), - Factory().authenticateAs({ email: 'trick@example.org', password: '1234' }), - Factory().authenticateAs({ email: 'track@example.org', password: '1234' }) - ]) - await Promise.all([ asAdmin.create('Post', { id: 'p0' }), asModerator.create('Post', { id: 'p1' }), @@ -151,6 +160,26 @@ import Factory from './factories' f.relate('User', 'Shouted', { from: 'u3', to: 'p4' }), f.relate('User', 'Shouted', { from: 'u4', to: 'p1' }) ]) + await Promise.all([ + asAdmin + .shout({ id: 'p2', type: 'Post' }), + asAdmin + .shout({ id: 'p6', type: 'Post' }), + asModerator + .shout({ id: 'p0', type: 'Post' }), + asModerator + .shout({ id: 'p6', type: 'Post' }), + asUser + .shout({ id: 'p6', type: 'Post' }), + asUser + .shout({ id: 'p7', type: 'Post' }), + asTick + .shout({ id: 'p8', type: 'Post' }), + asTick + .shout({ id: 'p9', type: 'Post' }), + asTrack + .shout({ id: 'p10', type: 'Post' }) + ]) await Promise.all([ f.create('Comment', { id: 'c1' }), From 9260705611a7b20717a53b6f4e4bca7c537345e8 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Fri, 8 Mar 2019 17:03:59 +0100 Subject: [PATCH 65/66] Fixed shout seeding --- src/seed/seed-db.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/seed/seed-db.js b/src/seed/seed-db.js index 51a456126..b16e9f323 100644 --- a/src/seed/seed-db.js +++ b/src/seed/seed-db.js @@ -152,14 +152,7 @@ import Factory from './factories' f.relate('Post', 'Tags', { from: 'p14', to: 't2' }), f.relate('Post', 'Tags', { from: 'p15', to: 't3' }) ]) - await Promise.all([ - f.relate('User', 'Shouted', { from: 'u1', to: 'p2' }), - f.relate('User', 'Shouted', { from: 'u1', to: 'p3' }), - f.relate('User', 'Shouted', { from: 'u2', to: 'p1' }), - f.relate('User', 'Shouted', { from: 'u3', to: 'p1' }), - f.relate('User', 'Shouted', { from: 'u3', to: 'p4' }), - f.relate('User', 'Shouted', { from: 'u4', to: 'p1' }) - ]) + await Promise.all([ asAdmin .shout({ id: 'p2', type: 'Post' }), From f792ad3c026fee3159a1d4cba35bb80595763bbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Fri, 8 Mar 2019 17:17:57 +0000 Subject: [PATCH 66/66] Bump neo4j-driver from 1.7.2 to 1.7.3 Bumps [neo4j-driver](https://github.com/neo4j/neo4j-javascript-driver) from 1.7.2 to 1.7.3. - [Release notes](https://github.com/neo4j/neo4j-javascript-driver/releases) - [Commits](https://github.com/neo4j/neo4j-javascript-driver/compare/1.7.2...1.7.3) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 24580fd3b..166ba7fce 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "linkifyjs": "~2.1.8", "lodash": "~4.17.11", "ms": "~2.1.1", - "neo4j-driver": "~1.7.2", + "neo4j-driver": "~1.7.3", "neo4j-graphql-js": "~2.4.1", "node-fetch": "~2.3.0", "npm-run-all": "~4.1.5", diff --git a/yarn.lock b/yarn.lock index bcb5fce76..38e52cad9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5171,10 +5171,10 @@ negotiator@0.6.1: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk= -neo4j-driver@^1.7.2, neo4j-driver@~1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.2.tgz#c72a6dfa6bd2106b00a42794dc52a82b227b48e0" - integrity sha512-0IvCFYhcP9hb5JveZk33epbReDKpFTn2u5vAa8zzGG344i6yFqZrBo0mtC114ciP9zFjAtfNOP72mRm8+NV0Fg== +neo4j-driver@^1.7.2, neo4j-driver@~1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.3.tgz#1c1108ab26b7243975f1b20045daf31d8f685207" + integrity sha512-UCNOFiQdouq14PvZGTr+psy657BJsBpO6O2cJpP+NprZnEF4APrDzAcydPZSFxE1nfooLNc50vfuZ0q54UyY2Q== dependencies: babel-runtime "^6.26.0" text-encoding "^0.6.4"