From 4732148cc6c95e8ef9a18eb2f7c649b75c15e1eb Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 5 Oct 2017 18:33:41 +0800 Subject: [PATCH 001/318] Add test related packages --- package.json | 4 +- src/__tests__/index.js | 0 src/db/plugins/createdAtModifier.js | 12 + src/db/plugins/index.js | 3 + yarn.lock | 1108 ++++++++++++++++++++++++++- 5 files changed, 1091 insertions(+), 36 deletions(-) create mode 100644 src/__tests__/index.js create mode 100644 src/db/plugins/createdAtModifier.js create mode 100644 src/db/plugins/index.js diff --git a/package.json b/package.json index fd17127a4..f6b1981a6 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,12 @@ "graphql-server-module-graphiql": "^0.8.2", "graphql-subscriptions": "^0.4.3", "graphql-tools": "^1.0.0", - "meteor-random": "^0.0.3", "moment": "^2.18.1", "mongoose": "^4.9.2", "passport": "^0.4.0", "passport-anonymous": "^1.0.1", "passport-http-bearer": "^1.0.1", + "shortid": "^2.2.8", "subscriptions-transport-ws": "^0.7.3", "underscore": "^1.8.3" }, @@ -52,7 +52,9 @@ "babel-plugin-transform-object-rest-spread": "^6.23.0", "babel-preset-env": "^1.6.0", "eslint": "3.19.0", + "faker": "^4.1.0", "husky": "^0.13.4", + "jest": "^21.2.1", "lint-staged": "^3.6.0", "nodemon": "^1.11.0", "prettier": "^1.4.4" diff --git a/src/__tests__/index.js b/src/__tests__/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/db/plugins/createdAtModifier.js b/src/db/plugins/createdAtModifier.js new file mode 100644 index 000000000..0120d6049 --- /dev/null +++ b/src/db/plugins/createdAtModifier.js @@ -0,0 +1,12 @@ +export const createdAtModifier = schema => { + schema.add({ + createdAt: Date, + }); + + schema.pre('save', function(next) { + if (this._id == undefined) { + this.createdAt = new Date(); + } + next(); + }); +}; diff --git a/src/db/plugins/index.js b/src/db/plugins/index.js new file mode 100644 index 000000000..22b90b85c --- /dev/null +++ b/src/db/plugins/index.js @@ -0,0 +1,3 @@ +import { createdAtModifier } from './createdAtModifier'; + +export { createdAtModifier }; diff --git a/yarn.lock b/yarn.lock index 3d8ba8891..426c67372 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,6 +40,10 @@ dependencies: "@types/node" "*" +abab@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" + abbrev@1: version "1.1.0" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" @@ -51,6 +55,12 @@ accepts@~1.3.3: mime-types "~2.1.16" negotiator "0.6.1" +acorn-globals@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf" + dependencies: + acorn "^4.0.4" + acorn-jsx@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" @@ -61,6 +71,10 @@ acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" +acorn@^4.0.4: + version "4.0.13" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" + acorn@^5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.2.tgz#911cb53e036807cf0fa778dc5d370fbd864246d7" @@ -85,6 +99,18 @@ ajv@^5.1.0: json-schema-traverse "^0.3.0" json-stable-stringify "^1.0.1" +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + ansi-align@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" @@ -95,6 +121,10 @@ ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" +ansi-escapes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -107,7 +137,7 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -ansi-styles@^3.1.0: +ansi-styles@^3.1.0, ansi-styles@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" dependencies: @@ -124,6 +154,12 @@ app-root-path@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46" +append-transform@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" + dependencies: + default-require-extensions "^1.0.0" + aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -151,6 +187,10 @@ arr-flatten@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -169,7 +209,7 @@ array-unique@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" -arrify@^1.0.0: +arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -181,6 +221,10 @@ assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" @@ -195,6 +239,16 @@ async@2.1.4: dependencies: lodash "^4.14.0" +async@^1.4.0: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + dependencies: + lodash "^4.14.0" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -236,7 +290,7 @@ babel-code-frame@^6.16.0, babel-code-frame@^6.26.0: esutils "^2.0.2" js-tokens "^3.0.2" -babel-core@^6.26.0: +babel-core@^6.0.0, babel-core@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" dependencies: @@ -260,7 +314,7 @@ babel-core@^6.26.0: slash "^1.0.0" source-map "^0.5.6" -babel-generator@^6.26.0: +babel-generator@^6.18.0, babel-generator@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" dependencies: @@ -374,6 +428,13 @@ babel-helpers@^6.24.1: babel-runtime "^6.22.0" babel-template "^6.24.1" +babel-jest@^21.2.0: + version "21.2.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-21.2.0.tgz#2ce059519a9374a2c46f2455b6fbef5ad75d863e" + dependencies: + babel-plugin-istanbul "^4.0.0" + babel-preset-jest "^21.2.0" + babel-messages@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" @@ -386,6 +447,18 @@ babel-plugin-check-es2015-constants@^6.22.0: dependencies: babel-runtime "^6.22.0" +babel-plugin-istanbul@^4.0.0: + version "4.1.5" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz#6760cdd977f411d3e175bb064f2bc327d99b2b6e" + dependencies: + find-up "^2.1.0" + istanbul-lib-instrument "^1.7.5" + test-exclude "^4.1.1" + +babel-plugin-jest-hoist@^21.2.0: + version "21.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-21.2.0.tgz#2cef637259bd4b628a6cace039de5fcd14dbb006" + babel-plugin-syntax-async-functions@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" @@ -394,7 +467,7 @@ babel-plugin-syntax-exponentiation-operator@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" -babel-plugin-syntax-object-rest-spread@^6.8.0: +babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" @@ -649,6 +722,13 @@ babel-preset-env@^1.6.0: invariant "^2.2.2" semver "^5.3.0" +babel-preset-jest@^21.2.0: + version "21.2.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-21.2.0.tgz#ff9d2bce08abd98e8a36d9a8a5189b9173b85638" + dependencies: + babel-plugin-jest-hoist "^21.2.0" + babel-plugin-syntax-object-rest-spread "^6.13.0" + babel-register@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" @@ -668,7 +748,7 @@ babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: core-js "^2.4.0" regenerator-runtime "^0.11.0" -babel-template@^6.24.1, babel-template@^6.26.0: +babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" dependencies: @@ -678,7 +758,7 @@ babel-template@^6.24.1, babel-template@^6.26.0: babylon "^6.18.0" lodash "^4.17.4" -babel-traverse@^6.24.1, babel-traverse@^6.26.0: +babel-traverse@^6.18.0, babel-traverse@^6.24.1, babel-traverse@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" dependencies: @@ -692,7 +772,7 @@ babel-traverse@^6.24.1, babel-traverse@^6.26.0: invariant "^2.2.2" lodash "^4.17.4" -babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: +babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" dependencies: @@ -787,6 +867,12 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" +browser-resolve@^1.11.2: + version "1.11.2" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce" + dependencies: + resolve "1.1.7" + browserslist@^2.1.2: version "2.4.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.4.0.tgz#693ee93d01e66468a6348da5498e011f578f87f8" @@ -794,6 +880,12 @@ browserslist@^2.1.2: caniuse-lite "^1.0.30000718" electron-to-chromium "^1.3.18" +bser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" + dependencies: + node-int64 "^0.4.0" + bson@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.4.tgz#93c10d39eaa5b58415cbc4052f3e53e562b0b72c" @@ -802,6 +894,10 @@ buffer-shims@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -816,7 +912,15 @@ callsites@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" -camelcase@^4.0.0: +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^4.0.0, camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -832,6 +936,13 @@ caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -898,6 +1009,22 @@ cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -957,11 +1084,15 @@ content-disposition@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" +content-type-parser@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.1.tgz#c3e56988c53c65127fb46d4032a3a900246fdc94" + content-type@~1.0.2, content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" -convert-source-map@^1.5.0: +convert-source-map@^1.4.0, convert-source-map@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" @@ -1025,9 +1156,15 @@ crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" -crypto@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/crypto/-/crypto-0.0.3.tgz#470a81b86be4c5ee17acc8207a1f5315ae20dbb0" +cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b" + +"cssstyle@>= 0.2.37 < 0.3.0": + version "0.2.37" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54" + dependencies: + cssom "0.3.x" d@1: version "1.0.0" @@ -1051,6 +1188,16 @@ debug@2.6.8, debug@^2.1.1, debug@^2.2.0, debug@^2.6.8: dependencies: ms "2.0.0" +debug@^2.6.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +decamelize@^1.0.0, decamelize@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + deep-equal@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -1063,6 +1210,12 @@ deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" +default-require-extensions@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" + dependencies: + strip-bom "^2.0.0" + define-properties@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" @@ -1112,6 +1265,10 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" +diff@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" + doctrine@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" @@ -1159,6 +1316,12 @@ encodeurl@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" +errno@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" + dependencies: + prr "~0.0.0" + error-ex@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" @@ -1255,6 +1418,17 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" +escodegen@^1.6.1: + version "1.9.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.0.tgz#9811a2f265dc1cd3894420ee3717064b632b8852" + dependencies: + esprima "^3.1.3" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.5.6" + escope@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" @@ -1311,6 +1485,10 @@ espree@^3.4.0: acorn "^5.1.1" acorn-jsx "^3.0.0" +esprima@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + esprima@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" @@ -1363,6 +1541,12 @@ eventemitter3@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" +exec-sh@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38" + dependencies: + merge "^1.1.3" + execa@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" @@ -1391,6 +1575,17 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" +expect@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-21.2.1.tgz#003ac2ac7005c3c29e73b38a272d4afadd6d1d7b" + dependencies: + ansi-styles "^3.2.0" + jest-diff "^21.2.1" + jest-get-type "^21.2.0" + jest-matcher-utils "^21.2.1" + jest-message-util "^21.2.1" + jest-regex-util "^21.2.0" + express@^4.15.2: version "4.15.4" resolved "https://registry.yarnpkg.com/express/-/express-4.15.4.tgz#032e2253489cf8fce02666beca3d11ed7a2daed1" @@ -1438,6 +1633,10 @@ extsprintf@1.3.0, extsprintf@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" +faker@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f" + fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" @@ -1446,6 +1645,12 @@ fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + dependencies: + bser "^2.0.0" + figures@^1.3.5, figures@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -1464,6 +1669,13 @@ filename-regex@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" +fileset@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" + dependencies: + glob "^7.0.3" + minimatch "^3.0.3" + fill-range@^2.1.0: version "2.2.3" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" @@ -1490,6 +1702,19 @@ find-parent-dir@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + flat-cache@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" @@ -1551,7 +1776,7 @@ fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" -fsevents@^1.0.0: +fsevents@^1.0.0, fsevents@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4" dependencies: @@ -1602,6 +1827,10 @@ generate-object-property@^1.1.0: dependencies: is-property "^1.0.0" +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -1625,7 +1854,7 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2, glob@~7.1.2: +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -1718,6 +1947,20 @@ graphql@^0.10.1: dependencies: iterall "^1.1.0" +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + +handlebars@^4.0.3: + version "4.0.10" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f" + dependencies: + async "^1.4.0" + optimist "^0.6.1" + source-map "^0.4.4" + optionalDependencies: + uglify-js "^2.6" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -1735,6 +1978,10 @@ has-ansi@^2.0.0: dependencies: ansi-regex "^2.0.0" +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + has-flag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" @@ -1773,6 +2020,16 @@ hooks-fixed@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/hooks-fixed/-/hooks-fixed-2.0.0.tgz#a01d894d52ac7f6599bbb1f63dfc9c411df70cba" +hosted-git-info@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +html-encoding-sniffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.1.tgz#79bf7a785ea495fe66165e734153f363ff5437da" + dependencies: + whatwg-encoding "^1.0.1" + http-errors@1.6.2, http-errors@~1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" @@ -1799,6 +2056,10 @@ husky@^0.13.4: is-ci "^1.0.9" normalize-path "^1.0.0" +iconv-lite@0.4.13: + version "0.4.13" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" + iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" @@ -1872,6 +2133,10 @@ invariant@^2.2.2: dependencies: loose-envify "^1.0.0" +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + ipaddr.js@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0" @@ -1890,11 +2155,17 @@ is-buffer@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + is-callable@^1.1.1, is-callable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" -is-ci@^1.0.9: +is-ci@^1.0.10, is-ci@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e" dependencies: @@ -2041,6 +2312,10 @@ is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -2059,15 +2334,303 @@ isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" +istanbul-api@^1.1.1: + version "1.1.14" + resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.14.tgz#25bc5701f7c680c0ffff913de46e3619a3a6e680" + dependencies: + async "^2.1.4" + fileset "^2.0.2" + istanbul-lib-coverage "^1.1.1" + istanbul-lib-hook "^1.0.7" + istanbul-lib-instrument "^1.8.0" + istanbul-lib-report "^1.1.1" + istanbul-lib-source-maps "^1.2.1" + istanbul-reports "^1.1.2" + js-yaml "^3.7.0" + mkdirp "^0.5.1" + once "^1.4.0" + +istanbul-lib-coverage@^1.0.1, istanbul-lib-coverage@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da" + +istanbul-lib-hook@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.7.tgz#dd6607f03076578fe7d6f2a630cf143b49bacddc" + dependencies: + append-transform "^0.4.0" + +istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.8.0.tgz#66f6c9421cc9ec4704f76f2db084ba9078a2b532" + dependencies: + babel-generator "^6.18.0" + babel-template "^6.16.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + babylon "^6.18.0" + istanbul-lib-coverage "^1.1.1" + semver "^5.3.0" + +istanbul-lib-report@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#f0e55f56655ffa34222080b7a0cd4760e1405fc9" + dependencies: + istanbul-lib-coverage "^1.1.1" + mkdirp "^0.5.1" + path-parse "^1.0.5" + supports-color "^3.1.2" + +istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.1.tgz#a6fe1acba8ce08eebc638e572e294d267008aa0c" + dependencies: + debug "^2.6.3" + istanbul-lib-coverage "^1.1.1" + mkdirp "^0.5.1" + rimraf "^2.6.1" + source-map "^0.5.3" + +istanbul-reports@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.2.tgz#0fb2e3f6aa9922bd3ce45d05d8ab4d5e8e07bd4f" + dependencies: + handlebars "^4.0.3" + iterall@^1.1.0, iterall@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.1.tgz#f7f0af11e9a04ec6426260f5019d9fcca4d50214" +jest-changed-files@^21.2.0: + version "21.2.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-21.2.0.tgz#5dbeecad42f5d88b482334902ce1cba6d9798d29" + dependencies: + throat "^4.0.0" + +jest-cli@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-21.2.1.tgz#9c528b6629d651911138d228bdb033c157ec8c00" + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + is-ci "^1.0.10" + istanbul-api "^1.1.1" + istanbul-lib-coverage "^1.0.1" + istanbul-lib-instrument "^1.4.2" + istanbul-lib-source-maps "^1.1.0" + jest-changed-files "^21.2.0" + jest-config "^21.2.1" + jest-environment-jsdom "^21.2.1" + jest-haste-map "^21.2.0" + jest-message-util "^21.2.1" + jest-regex-util "^21.2.0" + jest-resolve-dependencies "^21.2.0" + jest-runner "^21.2.1" + jest-runtime "^21.2.1" + jest-snapshot "^21.2.1" + jest-util "^21.2.1" + micromatch "^2.3.11" + node-notifier "^5.0.2" + pify "^3.0.0" + slash "^1.0.0" + string-length "^2.0.0" + strip-ansi "^4.0.0" + which "^1.2.12" + worker-farm "^1.3.1" + yargs "^9.0.0" + +jest-config@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-21.2.1.tgz#c7586c79ead0bcc1f38c401e55f964f13bf2a480" + dependencies: + chalk "^2.0.1" + glob "^7.1.1" + jest-environment-jsdom "^21.2.1" + jest-environment-node "^21.2.1" + jest-get-type "^21.2.0" + jest-jasmine2 "^21.2.1" + jest-regex-util "^21.2.0" + jest-resolve "^21.2.0" + jest-util "^21.2.1" + jest-validate "^21.2.1" + pretty-format "^21.2.1" + +jest-diff@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-21.2.1.tgz#46cccb6cab2d02ce98bc314011764bb95b065b4f" + dependencies: + chalk "^2.0.1" + diff "^3.2.0" + jest-get-type "^21.2.0" + pretty-format "^21.2.1" + +jest-docblock@^21.2.0: + version "21.2.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414" + +jest-environment-jsdom@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-21.2.1.tgz#38d9980c8259b2a608ec232deee6289a60d9d5b4" + dependencies: + jest-mock "^21.2.0" + jest-util "^21.2.1" + jsdom "^9.12.0" + +jest-environment-node@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-21.2.1.tgz#98c67df5663c7fbe20f6e792ac2272c740d3b8c8" + dependencies: + jest-mock "^21.2.0" + jest-util "^21.2.1" + +jest-get-type@^21.2.0: + version "21.2.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-21.2.0.tgz#f6376ab9db4b60d81e39f30749c6c466f40d4a23" + +jest-haste-map@^21.2.0: + version "21.2.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-21.2.0.tgz#1363f0a8bb4338f24f001806571eff7a4b2ff3d8" + dependencies: + fb-watchman "^2.0.0" + graceful-fs "^4.1.11" + jest-docblock "^21.2.0" + micromatch "^2.3.11" + sane "^2.0.0" + worker-farm "^1.3.1" + +jest-jasmine2@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-21.2.1.tgz#9cc6fc108accfa97efebce10c4308548a4ea7592" + dependencies: + chalk "^2.0.1" + expect "^21.2.1" + graceful-fs "^4.1.11" + jest-diff "^21.2.1" + jest-matcher-utils "^21.2.1" + jest-message-util "^21.2.1" + jest-snapshot "^21.2.1" + p-cancelable "^0.3.0" + +jest-matcher-utils@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-21.2.1.tgz#72c826eaba41a093ac2b4565f865eb8475de0f64" + dependencies: + chalk "^2.0.1" + jest-get-type "^21.2.0" + pretty-format "^21.2.1" + +jest-message-util@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-21.2.1.tgz#bfe5d4692c84c827d1dcf41823795558f0a1acbe" + dependencies: + chalk "^2.0.1" + micromatch "^2.3.11" + slash "^1.0.0" + +jest-mock@^21.2.0: + version "21.2.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-21.2.0.tgz#7eb0770e7317968165f61ea2a7281131534b3c0f" + +jest-regex-util@^21.2.0: + version "21.2.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-21.2.0.tgz#1b1e33e63143babc3e0f2e6c9b5ba1eb34b2d530" + +jest-resolve-dependencies@^21.2.0: + version "21.2.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-21.2.0.tgz#9e231e371e1a736a1ad4e4b9a843bc72bfe03d09" + dependencies: + jest-regex-util "^21.2.0" + +jest-resolve@^21.2.0: + version "21.2.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-21.2.0.tgz#068913ad2ba6a20218e5fd32471f3874005de3a6" + dependencies: + browser-resolve "^1.11.2" + chalk "^2.0.1" + is-builtin-module "^1.0.0" + +jest-runner@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-21.2.1.tgz#194732e3e518bfb3d7cbfc0fd5871246c7e1a467" + dependencies: + jest-config "^21.2.1" + jest-docblock "^21.2.0" + jest-haste-map "^21.2.0" + jest-jasmine2 "^21.2.1" + jest-message-util "^21.2.1" + jest-runtime "^21.2.1" + jest-util "^21.2.1" + pify "^3.0.0" + throat "^4.0.0" + worker-farm "^1.3.1" + +jest-runtime@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-21.2.1.tgz#99dce15309c670442eee2ebe1ff53a3cbdbbb73e" + dependencies: + babel-core "^6.0.0" + babel-jest "^21.2.0" + babel-plugin-istanbul "^4.0.0" + chalk "^2.0.1" + convert-source-map "^1.4.0" + graceful-fs "^4.1.11" + jest-config "^21.2.1" + jest-haste-map "^21.2.0" + jest-regex-util "^21.2.0" + jest-resolve "^21.2.0" + jest-util "^21.2.1" + json-stable-stringify "^1.0.1" + micromatch "^2.3.11" + slash "^1.0.0" + strip-bom "3.0.0" + write-file-atomic "^2.1.0" + yargs "^9.0.0" + +jest-snapshot@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-21.2.1.tgz#29e49f16202416e47343e757e5eff948c07fd7b0" + dependencies: + chalk "^2.0.1" + jest-diff "^21.2.1" + jest-matcher-utils "^21.2.1" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + pretty-format "^21.2.1" + +jest-util@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-21.2.1.tgz#a274b2f726b0897494d694a6c3d6a61ab819bb78" + dependencies: + callsites "^2.0.0" + chalk "^2.0.1" + graceful-fs "^4.1.11" + jest-message-util "^21.2.1" + jest-mock "^21.2.0" + jest-validate "^21.2.1" + mkdirp "^0.5.1" + +jest-validate@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-21.2.1.tgz#cc0cbca653cd54937ba4f2a111796774530dd3c7" + dependencies: + chalk "^2.0.1" + jest-get-type "^21.2.0" + leven "^2.1.0" + pretty-format "^21.2.1" + +jest@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-21.2.1.tgz#c964e0b47383768a1438e3ccf3c3d470327604e1" + dependencies: + jest-cli "^21.2.1" + js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@^3.4.3, js-yaml@^3.5.1: +js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0: version "3.10.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" dependencies: @@ -2078,6 +2641,30 @@ jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" +jsdom@^9.12.0: + version "9.12.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-9.12.0.tgz#e8c546fffcb06c00d4833ca84410fed7f8a097d4" + dependencies: + abab "^1.0.3" + acorn "^4.0.4" + acorn-globals "^3.1.0" + array-equal "^1.0.0" + content-type-parser "^1.0.1" + cssom ">= 0.3.2 < 0.4.0" + cssstyle ">= 0.2.37 < 0.3.0" + escodegen "^1.6.1" + html-encoding-sniffer "^1.0.1" + nwmatcher ">= 1.3.9 < 2.0.0" + parse5 "^1.5.1" + request "^2.79.0" + sax "^1.2.1" + symbol-tree "^3.2.1" + tough-cookie "^2.3.2" + webidl-conversions "^4.0.0" + whatwg-encoding "^1.0.1" + whatwg-url "^4.3.0" + xml-name-validator "^2.0.1" + jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" @@ -2147,6 +2734,20 @@ latest-version@^3.0.0: dependencies: package-json "^4.0.0" +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +leven@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" + levn@^0.3.0, levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" @@ -2215,6 +2816,32 @@ listr@^0.12.0: stream-to-observable "^0.1.0" strip-ansi "^3.0.1" +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + lodash._baseassign@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" @@ -2314,6 +2941,10 @@ log-update@^1.0.2: ansi-escapes "^1.0.0" cli-cursor "^1.0.2" +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + loose-envify@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" @@ -2337,6 +2968,12 @@ make-dir@^1.0.0: dependencies: pify "^2.3.0" +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + dependencies: + tmpl "1.0.x" + map-stream@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" @@ -2345,21 +2982,25 @@ media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" -meteor-random@^0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/meteor-random/-/meteor-random-0.0.3.tgz#0d1489ecdb9bcb58bb52decebfbceddf54473a68" - dependencies: - crypto "0.0.3" +merge@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" -micromatch@^2.1.5: +micromatch@^2.1.5, micromatch@^2.3.11: version "2.3.11" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" dependencies: @@ -2391,7 +3032,11 @@ mime@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" -minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -2401,10 +3046,14 @@ minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@^1.2.0, minimist@~1.2.0: +minimist@^1.1.1, minimist@^1.2.0, minimist@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -2488,6 +3137,19 @@ negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + +node-notifier@^5.0.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.1.2.tgz#2fa9e12605fa10009d44549d6fcd8a63dde0e4ff" + dependencies: + growly "^1.3.0" + semver "^5.3.0" + shellwords "^0.1.0" + which "^1.2.12" + node-pre-gyp@^0.6.36: version "0.6.37" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.37.tgz#3c872b236b2e266e4140578fe1ee88f693323a05" @@ -2531,6 +3193,15 @@ nopt@~1.0.10: dependencies: abbrev "1" +normalize-package-data@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + normalize-path@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" @@ -2574,6 +3245,10 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" +"nwmatcher@>= 1.3.9 < 2.0.0": + version "1.4.2" + resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.2.tgz#c5e545ab40d22a56b0326531c4beaed7a888b3ea" + oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" @@ -2603,7 +3278,7 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.3.3: +once@^1.3.0, once@^1.3.3, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: @@ -2613,7 +3288,14 @@ onetime@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" -optionator@^0.8.2: +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optionator@^0.8.1, optionator@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" dependencies: @@ -2637,6 +3319,14 @@ os-homedir@^1.0.0, os-homedir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -2656,10 +3346,24 @@ output-file-sync@^1.1.2: mkdirp "^0.5.1" object-assign "^4.1.0" +p-cancelable@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" +p-limit@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + p-map@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" @@ -2688,6 +3392,10 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse5@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" + parseurl@~1.3.1, parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" @@ -2715,6 +3423,16 @@ passport@^0.4.0: passport-strategy "1.x.x" pause "0.0.1" +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -2735,6 +3453,20 @@ path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" @@ -2753,6 +3485,10 @@ pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -2783,6 +3519,13 @@ prettier@^1.4.4: version "1.7.0" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.7.0.tgz#47481588f41f7c90f63938feb202ac82554e7150" +pretty-format@^21.2.1: + version "21.2.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-21.2.1.tgz#ae5407f3cf21066cd011aa1ba5fce7b6a2eddb36" + dependencies: + ansi-regex "^3.0.0" + ansi-styles "^3.2.0" + private@^0.1.6, private@^0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" @@ -2802,6 +3545,10 @@ proxy-addr@~1.1.5: forwarded "~0.1.0" ipaddr.js "1.4.0" +prr@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" + ps-tree@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014" @@ -2853,6 +3600,36 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + readable-stream@2.2.7: version "2.2.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.7.tgz#07057acbe2467b22042d36f98c5ad507054e95b1" @@ -2979,6 +3756,33 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" +request@^2.79.0: + version "2.83.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + request@^2.81.0: version "2.82.0" resolved "https://registry.yarnpkg.com/request/-/request-2.82.0.tgz#2ba8a92cd7ac45660ea2b10a53ae67cd247516ea" @@ -3006,10 +3810,18 @@ request@^2.81.0: tunnel-agent "^0.6.0" uuid "^3.1.0" +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + require-from-string@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + require-uncached@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" @@ -3032,6 +3844,10 @@ resolve-from@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + resolve@^1.1.6, resolve@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86" @@ -3051,6 +3867,12 @@ resumer@~0.0.0: dependencies: through "~2.3.4" +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" @@ -3077,13 +3899,31 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +sane@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-2.2.0.tgz#d6d2e2fcab00e3d283c93b912b7c3a20846f1d56" + dependencies: + anymatch "^1.3.0" + exec-sh "^0.2.0" + fb-watchman "^2.0.0" + minimatch "^3.0.2" + minimist "^1.1.1" + walker "~1.0.5" + watch "~0.18.0" + optionalDependencies: + fsevents "^1.1.1" + +sax@^1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" dependencies: semver "^5.0.3" -semver@^5.0.3, semver@^5.1.0, semver@^5.3.0: +"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" @@ -3114,7 +3954,7 @@ serve-static@1.12.4: parseurl "~1.3.1" send "0.15.4" -set-blocking@~2.0.0: +set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -3144,6 +3984,14 @@ shelljs@^0.7.5: interpret "^1.0.0" rechoir "^0.6.2" +shellwords@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + +shortid@^2.2.8: + version "2.2.8" + resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.8.tgz#033b117d6a2e975804f6f0969dbe7d3d0b355131" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -3176,10 +4024,30 @@ source-map-support@^0.4.15: dependencies: source-map "^0.5.6" -source-map@^0.5.6: +source-map@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + split@0.3: version "0.3.3" resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" @@ -3222,6 +4090,13 @@ stream-to-observable@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe" +string-length@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" + dependencies: + astral-regex "^1.0.0" + strip-ansi "^4.0.0" + string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -3267,10 +4142,16 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-bom@^3.0.0: +strip-bom@3.0.0, strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -3298,6 +4179,12 @@ supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" +supports-color@^3.1.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + dependencies: + has-flag "^1.0.0" + supports-color@^4.0.0: version "4.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" @@ -3308,6 +4195,10 @@ symbol-observable@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" +symbol-tree@^3.2.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" + table@^3.7.8: version "3.8.3" resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" @@ -3364,10 +4255,24 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" +test-exclude@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26" + dependencies: + arrify "^1.0.1" + micromatch "^2.3.11" + object-assign "^4.1.0" + read-pkg-up "^1.0.1" + require-main-filename "^1.0.1" + text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" +throat@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" + through@2, through@^2.3.6, through@~2.3, through@~2.3.1, through@~2.3.4, through@~2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -3376,6 +4281,10 @@ timed-out@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" @@ -3386,12 +4295,16 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@~2.3.2: +tough-cookie@^2.3.2, tough-cookie@~2.3.2, tough-cookie@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" dependencies: punycode "^1.4.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" @@ -3427,6 +4340,19 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" +uglify-js@^2.6: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + uid-number@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" @@ -3504,6 +4430,13 @@ v8flags@^2.1.1: dependencies: user-home "^1.1.1" +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + vary@^1, vary@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" @@ -3516,7 +4449,45 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -which@^1.2.10, which@^1.2.9: +walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + 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" + dependencies: + exec-sh "^0.2.0" + minimist "^1.2.0" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + +webidl-conversions@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + +whatwg-encoding@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.1.tgz#3c6c451a198ee7aec55b1ec61d0920c67801a5f4" + dependencies: + iconv-lite "0.4.13" + +whatwg-url@^4.3.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0" + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@^1.2.10, which@^1.2.12, which@^1.2.9: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" dependencies: @@ -3534,15 +4505,41 @@ widest-line@^1.0.0: dependencies: string-width "^1.0.1" +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + wordwrap@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" +worker-farm@^1.3.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.0.tgz#adfdf0cd40581465ed0a1f648f9735722afd5c8d" + dependencies: + errno "^0.1.4" + xtend "^4.0.1" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" -write-file-atomic@^2.0.0: +write-file-atomic@^2.0.0, write-file-atomic@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" dependencies: @@ -3568,10 +4565,51 @@ xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" -xtend@^4.0.0: +xml-name-validator@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" + +xtend@^4.0.0, xtend@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + +yargs@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" From b1686cd3548b3043a7e7833471b7e120fbe05da5 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 5 Oct 2017 18:43:46 +0800 Subject: [PATCH 002/318] Readd meteor-random --- package.json | 2 +- yarn.lock | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f6b1981a6..561cf99eb 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,12 @@ "graphql-server-module-graphiql": "^0.8.2", "graphql-subscriptions": "^0.4.3", "graphql-tools": "^1.0.0", + "meteor-random": "^0.0.3", "moment": "^2.18.1", "mongoose": "^4.9.2", "passport": "^0.4.0", "passport-anonymous": "^1.0.1", "passport-http-bearer": "^1.0.1", - "shortid": "^2.2.8", "subscriptions-transport-ws": "^0.7.3", "underscore": "^1.8.3" }, diff --git a/yarn.lock b/yarn.lock index 426c67372..5fe574607 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1156,6 +1156,10 @@ crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" +crypto@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-0.0.3.tgz#470a81b86be4c5ee17acc8207a1f5315ae20dbb0" + cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": version "0.3.2" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b" @@ -2996,6 +3000,12 @@ merge@^1.1.3: version "1.2.0" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" +meteor-random@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/meteor-random/-/meteor-random-0.0.3.tgz#0d1489ecdb9bcb58bb52decebfbceddf54473a68" + dependencies: + crypto "0.0.3" + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -3988,10 +3998,6 @@ shellwords@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" -shortid@^2.2.8: - version "2.2.8" - resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.8.tgz#033b117d6a2e975804f6f0969dbe7d3d0b355131" - signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" From a8e9d0507712a73ba505fcddd551f15c8435f78e Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Thu, 5 Oct 2017 21:25:07 +0800 Subject: [PATCH 003/318] root commit From f1c3be34abebc5ea4345c5a5c56f491b44d174b0 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Thu, 5 Oct 2017 21:54:41 +0800 Subject: [PATCH 004/318] tags mutation --- package.json | 6 +- src/__tests__/tags.test.js | 53 ++++++++++ src/data/resolvers/mutations/index.js | 2 + src/data/resolvers/mutations/tags.js | 134 ++++++++++++++++++++++++++ src/data/schema/index.js | 3 +- src/data/schema/tag.js | 14 +++ src/db/factories.js | 25 +++++ src/db/models/Tags.js | 15 +++ test.config.json | 4 + yarn.lock | 4 + 10 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/tags.test.js create mode 100644 src/data/resolvers/mutations/tags.js create mode 100644 src/db/factories.js create mode 100644 test.config.json diff --git a/package.json b/package.json index 561cf99eb..1c7098178 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "build": "babel src --out-dir dist --ignore __tests__,tests --copy-files", "lint": "eslint src", "format": "prettier --write --print-width 100 --single-quote --trailing-comma all 'src/**/*.js'", - "precommit": "lint-staged" + "precommit": "lint-staged", + "test": "jest --config=test.config.json" }, "lint-staged": { "*.js": [ @@ -57,6 +58,7 @@ "jest": "^21.2.1", "lint-staged": "^3.6.0", "nodemon": "^1.11.0", - "prettier": "^1.4.4" + "prettier": "^1.4.4", + "shortid": "^2.2.8" } } diff --git a/src/__tests__/tags.test.js b/src/__tests__/tags.test.js new file mode 100644 index 000000000..d22fb2119 --- /dev/null +++ b/src/__tests__/tags.test.js @@ -0,0 +1,53 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Tags, Users } from '../db/models'; +import { tagsFactory, userFactory } from '../db/factories'; +import tagsMutations from '../data/resolvers/mutations/tags'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Tags mutations', () => { + let _tag; + let _user; + + beforeEach(async () => { + // Creating test data + _tag = await tagsFactory(); + _user = await userFactory(); + }); + + afterEach(async () => { + // Clearing test data + await Tags.remove({}); + await Users.remove({}); + }); + + test('Create tag', async () => { + const tagObj = await tagsMutations.tagsAdd( + {}, + { name: `${_tag.name}1`, type: `${_tag.type}1`, colorCode: _tag.colorCode }, + { user: _user }, + ); + + expect(tagObj).toBeDefined(); + }); + + test('Update tag', async () => { + const tagObj = await tagsMutations.tagsEdit( + {}, + { _id: _tag.id, name: _tag.name, type: _tag.type, colorCode: _tag.colorCode }, + { user: _user }, + ); + + expect(tagObj).toBeDefined(); + }); + + test('Delete tag', async () => { + const isDeleted = await tagsMutations.tagsRemove({}, { ids: [_tag.id] }, { user: _user }); + expect(isDeleted).toBeTruthy(); + }); +}); diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index cd4940a8a..476fbcbf5 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -1,5 +1,7 @@ import conversation from './conversation'; +import tags from './tags'; export default { ...conversation, + ...tags, }; diff --git a/src/data/resolvers/mutations/tags.js b/src/data/resolvers/mutations/tags.js new file mode 100644 index 000000000..81634b3c7 --- /dev/null +++ b/src/data/resolvers/mutations/tags.js @@ -0,0 +1,134 @@ +import _ from 'underscore'; +import { Tags, Customers, Conversations, EngageMessages } from '../../../db/models'; + +const validateUniqueness = async (selector, data) => { + const { name, type } = data; + const filter = { name, type }; + + if (!name || !type) { + return true; + } + + // can't update name & type same time more than one tags. + const count = await Tags.find(selector).count(); + if (selector && count > 1) { + return false; + } + + const obj = selector && (await Tags.findOne(selector)); + if (obj) { + filter._id = { $ne: obj._id }; + } + + const existing = await Tags.findOne(filter); + if (existing) { + return false; + } + + return true; +}; + +async function tagObject({ tagIds, objectIds, collection, tagType }) { + if ((await Tags.find({ _id: { $in: tagIds }, type: tagType }).count()) !== tagIds.length) { + throw new Error('Tag not found.'); + } + + const objects = await collection.find({ _id: { $in: objectIds } }, { fields: { tagIds: 1 } }); + + let removeIds = []; + + objects.forEach(obj => { + removeIds.push(obj.tagIds || []); + }); + + removeIds = _.uniq(_.flatten(removeIds)); + + await Tags.update({ _id: { $in: removeIds } }, { $inc: { objectCount: -1 } }, { multi: true }); + + await collection.update({ _id: { $in: objectIds } }, { $set: { tagIds } }, { multi: true }); + + await Tags.update({ _id: { $in: tagIds } }, { $inc: { objectCount: 1 } }, { multi: true }); +} + +export default { + /** + * Create new tag + * @return {Promise} tag object + */ + async tagsAdd(root, { name, type, colorCode }, { user }) { + if (user) { + const isUnique = await validateUniqueness(null, { name, type, colorCode }); + if (!isUnique) { + throw new Error('Tag duplicated'); + } + + return await Tags.createTag({ name, type, colorCode }); + } + }, + + /** + * Update tag + * @return {Promise} tag object + */ + async tagsEdit(root, { _id, name, type, colorCode }, { user }) { + if (user) { + const isUnique = await validateUniqueness({ _id }, { name, type, colorCode }); + if (!isUnique) { + throw new Error('Tag duplicated'); + } + + await Tags.update({ _id: _id }, { name, type, colorCode }); + return Tags.findOne({ _id }); + } + }, + + /** + * Delete tag + * @return {Promise} + */ + async tagsRemove(root, { ids }, { user }) { + if (user) { + const tagCount = await Tags.find({ _id: { $in: ids } }).count(); + if (tagCount !== ids.length) { + throw new Error('Tag not found'); + } + + let count = 0; + + count += await Customers.find({ tagIds: { $in: ids } }).count(); + count += await Conversations.find({ tagIds: { $in: ids } }).count(); + count += await EngageMessages.find({ tagIds: { $in: ids } }).count(); + + if (count > 0) { + throw new Error("Can't remove a tag with tagged object(s)"); + } + + return Tags.remove({ _id: { $in: ids } }); + } + }, + + /** + * Attach a tag + * @return {Promise} + */ + async tagsTag(root, { type, targetIds, tagIds }, { user }) { + if (user) { + let collection = Conversations; + + if (type === 'customer') { + collection = Customers; + } + + if (type === 'engageMessage') { + collection = EngageMessages; + } + + await tagObject({ + tagIds, + objectIds: targetIds, + collection, + tagType: type, + }); + } + }, +}; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index a575fc437..2bfe6aacc 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -14,7 +14,7 @@ import { types as FormTypes, queries as FormQueries } from './form'; import { types as EngageTypes, queries as EngageQueries } from './engage'; -import { types as TagTypes, queries as TagQueries } from './tag'; +import { types as TagTypes, queries as TagQueries, mutations as TagMutations } from './tag'; import { types as CustomerTypes, queries as CustomerQueries } from './customer'; @@ -68,6 +68,7 @@ export const queries = ` export const mutations = ` type Mutation { ${ConversationMutations} + ${TagMutations} } `; diff --git a/src/data/schema/tag.js b/src/data/schema/tag.js index 661d8ba98..c8e0cf567 100644 --- a/src/data/schema/tag.js +++ b/src/data/schema/tag.js @@ -1,4 +1,11 @@ export const types = ` + enum TagType { + all + customer + conversation + engageMessage + } + type Tag { _id: String! name: String @@ -12,3 +19,10 @@ export const types = ` export const queries = ` tags(type: String): [Tag] `; + +export const mutations = ` + tagsAdd(name: String!, type: TagType!, colorCode: String): Tag + tagsEdit(_id: String!, name: String!, type: TagType!, colorCode: String): Tag + tagsRemove(ids: [String!]!): Tag + tagsTag(type: TagType!, targetIds: [String!]!, tagIds: [String!]!): Tag +`; diff --git a/src/db/factories.js b/src/db/factories.js new file mode 100644 index 000000000..94641f815 --- /dev/null +++ b/src/db/factories.js @@ -0,0 +1,25 @@ +import shortid from 'shortid'; +import faker from 'faker'; +import { Users, Tags } from './models'; + +export const userFactory = (params = {}) => { + const user = new Users({ + username: params.username || faker.random.word(), + details: { + fullName: params.fullName || faker.random.word(), + }, + }); + + return user.save(); +}; + +export const tagsFactory = (params = {}) => { + const tag = new Tags({ + name: faker.random.word(), + type: params.type || faker.random.word(), + colorCode: params.colorCode || shortid.generate(), + userId: shortid.generate(), + }); + + return tag.save(); +}; diff --git a/src/db/models/Tags.js b/src/db/models/Tags.js index c2289d507..77a2c2b1a 100644 --- a/src/db/models/Tags.js +++ b/src/db/models/Tags.js @@ -14,6 +14,21 @@ const TagSchema = mongoose.Schema({ objectCount: Number, }); +class Tag { + /** + * Create a tag + * @param {Object} tagObj object + * @return {Promise} Newly created tag object + */ + static createTag(tagObj) { + return this.create({ + ...tagObj, + createdAt: new Date(), + }); + } +} + +TagSchema.loadClass(Tag); const Tags = mongoose.model('tags', TagSchema); export default Tags; diff --git a/test.config.json b/test.config.json new file mode 100644 index 000000000..0f7b36321 --- /dev/null +++ b/test.config.json @@ -0,0 +1,4 @@ +{ + "testRegex": ".*test.js$", + "testEnvironment": "node" +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 5fe574607..508f02936 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3998,6 +3998,10 @@ shellwords@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" +shortid@^2.2.8: + version "2.2.8" + resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.8.tgz#033b117d6a2e975804f6f0969dbe7d3d0b355131" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" From 24b5f2b926c9dfbd018a4ce957cb79952b7e2254 Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Fri, 6 Oct 2017 12:38:56 +0800 Subject: [PATCH 005/318] add brand, emailTemplate, responseTemplate --- src/__tests__/brandMutations.test.js | 114 ++++++++++++++++++ src/__tests__/emailTemplateMutations.test.js | 89 ++++++++++++++ .../responseTemplateMutations.test.js | 104 ++++++++++++++++ src/data/resolvers/mutations/brands.js | 51 ++++++++ src/data/resolvers/mutations/emailTemplate.js | 40 ++++++ src/data/resolvers/mutations/index.js | 6 + .../resolvers/mutations/responseTemplate.js | 39 ++++++ src/data/schema/brand.js | 7 ++ src/data/schema/emailTemplate.js | 6 + src/data/schema/index.js | 17 ++- src/data/schema/responseTemplate.js | 8 ++ src/db/factories.js | 50 ++++++++ src/db/models/Brands.js | 27 ++++- src/db/models/ResponseTemplates.js | 4 +- 14 files changed, 557 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/brandMutations.test.js create mode 100644 src/__tests__/emailTemplateMutations.test.js create mode 100644 src/__tests__/responseTemplateMutations.test.js create mode 100644 src/data/resolvers/mutations/brands.js create mode 100644 src/data/resolvers/mutations/emailTemplate.js create mode 100644 src/data/resolvers/mutations/responseTemplate.js create mode 100644 src/db/factories.js diff --git a/src/__tests__/brandMutations.test.js b/src/__tests__/brandMutations.test.js new file mode 100644 index 000000000..e35d6fd25 --- /dev/null +++ b/src/__tests__/brandMutations.test.js @@ -0,0 +1,114 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Brands, Users } from '../db/models'; +import { brandFactory, userFactory } from '../db/factories'; +import brandMutations from '../data/resolvers/mutations/brands'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Brands mutations', () => { + let _brand; + let _user; + + beforeEach(async () => { + // Creating test data + _brand = await brandFactory(); + _user = await userFactory(); + }); + + afterEach(async () => { + // Clearing test data + await Brands.remove({}); + await Users.remove({}); + }); + + test('Create brand', async () => { + const brandObj = await brandMutations.brandsAdd( + {}, + { code: _brand.code, name: _brand.name, description: _brand.description }, + { user: _user }, + ); + expect(brandObj).toBeDefined(); + expect(brandObj.code).toBe(_brand.code); + expect(brandObj.name).toBe(_brand.name); + expect(brandObj.userId).toBe(_user._id); + + // invalid data + expect(() => + brandMutations.brandsAdd({}, { code: '', name: _brand.name }, { user: _user }), + ).toThrowError('Code is required field'); + + // Login required + expect(() => + brandMutations.brandsAdd({}, { code: _brand.code, name: brandObj.name }, {}), + ).toThrowError('Login required'); + }); + + test('Update brand', async () => { + // get new brand object + const _brand_update = await brandFactory(); + + // update brand object + const brandObj = await brandMutations.brandsEdit( + {}, + { + _id: _brand.id, + code: _brand_update.code, + name: _brand_update.name, + description: _brand_update.description, + }, + { user: _user }, + ); + + // check changes + expect(brandObj.code).toBe(_brand_update.code); + expect(brandObj.name).toBe(_brand_update.name); + expect(brandObj.description).toBe(_brand_update.description); + }); + + test('Update brand login required', async () => { + expect.assertions(1); + try { + await brandMutations.brandsEdit({}, { _id: _brand.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Delete brand', async () => { + const brandDeletedObj = await brandMutations.brandsRemove( + {}, + { _id: _brand.id }, + { user: _user }, + ); + expect(brandDeletedObj.id).toBe(_brand.id); + + const brandObj = await Brands.findOne({ _id: _brand.id }); + expect(brandObj).toBeNull(); + }); + + test('Delete brand login required', async () => { + expect.assertions(1); + try { + await brandMutations.brandsRemove({}, { _id: _brand.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Update brand email config', async () => { + const brandObj = await brandMutations.brandsConfigEmail( + {}, + { _id: _brand.id, emailConfig: _brand.emailConfig }, + { user: _brand.userId }, + ); + + expect(brandObj).toBeDefined(); + expect(brandObj.emailConfig.type).toBe(_brand.emailConfig.type); + expect(brandObj.emailConfig.template).toBe(_brand.emailConfig.template); + }); +}); diff --git a/src/__tests__/emailTemplateMutations.test.js b/src/__tests__/emailTemplateMutations.test.js new file mode 100644 index 000000000..acc7b5494 --- /dev/null +++ b/src/__tests__/emailTemplateMutations.test.js @@ -0,0 +1,89 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { EmailTemplates, Users } from '../db/models'; +import { emailTemplateFactory, userFactory } from '../db/factories'; +import emailTemplateMutations from '../data/resolvers/mutations/emailTemplate'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Email template mutations', () => { + let _emailTemplate; + let _user; + + beforeEach(async () => { + // Creating test data + _emailTemplate = await emailTemplateFactory(); + _user = await userFactory(); + }); + + afterEach(async () => { + // Clearing test data + await EmailTemplates.remove({}); + await Users.remove({}); + }); + + test('Create email template', async () => { + const emailTemplateObj = await emailTemplateMutations.emailTemplateAdd( + {}, + { name: _emailTemplate.name, content: _emailTemplate.content }, + { user: _user }, + ); + expect(emailTemplateObj).toBeDefined(); + expect(emailTemplateObj.name).toBe(_emailTemplate.name); + expect(emailTemplateObj.content).toBe(_emailTemplate.content); + + // Login required test + expect(() => + emailTemplateMutations.emailTemplateAdd( + {}, + { name: _emailTemplate.name, content: _emailTemplate.content }, + {}, + ), + ).toThrowError('Login required'); + }); + + test('Update email template', async () => { + const emailTemplateObj = await emailTemplateMutations.emailTemplateEdit( + {}, + { _id: _emailTemplate.id, name: _emailTemplate.name, content: _emailTemplate.content }, + { user: _user }, + ); + expect(emailTemplateObj).toBeDefined(); + expect(emailTemplateObj.id).toBe(_emailTemplate.id); + expect(emailTemplateObj.name).toBe(_emailTemplate.name); + expect(emailTemplateObj.content).toBe(_emailTemplate.content); + }); + + test('Update email template login required', async () => { + expect.assertions(1); + try { + await emailTemplateMutations.emailTemplateEdit({}, { _id: _emailTemplate.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Delete email template', async () => { + const deletedObj = await emailTemplateMutations.emailTemplateRemove( + {}, + { _id: _emailTemplate.id }, + { user: _user }, + ); + expect(deletedObj.id).toBe(_emailTemplate.id); + const emailTemplateObj = await EmailTemplates.findOne({ _id: _emailTemplate.id }); + expect(emailTemplateObj).toBeNull(); + }); + + test('Delete email template login required', async () => { + expect.assertions(1); + try { + await emailTemplateMutations.emailTemplateRemove({}, { _id: _emailTemplate.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); +}); diff --git a/src/__tests__/responseTemplateMutations.test.js b/src/__tests__/responseTemplateMutations.test.js new file mode 100644 index 000000000..a26f36784 --- /dev/null +++ b/src/__tests__/responseTemplateMutations.test.js @@ -0,0 +1,104 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { ResponseTemplates, Users } from '../db/models'; +import { responseTemplateFactory, userFactory } from '../db/factories'; +import responseTemplateMutations from '../data/resolvers/mutations/responseTemplate'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Response template mutations', () => { + let _responseTemplate; + let _user; + + beforeEach(async () => { + // Creating test data + _responseTemplate = await responseTemplateFactory(); + _user = await userFactory(); + }); + + afterEach(async () => { + // Clearing test data + await ResponseTemplates.remove({}); + await Users.remove({}); + }); + + test('Create response template', async () => { + const responseTemplateObj = await responseTemplateMutations.responseTemplateAdd( + {}, + { + name: _responseTemplate.name, + content: _responseTemplate.content, + brandId: _responseTemplate.brandId, + files: _responseTemplate.files, + }, + { user: _user }, + ); + expect(responseTemplateObj).toBeDefined(); + expect(responseTemplateObj.name).toBe(_responseTemplate.name); + expect(responseTemplateObj.content).toBe(_responseTemplate.content); + expect(responseTemplateObj.brandId).toBe(_responseTemplate.brandId); + expect(responseTemplateObj.files[0]).toBe(_responseTemplate.files[0]); + + // login required test + expect(() => + responseTemplateMutations.responseTemplateAdd( + {}, + { name: _responseTemplate.name, content: _responseTemplate.content }, + {}, + ), + ).toThrowError('Login required'); + }); + + test('Update response template', async () => { + const responseTemplateObj = await responseTemplateMutations.responseTemplateEdit( + {}, + { + _id: _responseTemplate.id, + name: _responseTemplate.name, + content: _responseTemplate.content, + brandId: _responseTemplate.brandId, + files: _responseTemplate.files, + }, + { user: _user }, + ); + expect(responseTemplateObj).toBeDefined(); + expect(responseTemplateObj.id).toBe(_responseTemplate.id); + expect(responseTemplateObj.name).toBe(_responseTemplate.name); + expect(responseTemplateObj.content).toBe(_responseTemplate.content); + expect(responseTemplateObj.brandId).toBe(_responseTemplate.brandId); + expect(responseTemplateObj.files[0]).toBe(_responseTemplate.files[0]); + }); + + test('Update response template login required', async () => { + expect.assertions(1); + try { + await responseTemplateMutations.responseTemplateEdit({}, { _id: _responseTemplate.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Delete response template', async () => { + const deletedObj = await responseTemplateMutations.responseTemplateRemove( + {}, + { _id: _responseTemplate.id }, + { user: _user }, + ); + expect(deletedObj.id).toBe(_responseTemplate.id); + const emailTemplateObj = await ResponseTemplates.findOne({ _id: _responseTemplate.id }); + expect(emailTemplateObj).toBeNull(); + }); + + test('Delete response template login required', async () => { + expect.assertions(1); + try { + await responseTemplateMutations.responseTemplateRemove({}, { _id: _responseTemplate.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); +}); diff --git a/src/data/resolvers/mutations/brands.js b/src/data/resolvers/mutations/brands.js new file mode 100644 index 000000000..34ce77dc6 --- /dev/null +++ b/src/data/resolvers/mutations/brands.js @@ -0,0 +1,51 @@ +import { Brands } from '../../../db/models'; + +export default { + /** + * Create new brand + * @return {Promise} brand object + */ + brandsAdd(root, { code, name, description }, { user }) { + if (!user) throw new Error('Login required'); + + if (!code) throw new Error('Code is required field'); + + return Brands.createBrand({ code, name, description, userId: user.id }); + }, + + /** + * Update brand + * @return {Promise} brand object + */ + async brandsEdit(root, { _id, code, name, description }, { user }) { + if (!user) throw new Error('Login required'); + + await Brands.update({ _id }, { code, name, description }); + return Brands.findOne({ _id }); + }, + + /** + * Delete brand + * @return {Promise} + */ + async brandsRemove(root, { _id }, { user }) { + if (!user) throw new Error('Login required'); + + const brandObj = await Brands.findOne({ _id }); + + if (!brandObj) throw new Error('Brand not found with id ' + _id); + + return brandObj.remove(); + }, + + /** + * Update brands email config + * @return {Promise} brand object + */ + async brandsConfigEmail(root, { _id, emailConfig }, { user }) { + if (!user) throw new Error('Login required'); + + await Brands.update({ _id }, { emailConfig }); + return Brands.findOne({ _id }); + }, +}; diff --git a/src/data/resolvers/mutations/emailTemplate.js b/src/data/resolvers/mutations/emailTemplate.js new file mode 100644 index 000000000..b77a4774c --- /dev/null +++ b/src/data/resolvers/mutations/emailTemplate.js @@ -0,0 +1,40 @@ +import { EmailTemplates } from '../../../db/models'; + +export default { + /** + * Create new email template + * @return {Promise} email template object + */ + emailTemplateAdd(root, { name, content }, { user }) { + if (!user) throw new Error('Login required'); + + return EmailTemplates.create({ name, content }); + }, + + /** + * Update email template + * @return {Promise} email template object + */ + async emailTemplateEdit(root, { _id, name, content }, { user }) { + if (!user) throw new Error('Login required'); + + await EmailTemplates.update({ _id }, { name, content }); + return EmailTemplates.findOne({ _id }); + }, + + /** + * Delete email template + * @return {Promise} + */ + async emailTemplateRemove(root, { _id }, { user }) { + if (!user) throw new Error('Login required'); + + const emailTemplateObj = await EmailTemplates.findOne({ _id }); + + if (!emailTemplateObj) { + throw new Error('Email template not found with id ' + _id); + } + + return emailTemplateObj.remove(); + }, +}; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index cd4940a8a..121e8170b 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -1,5 +1,11 @@ import conversation from './conversation'; +import brands from './brands'; +import emailTemplate from './emailTemplate'; +import responseTemplate from './responseTemplate'; export default { ...conversation, + ...brands, + ...emailTemplate, + ...responseTemplate, }; diff --git a/src/data/resolvers/mutations/responseTemplate.js b/src/data/resolvers/mutations/responseTemplate.js new file mode 100644 index 000000000..2e5500a15 --- /dev/null +++ b/src/data/resolvers/mutations/responseTemplate.js @@ -0,0 +1,39 @@ +import { ResponseTemplates } from '../../../db/models'; + +export default { + /** + * Create new response template + * @return {Promise} response template object + */ + responseTemplateAdd(root, { name, content, brandId, files }, { user }) { + if (!user) throw new Error('Login required'); + + return ResponseTemplates.create({ name, content, brandId, files }); + }, + + /** + * Update response template + * @return {Promise} response template object + */ + async responseTemplateEdit(root, { _id, name, content, brandId, files }, { user }) { + if (!user) throw new Error('Login required'); + + await ResponseTemplates.update({ _id }, { name, content, brandId, files }); + return ResponseTemplates.findOne({ _id }); + }, + + /** + * Delete response template + * @return {Promise} + */ + async responseTemplateRemove(root, { _id }, { user }) { + if (!user) throw new Error('Login required'); + + const responseTemplateObj = await ResponseTemplates.findOne({ _id }); + + if (!responseTemplateObj) { + throw new Error('Response template not found with id ' + _id); + } + return responseTemplateObj.remove(); + }, +}; diff --git a/src/data/schema/brand.js b/src/data/schema/brand.js index da906381a..2f67b7096 100644 --- a/src/data/schema/brand.js +++ b/src/data/schema/brand.js @@ -15,3 +15,10 @@ export const queries = ` brandDetail(_id: String!): Brand brandsTotalCount: Int `; + +export const mutations = ` + brandsAdd(code: String!, name: String, description: String): Brand + brandsEdit(_id: String!, code: String, name: String, description: String): Brand + brandsRemove(_id: String!): Brand + brandsConfigEmail(_id: String!, emailConfig: JSON): Brand +`; diff --git a/src/data/schema/emailTemplate.js b/src/data/schema/emailTemplate.js index 9402c3144..89f1482d1 100644 --- a/src/data/schema/emailTemplate.js +++ b/src/data/schema/emailTemplate.js @@ -10,3 +10,9 @@ export const queries = ` emailTemplates(limit: Int): [EmailTemplate] emailTemplatesTotalCount: Int `; + +export const mutations = ` + emailTemplateAdd(name: String, content: String): EmailTemplate + emailTemplateEdit(_id: String!, name: String, content: String): EmailTemplate + emailTemplateRemove(_id: String!): EmailTemplate +`; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index a575fc437..f14464736 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -2,13 +2,21 @@ import { types as UserTypes, queries as UserQueries } from './user'; import { types as ChannelTypes, queries as ChannelQueries } from './channel'; -import { types as BrandTypes, queries as BrandQueries } from './brand'; +import { types as BrandTypes, queries as BrandQueries, mutations as BrandMutations } from './brand'; import { types as IntegrationTypes, queries as IntegrationQueries } from './integration'; -import { types as ResponseTemplate, queries as ResponseTemplateQueries } from './responseTemplate'; +import { + types as ResponseTemplate, + queries as ResponseTemplateQueries, + mutations as ResponseTemplateMutations, +} from './responseTemplate'; -import { types as EmailTemplate, queries as EmailTemplateQueries } from './emailTemplate'; +import { + types as EmailTemplate, + queries as EmailTemplateQueries, + mutations as EmailTemplateMutations, +} from './emailTemplate'; import { types as FormTypes, queries as FormQueries } from './form'; @@ -68,6 +76,9 @@ export const queries = ` export const mutations = ` type Mutation { ${ConversationMutations} + ${BrandMutations} + ${ResponseTemplateMutations} + ${EmailTemplateMutations} } `; diff --git a/src/data/schema/responseTemplate.js b/src/data/schema/responseTemplate.js index b8d2c151e..cdfe1b2e9 100644 --- a/src/data/schema/responseTemplate.js +++ b/src/data/schema/responseTemplate.js @@ -13,3 +13,11 @@ export const queries = ` responseTemplates(limit: Int): [ResponseTemplate] responseTemplatesTotalCount: Int `; + +export const mutations = ` + responseTemplateAdd(name: String, content: String, brandId: String, files: JSON): + ResponseTemplate + responseTemplateEdit(_id: String!, name: String, content: String, brandId: String, files: JSON): + ResponseTemplate + responseTemplateRemove(_id: String!): ResponseTemplate +`; diff --git a/src/db/factories.js b/src/db/factories.js new file mode 100644 index 000000000..585f1ebd6 --- /dev/null +++ b/src/db/factories.js @@ -0,0 +1,50 @@ +import faker from 'faker'; +import Random from 'meteor-random'; + +import { Users, Brands, EmailTemplates, ResponseTemplates } from './models'; + +export const userFactory = (params = {}) => { + const user = new Users({ + username: params.username || faker.random.word(), + details: { + fullName: params.fullName || faker.random.word(), + }, + }); + + return user.save(); +}; + +export const brandFactory = (params = {}) => { + const brand = new Brands({ + name: faker.random.word(), + code: params.code || faker.random.word(), + userId: () => Random.id(), + description: params.description || faker.random.word(), + emailConfig: { + type: 'simple', + template: faker.random.word(), + }, + }); + + return brand.save(); +}; + +export const emailTemplateFactory = (params = {}) => { + const emailTemplate = new EmailTemplates({ + name: faker.random.word(), + content: params.content || faker.random.word(), + }); + + return emailTemplate.save(); +}; + +export const responseTemplateFactory = (params = {}) => { + const responseTemplate = new ResponseTemplates({ + name: faker.random.word(), + content: params.content || faker.random.word(), + brandId: params.brandId || Random.id(), + files: [faker.random.image()], + }); + + return responseTemplate.save(); +}; diff --git a/src/db/models/Brands.js b/src/db/models/Brands.js index f1b521f4e..8accf01d9 100644 --- a/src/db/models/Brands.js +++ b/src/db/models/Brands.js @@ -1,6 +1,14 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +const BrandEmailConfigSchema = mongoose.Schema({ + type: { + type: String, + allowedValues: ['simple', 'custom'], + }, + template: String, +}); + const BrandSchema = mongoose.Schema({ _id: { type: String, @@ -12,9 +20,26 @@ const BrandSchema = mongoose.Schema({ description: String, userId: String, createdAt: Date, - emailConfig: Object, + emailConfig: BrandEmailConfigSchema, }); +class Brand { + /** + * Create a brand + * @param {Object} brandObj object + * @return {Promise} Newly created brand object + */ + static createBrand(brandObj, emailConfigData) { + return this.create({ + ...brandObj, + createdAt: new Date(), + emailConfig: emailConfigData, + }); + } +} + +BrandSchema.loadClass(Brand); + const Brands = mongoose.model('brands', BrandSchema); export default Brands; diff --git a/src/db/models/ResponseTemplates.js b/src/db/models/ResponseTemplates.js index ae27cd10d..a3628c761 100644 --- a/src/db/models/ResponseTemplates.js +++ b/src/db/models/ResponseTemplates.js @@ -10,7 +10,9 @@ const ResponseTemplateSchema = mongoose.Schema({ name: String, content: String, brandId: String, - files: [Object], + files: { + type: Array, + }, }); const ResponseTemplates = mongoose.model('response_templates', ResponseTemplateSchema); From 0f756cc21bf542bc26c8475044042c34a1e6949f Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Fri, 6 Oct 2017 13:57:29 +0800 Subject: [PATCH 006/318] fix lint bug --- src/__tests__/emailTemplateMutations.test.js | 7 +++---- src/data/resolvers/mutations/brands.js | 12 ++++++------ src/data/resolvers/mutations/emailTemplate.js | 10 +++++----- src/data/resolvers/mutations/responseTemplate.js | 10 +++++----- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/__tests__/emailTemplateMutations.test.js b/src/__tests__/emailTemplateMutations.test.js index acc7b5494..025fb345f 100644 --- a/src/__tests__/emailTemplateMutations.test.js +++ b/src/__tests__/emailTemplateMutations.test.js @@ -68,14 +68,13 @@ describe('Email template mutations', () => { }); test('Delete email template', async () => { - const deletedObj = await emailTemplateMutations.emailTemplateRemove( + await emailTemplateMutations.emailTemplateRemove( {}, { _id: _emailTemplate.id }, { user: _user }, ); - expect(deletedObj.id).toBe(_emailTemplate.id); - const emailTemplateObj = await EmailTemplates.findOne({ _id: _emailTemplate.id }); - expect(emailTemplateObj).toBeNull(); + const count = await EmailTemplates.find({ _id: _emailTemplate.id }).count(); + expect(count).toBe(0); }); test('Delete email template login required', async () => { diff --git a/src/data/resolvers/mutations/brands.js b/src/data/resolvers/mutations/brands.js index 34ce77dc6..d978d94d6 100644 --- a/src/data/resolvers/mutations/brands.js +++ b/src/data/resolvers/mutations/brands.js @@ -5,22 +5,22 @@ export default { * Create new brand * @return {Promise} brand object */ - brandsAdd(root, { code, name, description }, { user }) { + brandsAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); - if (!code) throw new Error('Code is required field'); + if (!doc.code) throw new Error('Code is required field'); - return Brands.createBrand({ code, name, description, userId: user.id }); + return Brands.createBrand({ userId: user._id, ...doc }); }, /** * Update brand * @return {Promise} brand object */ - async brandsEdit(root, { _id, code, name, description }, { user }) { + async brandsEdit(root, { _id, ...fields }, { user }) { if (!user) throw new Error('Login required'); - await Brands.update({ _id }, { code, name, description }); + await Brands.update({ _id }, { ...fields }); return Brands.findOne({ _id }); }, @@ -33,7 +33,7 @@ export default { const brandObj = await Brands.findOne({ _id }); - if (!brandObj) throw new Error('Brand not found with id ' + _id); + if (!brandObj) throw new Error(`Brand not found with id ${_id}`); return brandObj.remove(); }, diff --git a/src/data/resolvers/mutations/emailTemplate.js b/src/data/resolvers/mutations/emailTemplate.js index b77a4774c..ffa4cc24d 100644 --- a/src/data/resolvers/mutations/emailTemplate.js +++ b/src/data/resolvers/mutations/emailTemplate.js @@ -5,20 +5,20 @@ export default { * Create new email template * @return {Promise} email template object */ - emailTemplateAdd(root, { name, content }, { user }) { + emailTemplateAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); - return EmailTemplates.create({ name, content }); + return EmailTemplates.create({ ...doc }); }, /** * Update email template * @return {Promise} email template object */ - async emailTemplateEdit(root, { _id, name, content }, { user }) { + async emailTemplateEdit(root, { _id, ...fields }, { user }) { if (!user) throw new Error('Login required'); - await EmailTemplates.update({ _id }, { name, content }); + await EmailTemplates.update({ _id }, { ...fields }); return EmailTemplates.findOne({ _id }); }, @@ -32,7 +32,7 @@ export default { const emailTemplateObj = await EmailTemplates.findOne({ _id }); if (!emailTemplateObj) { - throw new Error('Email template not found with id ' + _id); + throw new Error(`Email template not found with id ${_id}`); } return emailTemplateObj.remove(); diff --git a/src/data/resolvers/mutations/responseTemplate.js b/src/data/resolvers/mutations/responseTemplate.js index 2e5500a15..dfd4da098 100644 --- a/src/data/resolvers/mutations/responseTemplate.js +++ b/src/data/resolvers/mutations/responseTemplate.js @@ -5,20 +5,20 @@ export default { * Create new response template * @return {Promise} response template object */ - responseTemplateAdd(root, { name, content, brandId, files }, { user }) { + responseTemplateAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); - return ResponseTemplates.create({ name, content, brandId, files }); + return ResponseTemplates.create({ ...doc }); }, /** * Update response template * @return {Promise} response template object */ - async responseTemplateEdit(root, { _id, name, content, brandId, files }, { user }) { + async responseTemplateEdit(root, { _id, ...fields }, { user }) { if (!user) throw new Error('Login required'); - await ResponseTemplates.update({ _id }, { name, content, brandId, files }); + await ResponseTemplates.update({ _id }, { ...fields }); return ResponseTemplates.findOne({ _id }); }, @@ -32,7 +32,7 @@ export default { const responseTemplateObj = await ResponseTemplates.findOne({ _id }); if (!responseTemplateObj) { - throw new Error('Response template not found with id ' + _id); + throw new Error(`Response template not found with id ${_id}`); } return responseTemplateObj.remove(); }, From bcd5b6b21cb567c05f00411b6af107707a72fb4c Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Fri, 6 Oct 2017 14:17:16 +0800 Subject: [PATCH 007/318] fix override update fields --- src/data/resolvers/mutations/brands.js | 2 +- src/data/resolvers/mutations/emailTemplate.js | 4 ++-- src/data/resolvers/mutations/responseTemplate.js | 5 +++-- src/db/models/Brands.js | 5 ++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/data/resolvers/mutations/brands.js b/src/data/resolvers/mutations/brands.js index d978d94d6..9248db359 100644 --- a/src/data/resolvers/mutations/brands.js +++ b/src/data/resolvers/mutations/brands.js @@ -20,7 +20,7 @@ export default { async brandsEdit(root, { _id, ...fields }, { user }) { if (!user) throw new Error('Login required'); - await Brands.update({ _id }, { ...fields }); + await Brands.update({ _id }, { $set: { ...fields } }); return Brands.findOne({ _id }); }, diff --git a/src/data/resolvers/mutations/emailTemplate.js b/src/data/resolvers/mutations/emailTemplate.js index ffa4cc24d..0d5c2cde7 100644 --- a/src/data/resolvers/mutations/emailTemplate.js +++ b/src/data/resolvers/mutations/emailTemplate.js @@ -8,7 +8,7 @@ export default { emailTemplateAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); - return EmailTemplates.create({ ...doc }); + return EmailTemplates.create(doc); }, /** @@ -18,7 +18,7 @@ export default { async emailTemplateEdit(root, { _id, ...fields }, { user }) { if (!user) throw new Error('Login required'); - await EmailTemplates.update({ _id }, { ...fields }); + await EmailTemplates.update({ _id }, { $set: { ...fields } }); return EmailTemplates.findOne({ _id }); }, diff --git a/src/data/resolvers/mutations/responseTemplate.js b/src/data/resolvers/mutations/responseTemplate.js index dfd4da098..b18b4ddf4 100644 --- a/src/data/resolvers/mutations/responseTemplate.js +++ b/src/data/resolvers/mutations/responseTemplate.js @@ -8,7 +8,7 @@ export default { responseTemplateAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); - return ResponseTemplates.create({ ...doc }); + return ResponseTemplates.create(doc); }, /** @@ -18,7 +18,7 @@ export default { async responseTemplateEdit(root, { _id, ...fields }, { user }) { if (!user) throw new Error('Login required'); - await ResponseTemplates.update({ _id }, { ...fields }); + await ResponseTemplates.update({ _id }, { $set: { ...fields } }); return ResponseTemplates.findOne({ _id }); }, @@ -34,6 +34,7 @@ export default { if (!responseTemplateObj) { throw new Error(`Response template not found with id ${_id}`); } + return responseTemplateObj.remove(); }, }; diff --git a/src/db/models/Brands.js b/src/db/models/Brands.js index 8accf01d9..ee8a1bc9f 100644 --- a/src/db/models/Brands.js +++ b/src/db/models/Brands.js @@ -29,11 +29,10 @@ class Brand { * @param {Object} brandObj object * @return {Promise} Newly created brand object */ - static createBrand(brandObj, emailConfigData) { + static createBrand(doc) { return this.create({ - ...brandObj, + ...doc, createdAt: new Date(), - emailConfig: emailConfigData, }); } } From 236e2fb1771a54d9d5c681cea83415541d5296f2 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 6 Oct 2017 14:20:56 +0800 Subject: [PATCH 008/318] Remove unnessary test index file --- src/__tests__/index.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/__tests__/index.js diff --git a/src/__tests__/index.js b/src/__tests__/index.js deleted file mode 100644 index e69de29bb..000000000 From 32a660501cfa097f889629e971a6718e3d1e813f Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 7 Oct 2017 05:39:01 +0800 Subject: [PATCH 009/318] channel, form, integration mutations --- package.json | 7 +- src/__tests__/channelMutations.test.js | 124 +++++ src/__tests__/formMutations.test.js | 328 ++++++++++++ src/__tests__/integrationMutations.test.js | 533 ++++++++++++++++++++ src/data/resolvers/mutations/channel.js | 38 ++ src/data/resolvers/mutations/form.js | 111 ++++ src/data/resolvers/mutations/index.js | 6 + src/data/resolvers/mutations/integration.js | 102 ++++ src/data/schema/brand.js | 7 + src/data/schema/channel.js | 23 +- src/data/schema/form.js | 43 ++ src/data/schema/index.js | 17 +- src/data/schema/integration.js | 74 ++- src/db/constants.js | 21 + src/db/factories.js | 102 ++++ src/db/models/Channels.js | 90 +++- src/db/models/Forms.js | 135 ++++- src/db/models/Integrations.js | 175 ++++++- test.config.json | 4 + yarn.lock | 4 + 20 files changed, 1905 insertions(+), 39 deletions(-) create mode 100644 src/__tests__/channelMutations.test.js create mode 100644 src/__tests__/formMutations.test.js create mode 100644 src/__tests__/integrationMutations.test.js create mode 100644 src/data/resolvers/mutations/channel.js create mode 100644 src/data/resolvers/mutations/form.js create mode 100644 src/data/resolvers/mutations/integration.js create mode 100644 src/db/constants.js create mode 100644 src/db/factories.js create mode 100644 test.config.json diff --git a/package.json b/package.json index 561cf99eb..2a81def97 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "build": "babel src --out-dir dist --ignore __tests__,tests --copy-files", "lint": "eslint src", "format": "prettier --write --print-width 100 --single-quote --trailing-comma all 'src/**/*.js'", - "precommit": "lint-staged" + "precommit": "lint-staged", + "test": "jest --config=test.config.json" }, "lint-staged": { "*.js": [ @@ -32,6 +33,7 @@ "cors": "^2.8.1", "dotenv": "^4.0.0", "express": "^4.15.2", + "faker": "^4.1.0", "graphql": "^0.10.1", "graphql-server-core": "^0.8.2", "graphql-server-express": "^0.8.2", @@ -57,6 +59,7 @@ "jest": "^21.2.1", "lint-staged": "^3.6.0", "nodemon": "^1.11.0", - "prettier": "^1.4.4" + "prettier": "^1.4.4", + "shortid": "^2.2.8" } } diff --git a/src/__tests__/channelMutations.test.js b/src/__tests__/channelMutations.test.js new file mode 100644 index 000000000..48deeb56f --- /dev/null +++ b/src/__tests__/channelMutations.test.js @@ -0,0 +1,124 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { userFactory, integrationFactory } from '../db/factories'; +import { Channels, Users, Integrations } from '../db/models'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('channel creation tests', () => { + let _user; + let _user2; + let _integration; + + /** + * Before each test create test data + * containing 2 users and an integration + */ + beforeEach(async () => { + _user = await userFactory({}); + _integration = await integrationFactory({}); + _user2 = await userFactory({}); + }); + + /** + * After each test remove the test data + */ + afterEach(async () => { + await Channels.remove({}); + await Users.remove({}); + await Integrations.remove({}); + }); + + test('create channel tests', async () => { + try { + Channels.createChannel({ + name: 'Channel test', + }); + } catch (e) { + expect(e.value).toBe('channel.create.exception'); + expect(e.message).toBe('userId must be supplied'); + } + + const doc = { + name: 'Channel test', + userId: _user._id, + memberIds: [_user2._id], + integrationIds: [_integration._id], + }; + + const channel = await Channels.createChannel(doc); + expect(channel.memberIds.length).toBe(2); + expect(channel.conversationCount).toBe(0); + expect(channel.openConversationCount).toBe(0); + }); +}); + +describe('channel update tests', () => { + let _user; + let _user2; + let _integration; + /** + * Before each test create test data + * containing 2 users and an integration + */ + beforeEach(async () => { + _user = await userFactory({}); + _integration = await integrationFactory({}); + _user2 = await userFactory({}); + }); + + /** + * After each test remove the test data + */ + afterEach(async () => { + await Channels.remove({}); + await Users.remove({}); + await Integrations.remove({}); + }); + + test('update channel tests', async () => { + const doc = { + name: 'Channel test', + userId: _user._id, + memberIds: [_user2._id], + integrationIds: [_integration._id], + }; + + let channel = await Channels.createChannel(doc); + + doc.memberIds = [_user2._id]; + await Channels.updateChannel(channel._id, doc); + channel = await Channels.findOne({ _id: channel._id }); + expect(channel.memberIds.length).toBe(2); + + doc.memberIds = [_user._id]; + await Channels.updateChannel(channel._id, doc); + channel = await Channels.findOne({ _id: channel._id }); + expect(channel.memberIds.length).toBe(1); + expect(channel.memberIds[0]).toBe(_user._id); + }); +}); + +describe('channel remove test', () => { + let _channel; + /** + * Before each test create test data + * containing 2 users and an integration + */ + beforeEach(async () => { + const user = await userFactory({}); + _channel = await Channels.createChannel({ + name: 'Channel test', + userId: user._id, + }); + }); + + test('channel remove test', async () => { + await Channels.removeChannel(_channel._id); + const channelCount = await Channels.find({}).count(); + expect(channelCount).toBe(0); + }); +}); diff --git a/src/__tests__/formMutations.test.js b/src/__tests__/formMutations.test.js new file mode 100644 index 000000000..f1c3ae510 --- /dev/null +++ b/src/__tests__/formMutations.test.js @@ -0,0 +1,328 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { userFactory, formFactory, formFieldFactory } from '../db/factories'; +import { Forms, Users, FormFields } from '../db/models'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('form creation tests', () => { + let _user; + /** + * Testing with an _user object + */ + beforeEach(async () => { + _user = await userFactory({}); + }); + + /** + * Deleting the data that was used in test + */ + afterEach(async () => { + await Users.remove({}); + await Forms.remove({}); + }); + + test('form creation test without userId supplied', async () => { + expect.assertions(1); + try { + await Forms.createForm({ + title: 'Test form', + description: 'Test form description', + }); + } catch (e) { + expect(e.message).toEqual('createdUserId must be supplied'); + } + }), + test('form creating tests', async () => { + let form = await Forms.createForm({ + title: 'Test form', + description: 'Test form description', + createdUserId: _user._id, + }); + + form = await Forms.findOne({ _id: form._id }); + + expect(form.title).toBe('Test form'); + expect(form.description).toBe('Test form description'); + expect(typeof form.code).toBe('string'); + expect(form.code.length).toEqual(6); + expect(typeof form.createdDate).toBe('object'); + expect(form.createdUserId).toBe(_user._id); + }); +}); + +describe('form update tests', () => { + let _user; + /** + * Testing with an _user object + */ + beforeEach(async () => { + _user = await userFactory({}); + }); + + /** + * Deleting the data that was used in test + */ + afterEach(async () => { + await Users.remove({}); + await Forms.remove({}); + }); + + test('form update tests', async () => { + const form = await Forms.createForm({ + title: 'Test form', + description: 'Test form description', + createdUserId: _user._id, + }); + + await Forms.updateForm({ + id: form._id, + title: 'Test form 2', + description: 'Test form description 2', + }); + + const formAfterUpdate = await Forms.findOne({ _id: form._id }); + expect(formAfterUpdate.title).toBe('Test form 2'); + expect(formAfterUpdate.description).toBe('Test form description 2'); + expect(form.createdUserId).toBe(formAfterUpdate.createdUserId); + expect(form.code).toBe(formAfterUpdate.code); + expect(typeof form.createdDate).toBe('object'); + }); +}); + +describe('form remove tests', async () => { + let _user; + /** + * Testing with an _user object + */ + beforeEach(async () => { + _user = await userFactory({}); + }); + + /** + * Deleting the data that was used in test + */ + afterEach(async () => { + await Users.remove({}); + await Forms.remove({}); + }); + + test('form removal test', async () => { + const form = await Forms.createForm({ + title: 'Test form', + description: 'Test form description', + createdUserId: _user._id, + }); + + await Forms.removeForm(form._id); + const formCount = await Forms.find({}).count(); + expect(formCount).toBe(0); + }); +}); + +describe('add form field test', async () => { + let _user; + let _form; + /** + * Testing with an _user object and a _form object + */ + beforeEach(async () => { + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + }); + + /** + * Deleting the data that was used in test + */ + afterEach(async () => { + await Users.remove({}); + await FormFields.remove({}); + await Forms.remove({}); + }); + + test('add form field test', async () => { + const newFormField = await FormFields.createFormField(_form._id, { + type: 'input', + validation: 'number', + text: 'How old are you?', + description: 'Form field description', + options: ['This', 'should', 'not', 'be', 'here', 'tho'], + isRequired: false, + }); + + expect(newFormField.formId).toEqual(_form._id); + expect(newFormField.order).toEqual(0); + expect(newFormField.type).toEqual('input'); + expect(newFormField.validation).toEqual('number'); + expect(newFormField.text).toEqual('How old are you?'); + expect(newFormField.description).toEqual('Form field description'); + expect.arrayContaining(newFormField.options); + expect(newFormField.isRequired).toEqual(false); + }); +}); + +describe('update form field test', async () => { + let _user; + let _form; + let _form_field; + /** + * Testing with an _user object and a _form object + */ + beforeEach(async () => { + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + _form_field = await formFieldFactory(_form._id, {}); + }); + + /** + * Deleting the data that was used in test + */ + afterEach(async () => { + await Users.remove({}); + await FormFields.remove({}); + await Forms.remove({}); + }); + + test('update form field test', async () => { + let updatedFormField = await FormFields.updateFormField(_form_field._id, { + type: 'input 1', + validation: 'number 1', + text: 'How old are you? 1', + description: 'Form field description 1', + options: ['This', 'should', 'not', 'be', 'here', 'tho', '1'], + isRequired: true, + }); + + updatedFormField = await FormFields.findOne({ _id: _form_field._id }); + expect(updatedFormField.formId).toEqual(_form._id); + expect(updatedFormField.type).toEqual('input 1'); + expect(updatedFormField.validation).toEqual('number 1'); + expect(updatedFormField.text).toEqual('How old are you? 1'); + expect(updatedFormField.description).toEqual('Form field description 1'); + expect.arrayContaining(updatedFormField.options); + expect(updatedFormField.options.length).toEqual(7); + expect(updatedFormField.isRequired).toEqual(true); + }); +}); + +describe('remove form field test', async () => { + let _user; + let _form; + let _form_field; + /** + * Testing with an _user object and a _form object + */ + beforeEach(async () => { + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + _form_field = await formFieldFactory(_form._id, {}); + }); + + /** + * Deleting the data that was used in test + */ + afterEach(async () => { + await Users.remove({}); + await FormFields.remove({}); + await Forms.remove({}); + }); + + test('remove form field test', async () => { + await FormFields.removeFormField(_form_field._id); + expect(await FormFields.find({}).count()).toEqual(0); + }); +}); + +describe('test of update order of form fields', async () => { + let _user; + let _form; + let _form_field; + let _form_field2; + let _form_field3; + /** + * Testing with an _user object and a _form object with 3 fields in it + * to test the setting the new order + */ + beforeEach(async () => { + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + _form_field = await formFieldFactory(_form._id, {}); + _form_field2 = await formFieldFactory(_form._id, {}); + _form_field3 = await formFieldFactory(_form._id, {}); + }); + + /** + * Deleting the data that was used in test + */ + afterEach(async () => { + await Users.remove({}); + await FormFields.remove({}); + await Forms.remove({}); + }); + + test('test of update order of form fields', async () => { + expect(_form_field.order).toBe(0); + expect(_form_field2.order).toBe(1); + expect(_form_field3.order).toBe(2); + + const orderDictArray = [ + { id: _form_field3._id, order: 10 }, + { id: _form_field2._id, order: 9 }, + { id: _form_field._id, order: 8 }, + ]; + + await Forms.updateFormFieldsOrder(orderDictArray); + const ff1 = await FormFields.findOne({ _id: _form_field3._id }); + expect(ff1.order).toBe(10); + expect(ff1.text).toBe(_form_field3.text); + + const ff2 = await FormFields.findOne({ _id: _form_field2._id }); + expect(ff2.order).toBe(9); + expect(ff2.text).toBe(_form_field2.text); + + const ff3 = await FormFields.findOne({ _id: _form_field._id }); + expect(ff3.order).toBe(8); + expect(ff3.text).toBe(_form_field.text); + }); +}); + +describe('test of form duplication', async () => { + let _user; + let _form; + /** + * Testing with an _user object and a _form object with 3 fields in it + */ + beforeEach(async () => { + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + await formFieldFactory(_form._id, {}); + await formFieldFactory(_form._id, {}); + await formFieldFactory(_form._id, {}); + }); + + /** + * Deleting the data that was used in test + */ + afterEach(async () => { + await Users.remove({}); + await FormFields.remove({}); + await Forms.remove({}); + }); + + test('test of form duplication', async () => { + const duplicatedForm = await Forms.duplicate(_form._id); + expect(duplicatedForm.title).toBe(`${_form.title} duplicated`); + expect(duplicatedForm.description).toBe(_form.description); + expect(typeof duplicatedForm.code).toBe('string'); + expect(duplicatedForm.code.length).toEqual(6); + expect(duplicatedForm.createdUserId).toBe(_form.createdUserId); + + const formFieldsCount = await FormFields.find({}).count(); + const duplicateFormFieldsCount = await FormFields.find({ formId: duplicatedForm._id }).count(); + expect(formFieldsCount).toEqual(6); + expect(duplicateFormFieldsCount).toEqual(3); + }); +}); diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js new file mode 100644 index 000000000..146ab3fe5 --- /dev/null +++ b/src/__tests__/integrationMutations.test.js @@ -0,0 +1,533 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ +import faker from 'faker'; +import { connect, disconnect } from '../db/connection'; +import { brandFactory, integrationFactory, formFactory, userFactory } from '../db/factories'; +import { Integrations, Brands, Users, Forms } from '../db/models'; +import mutations from '../data/resolvers/mutations'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('messenger integration add test', () => { + let _brand; + /** + */ + beforeEach(async () => { + _brand = await brandFactory({}); + }); + + /** + */ + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + }); + + test('messenger integration add test', async () => { + const integration = await Integrations.createMessengerIntegration({ + name: 'Integration test', + brandId: _brand._id, + }); + + expect(integration.name).toBe('Integration test'); + expect(integration.brandId).toBe(_brand._id); + expect(integration.kind).toBe('messenger'); + }); +}); + +describe('messenger integration edit test', () => { + let _brand; + let _integration; + /** + */ + beforeEach(async () => { + _brand = await brandFactory({}); + _integration = await integrationFactory({ + kind: 'messenger', + }); + }); + + /** + */ + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + }); + + test('messenger integration edit test', async () => { + await Integrations.updateMessengerIntegration(_integration._id, { + name: 'Integration test 2', + brandId: _brand._id, + kind: 'new kind', + }); + + const updatedIntegration = await Integrations.findOne({ _id: _integration._id }); + + expect(updatedIntegration.name).toBe('Integration test 2'); + expect(updatedIntegration.brandId).toBe(_brand._id); + expect(updatedIntegration.kind).toBe('messenger'); + }); +}); + +describe('create form integration test without formData', () => { + let _brand; + let _form; + let _user; + /** + */ + beforeEach(async () => { + _brand = await brandFactory({}); + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + }); + + /** + */ + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + await Users.remove({}); + await Forms.remove({}); + }); + + test('create form integration test wihtout formData', async () => { + expect.assertions(1); + const mainDoc = { + name: 'form integration test', + brandId: _brand._id, + formId: _form._id, + }; + + try { + await Integrations.createFormIntegration(mainDoc); + } catch (e) { + expect(e).toEqual('formData must be supplied'); + } + }); +}); + +describe('create form integration test', () => { + let _brand; + let _form; + let _user; + /** + */ + beforeEach(async () => { + _brand = await brandFactory({}); + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + }); + + /** + */ + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + await Users.remove({}); + await Forms.remove({}); + }); + + test('create form integration test wihtout formData', async () => { + const mainDoc = { + name: 'form integration test', + brandId: _brand._id, + formId: _form._id, + }; + + const formData = { + loadType: 'embedded', + }; + + const integration = await Integrations.createFormIntegration({ ...mainDoc, formData }); + + expect(integration.formId).toEqual(_form._id); + expect(integration.name).toEqual('form integration test'); + expect(integration.brandId).toEqual(_brand._id); + expect(integration.formData.loadType).toEqual('embedded'); + expect(integration.kind).toEqual('form'); + }); +}); + +describe('edit form integration test', () => { + let _brand; + let _brand2; + let _form; + let _form2; + let _user; + let _form_integration; + /** + */ + beforeEach(async () => { + _brand = await brandFactory({}); + _brand2 = await brandFactory({}); + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + _form2 = await formFactory({ createdUserId: _user._id }); + _form_integration = await integrationFactory({ + name: 'form integration test', + brandId: _brand._id, + formId: _form._id, + kind: 'form', + formData: { + loadType: 'embedded', + }, + }); + }); + + /** + */ + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + await Users.remove({}); + await Forms.remove({}); + }); + + test('edit form integration test', async () => { + const mainDoc = { + name: 'form integration test 2', + brandId: _brand2._id, + formId: _form2._id, + }; + + const formData = { + loadType: 'shoutbox', + }; + + await Integrations.updateFormIntegration(_form_integration._id, { + ...mainDoc, + formData, + }); + + const integration = await Integrations.findOne({ _id: _form_integration._id }); + + expect(integration.name).toEqual('form integration test 2'); + expect(integration.formId).toEqual(_form2._id); + expect(integration.brandId).toEqual(_brand2._id); + expect(integration.formData.loadType).toEqual('shoutbox'); + }); +}); + +describe('remove integration test', () => { + let _brand; + let _integration; + /** + */ + beforeEach(async () => { + _brand = await brandFactory({}); + _integration = await integrationFactory({ + name: 'form integration test', + brandId: _brand._id, + kind: 'form', + }); + }); + + /** + */ + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + }); + + test('remove form integration test', async () => { + await mutations.integrationsRemove(null, { id: _integration._id }); + const integrationCount = await Integrations.find({}).count(); + expect(integrationCount).toEqual(0); + }); +}); + +describe('save integration messenger appearance test', () => { + let _brand; + let _integration; + /** + */ + beforeEach(async () => { + _brand = await brandFactory({}); + _integration = await integrationFactory({ + name: 'messenger integration test', + brandId: _brand._id, + kind: 'messenger', + }); + }); + + /** + */ + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + }); + + test('save integration messenger appearance test', async () => { + const uiOptions = { + color: faker.random.word(), + wallpaper: faker.random.word(), + logo: faker.random.word(), + }; + + await Integrations.saveMessengerAppearanceData(_integration._id, uiOptions); + + const integration = await Integrations.findOne({ _id: _integration._id }); + + expect(integration.uiOptions.color).toEqual(uiOptions.color); + expect(integration.uiOptions.wallpaper).toEqual(uiOptions.wallpaper); + expect(integration.uiOptions.logo).toEqual(uiOptions.logo); + }); +}); + +describe('save integration messenger configurations test', () => { + let _brand; + let _integration; + + /** + */ + beforeEach(async () => { + _brand = await brandFactory({}); + _integration = await integrationFactory({ + name: 'messenger integration test', + brandId: _brand._id, + kind: 'messenger', + }); + }); + + /** + */ + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + }); + + test('save integration messenger configurations test', async () => { + const messengerData = { + notifyCustomer: true, + availabilityMethod: 'manual', + isOnline: false, + onlineHours: [ + { + day: 'Monday', + from: '8am', + to: '12pm', + }, + { + day: 'Monday', + from: '2pm', + to: '6pm', + }, + ], + timezone: 'CET', + welcomeMessage: 'Welcome user', + awayMessage: 'Bye bye', + thankYouMessage: 'Thank you', + }; + + await Integrations.saveMessengerConfigs(_integration._id, messengerData); + + const integration = await Integrations.findOne({ _id: _integration._id }); + + expect(integration.messengerData.notifyCustomer).toEqual(true); + expect(integration.messengerData.availabilityMethod).toEqual('manual'); + expect(integration.messengerData.isOnline).toEqual(false); + expect(integration.messengerData.onlineHours[0].day).toEqual('Monday'); + expect(integration.messengerData.onlineHours[0].from).toEqual('8am'); + expect(integration.messengerData.onlineHours[0].to).toEqual('12pm'); + expect(integration.messengerData.onlineHours[1].day).toEqual('Monday'); + expect(integration.messengerData.onlineHours[1].from).toEqual('2pm'); + expect(integration.messengerData.onlineHours[1].to).toEqual('6pm'); + expect(integration.messengerData.timezone).toEqual('CET'); + expect(integration.messengerData.welcomeMessage).toEqual('Welcome user'); + expect(integration.messengerData.awayMessage).toEqual('Bye bye'); + expect(integration.messengerData.thankYouMessage).toEqual('Thank you'); + + const newMessengerData = { + notifyCustomer: false, + availabilityMethod: 'auto', + isOnline: true, + onlineHours: [ + { + day: 'Tuesday', + from: '9am', + to: '1pm', + }, + { + day: 'Tuesday', + from: '3pm', + to: '7pm', + }, + ], + timezone: 'EET', + welcomeMessage: 'Welcome customer', + awayMessage: 'Good bye', + thankYouMessage: 'Gracias', + }; + + await Integrations.saveMessengerConfigs(_integration._id, newMessengerData); + + const updatedIntegration = await Integrations.findOne({ _id: _integration._id }); + + expect(updatedIntegration.messengerData.notifyCustomer).toEqual(false); + expect(updatedIntegration.messengerData.availabilityMethod).toEqual('auto'); + expect(updatedIntegration.messengerData.isOnline).toEqual(true); + expect(updatedIntegration.messengerData.onlineHours[0].day).toEqual('Tuesday'); + expect(updatedIntegration.messengerData.onlineHours[0].from).toEqual('9am'); + expect(updatedIntegration.messengerData.onlineHours[0].to).toEqual('1pm'); + expect(updatedIntegration.messengerData.onlineHours[1].day).toEqual('Tuesday'); + expect(updatedIntegration.messengerData.onlineHours[1].from).toEqual('3pm'); + expect(updatedIntegration.messengerData.onlineHours[1].to).toEqual('7pm'); + expect(updatedIntegration.messengerData.timezone).toEqual('EET'); + expect(updatedIntegration.messengerData.welcomeMessage).toEqual('Welcome customer'); + expect(updatedIntegration.messengerData.awayMessage).toEqual('Good bye'); + expect(updatedIntegration.messengerData.thankYouMessage).toEqual('Gracias'); + }); +}); + +describe('mutation test', () => { + let _brand; + let _brand2; + let _user; + let _form; + let _form2; + + beforeEach(async () => { + _brand = await brandFactory({}); + _brand2 = await brandFactory({}); + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + _form2 = await formFactory({ createdUserId: _user._id }); + }), + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + await Users.remove({}); + await Forms.remove({}); + }); + + test('mutation test', async () => { + let integration = await mutations.integrationsCreateMessengerIntegration(null, { + name: 'Integration test', + brandId: _brand._id, + }); + + expect(integration.name).toBe('Integration test'); + expect(integration.brandId).toBe(_brand._id); + expect(integration.kind).toBe('messenger'); + + await mutations.integrationsEditMessengerIntegration(null, { + id: integration._id, + name: 'Integration test 2', + brandId: _brand2._id, + }); + + integration = await Integrations.findOne({ _id: integration._id }); + + expect(integration.name).toEqual('Integration test 2'); + expect(integration.brandId).toEqual(_brand2._id); + + const uiOptions = { + color: faker.random.word(), + wallpaper: faker.random.word(), + logo: faker.random.word(), + }; + + await mutations.integrationsSaveMessengerAppearanceData(null, { + id: integration._id, + uiOptions, + }); + + integration = await Integrations.findOne({ _id: integration._id }); + + expect(integration.uiOptions.color).toEqual(uiOptions.color); + expect(integration.uiOptions.wallpaper).toEqual(uiOptions.wallpaper); + expect(integration.uiOptions.logo).toEqual(uiOptions.logo); + + let mainDoc = { + name: 'form integration test', + brandId: _brand._id, + formId: _form._id, + }; + + let formData = { + loadType: 'embedded', + }; + + let integration2 = await mutations.integrationsCreateFormIntegration(null, { + ...mainDoc, + formData, + }); + expect(integration2.formId).toEqual(_form._id); + expect(integration2.name).toEqual('form integration test'); + expect(integration2.brandId).toEqual(_brand._id); + expect(integration2.formData.loadType).toEqual('embedded'); + + mainDoc = { + name: 'form integration test 2', + brandId: _brand2._id, + formId: _form2._id, + }; + + formData = { + loadType: 'shoutbox', + }; + + await mutations.integrationsEditFormIntegration(null, { + id: integration2._id, + ...mainDoc, + formData, + }); + + const updatedIntegration = await Integrations.findOne({ _id: integration2._id }); + + expect(updatedIntegration.name).toEqual('form integration test 2'); + expect(updatedIntegration.formId).toEqual(_form2._id); + expect(updatedIntegration.brandId).toEqual(_brand2._id); + expect(updatedIntegration.formData.loadType).toEqual('shoutbox'); + + const messengerData = { + notifyCustomer: true, + availabilityMethod: 'manual', + isOnline: false, + onlineHours: [ + { + day: 'Monday', + from: '8am', + to: '12pm', + }, + { + day: 'Monday', + from: '2pm', + to: '6pm', + }, + ], + timezone: 'CET', + welcomeMessage: 'Welcome user', + awayMessage: 'Bye bye', + thankYouMessage: 'Thank you', + }; + + await mutations.integrationsSaveMessengerConfigs(null, { id: integration._id, messengerData }); + + integration = await Integrations.findOne({ _id: integration._id }); + + expect(integration.messengerData.notifyCustomer).toEqual(true); + expect(integration.messengerData.availabilityMethod).toEqual('manual'); + expect(integration.messengerData.isOnline).toEqual(false); + expect(integration.messengerData.onlineHours[0].day).toEqual('Monday'); + expect(integration.messengerData.onlineHours[0].from).toEqual('8am'); + expect(integration.messengerData.onlineHours[0].to).toEqual('12pm'); + expect(integration.messengerData.onlineHours[1].day).toEqual('Monday'); + expect(integration.messengerData.onlineHours[1].from).toEqual('2pm'); + expect(integration.messengerData.onlineHours[1].to).toEqual('6pm'); + expect(integration.messengerData.timezone).toEqual('CET'); + expect(integration.messengerData.welcomeMessage).toEqual('Welcome user'); + expect(integration.messengerData.awayMessage).toEqual('Bye bye'); + expect(integration.messengerData.thankYouMessage).toEqual('Thank you'); + + const integrations = await Integrations.find({}, { _id: 1 }); + const promiseArray = integrations.map(i => { + return mutations.integrationsRemove(null, { id: i._id }); + }); + await Promise.all(promiseArray); + + const integrationCount = await Integrations.find({}).count(); + expect(integrationCount).toEqual(0); + }); +}); diff --git a/src/data/resolvers/mutations/channel.js b/src/data/resolvers/mutations/channel.js new file mode 100644 index 000000000..65505091f --- /dev/null +++ b/src/data/resolvers/mutations/channel.js @@ -0,0 +1,38 @@ +import { Channels } from '../../../db/models'; +export default { + /** + * Create a new channel and send notifications to its members bar the creator + * @param {Object} + * @param {Object} args + * @return {Promise} returns true + */ + channelsCreate(root, args) { + // TODO: sendNotifications method should here + return Channels.createChannel(args); + }, + + /** + * Update channel data + * @param {Object} + * @param {String} args.id + * @param {Object} args + * @return {Promise} returns mongoose model update method return value + */ + channelsUpdate(root, args) { + const { id } = args; + delete args.id; + Channels.updateChannel(id, args); + // TODO: sendNotifications method shoul be here + return; + }, + + /** + * Remove a channel + * @param {Object} + * @param {String} id + * @return {Promise} null + */ + channelsRemove(root, id) { + return Channels.remove(id); + }, +}; diff --git a/src/data/resolvers/mutations/form.js b/src/data/resolvers/mutations/form.js new file mode 100644 index 000000000..c31f63756 --- /dev/null +++ b/src/data/resolvers/mutations/form.js @@ -0,0 +1,111 @@ +import { Forms, FormFields } from '../../../db/models'; +export default { + /** + * Create a new form + * @param {Object} + * @param {String} args.title + * @param {String} args.description + * @param {String} args.userId + * @return {Promise} returns the form + * @throws {Error} apollo level error based on validation + */ + formsCreate(root, args) { + return Forms.createForm(args); + }, + + /** + * Update form data + * @param {Object} + * @param {String} args.id + * @param {String} args.title + * @param {String} args.description + * @return {Promise} returns null + * @throws {Error} apollo level error based on validation + */ + async formsUpdate(root, args) { + await Forms.updateForm(args); + return; + }, + + /** + * Remove a form + * @param {Object} + * @param {String} id + * @return {Promise} null + * @throws apollo level error based on validation + */ + formsRemove(root, { id }) { + return Forms.removeForm(id); + }, + + /** + * Adds a form field to the form + * @param {Object} + * @param {String} args.formId + * @param {String} args.type + * @param {String} args.validation + * @param {String} args.text + * @param {String} args.description + * @param {Array} args.options + * @param {Boolean} args.isRequired + * @return {Promise} return Promise(null) + * @throws {Error} throws apollo error based on validation + */ + formsAddFormField(root, args) { + const { formId } = args; + delete args.formId; + return FormFields.createFormField(formId, args); + }, + + /** + * @param {String} args.id form field id + * @param {String} args.type + * @param {String} args.validation + * @param {String} args.text + * @param {String} args.description + * @param {Array} args.options + * @param {Boolean} args.isRequired + * @return {Promise} return Promise(null) + * @throws {Error} throws apollo error based on validation + */ + formsUpdateFormField(root, args) { + const { id } = args; + delete args.id; + return FormFields.updateFormField(id, args); + }, + + /** + * Remove a channel + * @param {Object} + * @param {String} id + * @return {Promise} null + * @throws {Error} throws apollo error based on validation + */ + formsRemoveFormField(root, args) { + const { id } = args; + return FormFields.removeFormField(id); + }, + + /** + * Rearranges order based on given value + * @param {Object} + * @param {Array} args.orderDics + * @return {Promise} null + * @throws {Error} throws apollo error based on validation + */ + formsUpdateFormFieldsOrder(root, { orderDics }) { + return Forms.updateFormFieldsOrder(orderDics); + }, + + /** + * Duplicates the form and its fields + * @param {Object} + * @param {String} id + * @return {Promise} returns form object + * @throws {Error} throws apollo error based on validation + */ + formsDuplicate(root, args) { + const { id } = args; + return Forms.duplicate(id); + }, +}; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index cd4940a8a..d7f7503f6 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -1,5 +1,11 @@ import conversation from './conversation'; +import channel from './channel'; +import form from './form'; +import integration from './integration'; export default { ...conversation, + ...channel, + ...form, + ...integration, }; diff --git a/src/data/resolvers/mutations/integration.js b/src/data/resolvers/mutations/integration.js new file mode 100644 index 000000000..8693446ab --- /dev/null +++ b/src/data/resolvers/mutations/integration.js @@ -0,0 +1,102 @@ +import { Integrations } from '../../../db/models'; + +export default { + /** + * Create a new messenger integration + * @param {Object} + * @param {String} doc.title + * @param {String} doc.brandId + * @return {Promise} returns the messenger integration + * @throws {Error} apollo level error based on validation + */ + integrationsCreateMessengerIntegration(root, doc) { + return Integrations.createMessengerIntegration(doc); + }, + + /** + * Edit a messenger integration + * @param {Object} + * @param {String} args.id + * @param {String} args.title + * @param {String} args.brandId + * @return {Promise} returns null + * @throws {Error} apollo level error based on validation + */ + integrationsEditMessengerIntegration(root, { id, ...fields }) { + return Integrations.updateMessengerIntegration(id, fields); + }, + + /** + * Edit/save messenger appearance data + * @param {Object} + * @param {String} args.id + * @param {String} args.color + * @param {String} args.wallpaper + * @param {String} args.logo + * @return {Promise} returns null + * @throws {Error} apollo level error based on validation + */ + integrationsSaveMessengerAppearanceData(root, { id, uiOptions }) { + return Integrations.saveMessengerAppearanceData(id, uiOptions); + }, + + /** + * Edit/save messenger data + * @param {Object} + * @param {String} args.id + * @param {Boolean} args.notifyCustomer + * @param {String} args.availabilityMethod + * @param {Boolean} args.isOnline + * @param {String} args.onlineHours.day + * @param {String} args.onlineHours.from + * @param {String} args.onlineHours.to + * @param {String} args.timezone + * @param {String} args.welcomeMessage + * @param {String} args.awayMessage + * @param {String} args.thankYouMessage + * @return {Promise} returns null + * @throws {Error} apollo level error based on validation + */ + integrationsSaveMessengerConfigs(root, { id, messengerData }) { + return Integrations.saveMessengerConfigs(id, messengerData); + }, + + /** + * Create a new messenger integration + * @param {Object} + * @param {String} doc.title + * @param {String} doc.brandId + * @param {String} doc.formId + * @param {Object} doc.formData + * @return {Promise} returns the messenger integration + * @throws {Error} apollo level error based on validation + */ + integrationsCreateFormIntegration(root, doc) { + return Integrations.createFormIntegration(doc); + }, + + /** + * Edit a form integration + * @param {Object} + * @param {String} doc.title + * @param {String} doc.brandId + * @param {String} doc.formId + * @param {Object} doc.formData + * @return {Promise} returns null + * @throws {Error} apollo level error based on validation + */ + integrationsEditFormIntegration(root, { id, ...doc }) { + return Integrations.updateFormIntegration(id, doc); + }, + + /** + * Delete an integration + * @param {Object} + * @param {String} args.id + * @return {Promise} returns the messenger integration + * @throws {Error} apollo level error based on validation + */ + integrationsRemove(root, { id }) { + return Integrations.removeIntegration({ _id: id }); + }, +}; diff --git a/src/data/schema/brand.js b/src/data/schema/brand.js index da906381a..b093cc4bc 100644 --- a/src/data/schema/brand.js +++ b/src/data/schema/brand.js @@ -10,6 +10,13 @@ export const types = ` } `; +export const mutations = ` + brandsAdd(code: String!, name: String, description: String): Brand + brandsEdit(_id: String!, code: String, name: String, description: String): Brand + brandsRemove(_id: String!): Brand + brandsConfigEmail(_id: String!, emailConfig: JSON): Brand +`; + export const queries = ` brands(limit: Int): [Brand] brandDetail(_id: String!): Brand diff --git a/src/data/schema/channel.js b/src/data/schema/channel.js index af231e617..12bd304e8 100644 --- a/src/data/schema/channel.js +++ b/src/data/schema/channel.js @@ -1,12 +1,12 @@ export const types = ` type Channel { _id: String! - name: String + name: String! description: String integrationIds: [String] memberIds: [String] createdAt: Date - userId: String + userId: String! conversationCount: Int openConversationCount: Int } @@ -16,3 +16,22 @@ export const queries = ` channels(limit: Int, memberIds: [String]): [Channel] channelsTotalCount: Int `; + +export const mutations = ` + channelsCreate( + name: String!, + description: String, + memberIds: [String], + integrationIds: [String], + userId: String!): Channel + + channelsUpdate( + id: String!, + name: String!, + description: String, + memberIds: [String], + integrationIds: [String], + userId: String!): Boolean + + channelsRemove(id: String!): Boolean +`; diff --git a/src/data/schema/form.js b/src/data/schema/form.js index d7cc8fb68..a71b0c181 100644 --- a/src/data/schema/form.js +++ b/src/data/schema/form.js @@ -21,6 +21,49 @@ export const types = ` isRequired: Boolean order: Int } + + input DicItem { + id: String! + order: Int! + } +`; + +export const mutations = ` + formsCreate( + title: String!, + description: String, + createdUserId: String!): Form + + formsUpdate( + id: String!, + title: String!, + description: String): Boolean + + formsRemove(id: String!): Boolean + + formsAddFormField( + formId: String!, + type: String!, + validation: String, + text: String, + description: String, + options: [String], + isRequired: Boolean): FormField + + formsUpdateFormField( + id: String!, + type: String!, + validation: String, + text: String, + description: String, + options: [String], + isRequired: Boolean): Boolean + + formsRemoveFormField(id: String!): Boolean + + formsUpdateFormFieldsOrder(orderDics: [DicItem]): Boolean + + formsDuplicate(id: String!): Form `; export const queries = ` diff --git a/src/data/schema/index.js b/src/data/schema/index.js index a575fc437..6fb2eac29 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -1,16 +1,24 @@ import { types as UserTypes, queries as UserQueries } from './user'; -import { types as ChannelTypes, queries as ChannelQueries } from './channel'; +import { + types as ChannelTypes, + queries as ChannelQueries, + mutations as ChannelMutations, +} from './channel'; import { types as BrandTypes, queries as BrandQueries } from './brand'; -import { types as IntegrationTypes, queries as IntegrationQueries } from './integration'; +import { + types as IntegrationTypes, + queries as IntegrationQueries, + mutations as IntegrationMutations, +} from './integration'; import { types as ResponseTemplate, queries as ResponseTemplateQueries } from './responseTemplate'; import { types as EmailTemplate, queries as EmailTemplateQueries } from './emailTemplate'; -import { types as FormTypes, queries as FormQueries } from './form'; +import { types as FormTypes, queries as FormQueries, mutations as FormMutatons } from './form'; import { types as EngageTypes, queries as EngageQueries } from './engage'; @@ -68,6 +76,9 @@ export const queries = ` export const mutations = ` type Mutation { ${ConversationMutations} + ${ChannelMutations} + ${FormMutatons} + ${IntegrationMutations} } `; diff --git a/src/data/schema/integration.js b/src/data/schema/integration.js index 07c970fb4..5df6fe3da 100644 --- a/src/data/schema/integration.js +++ b/src/data/schema/integration.js @@ -1,10 +1,10 @@ export const types = ` type Integration { _id: String! - kind: String - name: String + kind: String! + name: String! + brandId: String! code: String - brandId: String formId: String formData: JSON messengerData: JSON @@ -16,6 +16,44 @@ export const types = ` form: Form channels: [Channel] } + + input IntegrationFormData { + loadType: String + successAction: String + fromEmail: String, + userEmailTitle: String + userEmailContent: String + adminEmails: [String] + adminEmailTitle: String + adminEmailContent: String + thankContent: String + redirectUrl: String + } + + input MessengerOnlineHoursSchema { + _id: String + day: String + from: String + to: String + } + + input IntegrationMessengerData { + _id: String + notifyCustomer: Boolean + availabilityMethod: String + isOnline: Boolean, + onlineHours: [MessengerOnlineHoursSchema] + timezone: String + welcomeMessage: String + awayMessage: String + thankYouMessage: String + } + + input MessengerUIOptions { + color: String + wallpaper: String + logo: String + } `; export const queries = ` @@ -23,3 +61,33 @@ export const queries = ` integrationDetail(_id: String!): Integration integrationsTotalCount(kind: String): Int `; + +export const mutations = ` + integrationsCreateMessengerIntegration( + name: String!, + brandId: String!): Integration + + integrationsEditMessengerIntegration( + id: String!, + name: String!, + brandId: String!): Integration + + integrationsSaveMessengerAppearanceData(id: String!, uiOptions: MessengerUiOptions): Boolean + + integrationsSaveMessengerConfigs(id: String!, messengerData: IntegrationMessengerData): Boolean + + integrationsCreateFormIntegration( + name: String!, + brandId: String!, + formId: String, + formData: IntegrationFormData!): Integration + + integrationsEditFormIntegration( + id: String! + name: String!, + brandId: String!, + formId: String, + formData: IntegrationFormData!): Boolean + + integrationsRemove(id: String!): Boolean +`; diff --git a/src/db/constants.js b/src/db/constants.js new file mode 100644 index 000000000..392b8cbe9 --- /dev/null +++ b/src/db/constants.js @@ -0,0 +1,21 @@ +export const FORM_LOAD_TYPES = { + SHOUTBOX: 'shoutbox', + POPUP: 'popup', + EMBEDDED: 'embedded', + ALL_LIST: ['', 'shoutbox', 'popup', 'embedded'], +}; + +export const FORM_SUCCESS_ACTIONS = { + EMAIL: 'email', + REDIRECT: 'redirect', + ONPAGE: 'onPage', + ALL_LIST: ['', 'email', 'redirect', 'onPage'], +}; + +export const KIND_CHOICES = { + MESSENGER: 'messenger', + FORM: 'form', + TWITTER: 'twitter', + FACEBOOK: 'facebook', + ALL_LIST: ['messenger', 'form', 'twitter', 'facebook'], +}; diff --git a/src/db/factories.js b/src/db/factories.js new file mode 100644 index 000000000..dc4897d6e --- /dev/null +++ b/src/db/factories.js @@ -0,0 +1,102 @@ +import shortid from 'shortid'; +import faker from 'faker'; + +import { + Users, + Integrations, + Brands, + EmailTemplates, + ResponseTemplates, + Tags, + Forms, + FormFields, +} from './models'; + +export const userFactory = (params = {}) => { + const user = new Users({ + username: params.username || faker.random.word(), + details: { + fullName: params.fullName || faker.random.word(), + }, + }); + + return user.save(); +}; + +export const integrationFactory = params => { + const kind = params.kind || 'messenger'; + return Integrations.create({ + name: faker.random.word(), + kind: kind, + brandId: params.brandId || shortid.generate(), + formId: params.formId || shortid.generate(), + messengerData: { welcomeMessage: 'welcome' } || {}, + }); +}; + +export const brandFactory = (params = {}) => { + const brand = new Brands({ + name: faker.random.word(), + code: params.code || faker.random.word(), + userId: shortid.generate(), + description: params.description || faker.random.word(), + emailConfig: { + type: 'simple', + template: faker.random.word(), + }, + }); + + return brand.save(); +}; + +export const emailTemplateFactory = (params = {}) => { + const emailTemplate = new EmailTemplates({ + name: faker.random.word(), + content: params.content || faker.random.word(), + }); + + return emailTemplate.save(); +}; + +export const responseTemplateFactory = (params = {}) => { + const responseTemplate = new ResponseTemplates({ + name: faker.random.word(), + content: params.content || faker.random.word(), + brandId: params.brandId || shortid.generate(), + files: [faker.random.image()], + }); + + return responseTemplate.save(); +}; + +export const tagsFactory = (params = {}) => { + const tag = new Tags({ + name: faker.random.word(), + type: params.type || faker.random.word(), + colorCode: params.colorCode || shortid.generate(), + userId: shortid.generate(), + }); + + return tag.save(); +}; + +export const formFactory = ({ title, code, createdUserId }) => { + return Forms.createForm({ + title: title || faker.random.word(), + description: faker.random.word(), + code: code || shortid.generate(), + createdUserId, + }); +}; + +export const formFieldFactory = (formId, params) => { + return FormFields.createFormField(formId || shortid.id(), { + type: params.type || faker.random.word(), + name: faker.random.word(), + validation: params.validation || faker.random.word(), + text: faker.random.word(), + description: faker.random.word(), + isRequired: params.isRequired || false, + number: faker.random.word(), + }); +}; diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index 70b8d1358..f301094fc 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -1,22 +1,88 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; +import shortid from 'shortid'; +import { createdAtModifier } from '../plugins'; + +function ChannelCreationException(message) { + this.message = message; + this.value = 'channel.create.exception'; + this.toString = `${this.value} - ${this.value}`; +} const ChannelSchema = mongoose.Schema({ _id: { type: String, - unique: true, - default: () => Random.id(), + default: shortid.generate, + }, + name: { + type: String, + required: true, }, - name: String, description: String, - integrationIds: [String], - memberIds: [String], - createdAt: Date, - userId: String, - conversationCount: Number, - openConversationCount: Number, + // TODO: Check if regex id is available for use + integrationIds: { + type: [String], + }, + // TODO: Check if regex id is available for use + memberIds: { + type: [String], + }, + userId: { + type: String, + }, + conversationCount: { + type: Number, + }, + openConversationCount: { + type: Number, + }, }); -const Channels = mongoose.model('channels', ChannelSchema); +class Channel { + /** + * + */ + static preSave(doc) { + const { userId } = doc; + + if (!userId) { + throw new ChannelCreationException('userId must be supplied'); + } + + doc.memberIds = doc.memberIds || []; + + if (!doc.memberIds.includes(doc.userId)) { + doc.memberIds.push(doc.userId); + } + } + + /** + * Create a new channel, + * adds `userId` to the `memberIds` if it doesn't contain it + * @param {Object} args + * @return {Promise} Newly created channel obj + */ + static createChannel(doc) { + this.preSave(doc); + doc.conversationCount = 0; + doc.openConversationCount = 0; + return this.create(doc); + } + + static updateChannel(id, doc) { + if (doc && doc._id) { + delete doc._id; + } + + this.preSave(doc); + return this.update({ _id: id }, doc); + } + + static removeChannel(id) { + return this.remove({ _id: id }); + } +} + +ChannelSchema.plugin(createdAtModifier); +ChannelSchema.loadClass(Channel); -export default Channels; +export default mongoose.model('channels', ChannelSchema); diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index 9b186f89c..4df2b2be0 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -1,35 +1,152 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; +import shortid from 'shortid'; +import Integrations from './Integrations'; const FormSchema = mongoose.Schema({ _id: { type: String, - unique: true, - default: () => Random.id(), + default: shortid.generate, }, title: String, - code: String, description: String, + code: String, createdUserId: String, createdDate: Date, }); +class Form { + static async generateCode() { + // generate code automatically + let code = shortid.generate().substr(0, 6); + let foundForm = await Forms.findOne({ code }); + + while (foundForm) { + code = shortid.generate().substr(0, 6); + foundForm = await Forms.findOne({ code }); + } + + return code; + } + + static async createForm(doc) { + const { createdUserId } = doc; + + if (!createdUserId) { + return Promise.reject(new Error('createdUserId must be supplied')); + } + + doc.code = await this.generateCode(); + doc.createdDate = new Date(); + return this.create(doc); + } + + static updateForm(doc) { + let { id } = doc; + if (doc && doc.id) { + delete doc.id; + } + + return this.update({ _id: id }, doc); + } + + static async removeForm(id) { + const fieldCount = await FormFields.find({ formId: id }).count(); + + if (fieldCount > 0) { + throw 'You cannot delete this form. This form has some fields.'; + } + + const integrationCount = await Integrations.find({ formId: id }).count(); + + if (integrationCount > 0) { + throw 'You cannot delete this form. This form used in integration.'; + } + + return this.remove({ _id: id }); + } + + static updateFormFieldsOrder(orderDics) { + // update each field's order + orderDics.forEach(async ({ id, order }) => { + await FormFields.updateFormField(id, { order }); + }); + } + + static async duplicate(id) { + const form = await this.findOne({ _id: id }); + + // duplicate form + const newForm = await this.createForm({ + title: `${form.title} duplicated`, + description: form.description, + createdUserId: form.createdUserId, + }); + + // duplicate fields + const formFields = await FormFields.find({ formId: id }); + + const promiseArray = formFields.map(field => { + const content = FormFields.createFormField(newForm._id, { + type: field.type, + validation: field.validation, + text: field.text, + description: field.description, + options: field.options, + isRequired: field.isRequired, + order: field.order, + }); + return content; + }); + await Promise.all(promiseArray); + return newForm; + } +} + +FormSchema.loadClass(Form); export const Forms = mongoose.model('forms', FormSchema); -const FieldSchema = mongoose.Schema({ +const FormFieldSchema = mongoose.Schema({ _id: { type: String, - unique: true, - default: () => Random.id(), + default: shortid.generate, }, - formId: String, type: String, validation: String, text: String, description: String, options: [String], isRequired: Boolean, + formId: { + required: true, + type: String, + }, order: Number, }); -export const FormFields = mongoose.model('form_fields', FieldSchema); +class FormField { + static async createFormField(formId, doc) { + const lastField = await FormFields.findOne({}, { order: 1 }, { sort: { order: -1 } }); + + doc.formId = formId; + // if there is no field then start with 0 + let order = 0; + + if (lastField) { + order = lastField.order + 1; + } + + doc.order = order; + return this.create(doc); + } + + static updateFormField(id, doc) { + return this.update({ _id: id }, doc); + } + + static removeFormField(id) { + return this.remove({ _id: id }); + } +} + +FormFieldSchema.loadClass(FormField); +export const FormFields = mongoose.model('form_fields', FormFieldSchema); diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index 4cc3017e1..18c7244cc 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -1,23 +1,182 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; +import shortid from 'shortid'; +import { Messages, Conversations } from './Conversations'; +import { Customers } from './Customers'; +import { KIND_CHOICES, FORM_SUCCESS_ACTIONS, FORM_LOAD_TYPES } from '../constants'; + +const MessengerOnlineHoursSchema = mongoose.Schema({ + _id: { + type: String, + }, + day: { + type: String, + }, + from: { + type: String, + }, + to: { + type: String, + }, +}); + +const MessengerDataSchema = mongoose.Schema({ + notifyCustomer: { + type: Boolean, + }, + + // manual, auto + availabilityMethod: { + type: String, + enum: ['manual', 'auto'], + }, + isOnline: { + type: Boolean, + }, + onlineHours: [MessengerOnlineHoursSchema], + timezone: { + type: String, + }, + welcomeMessage: { + type: String, + }, + awayMessage: { + type: String, + }, + thankYouMessage: { + type: String, + }, +}); + +const FormDataSchema = mongoose.Schema({ + loadType: { + type: String, + enum: FORM_LOAD_TYPES.ALL_LIST, + }, + successAction: { + type: String, + enum: FORM_SUCCESS_ACTIONS.ALL_LIST, + }, + fromEmail: { + type: String, + }, + userEmailTitle: { + type: String, + }, + userEmailContent: { + type: String, + }, + adminEmails: { + type: [String], + }, + adminEmailTitle: { + type: String, + }, + adminEmailContent: { + type: String, + }, + thankContent: { + type: String, + }, + redirectUrl: { + type: String, + }, +}); + +const UiOptionsSchema = mongoose.Schema({ + color: String, + wallpaper: String, + logo: String, +}); const IntegrationSchema = mongoose.Schema({ _id: { type: String, - unique: true, - default: () => Random.id(), + default: shortid.generate, }, kind: String, name: String, brandId: String, formId: String, - formData: Object, - messengerData: Object, + formData: FormDataSchema, + messengerData: MessengerDataSchema, twitterData: Object, facebookData: Object, - uiOptions: Object, + uiOptions: UiOptionsSchema, }); -const Integrations = mongoose.model('integrations', IntegrationSchema); +class Integration { + static generateFormDoc(mainDoc, formData) { + return { + ...mainDoc, + kind: KIND_CHOICES.FORM, + formData, + }; + } + + static createIntegration(doc) { + return this.create(doc); + } + + static createMessengerIntegration({ name, brandId }) { + return this.createIntegration({ + name, + brandId, + kind: KIND_CHOICES.MESSENGER, + }); + } + + static updateMessengerIntegration(id, { name, brandId }) { + return this.update({ _id: id }, { name, brandId }, { runValidators: true }); + } + + static saveMessengerAppearanceData(id, { color, wallpaper, logo }) { + return this.update( + { _id: id }, + { uiOptions: { color, wallpaper, logo } }, + { runValdatiors: true }, + ); + } + + static saveMessengerConfigs(id, messengerData) { + return this.update({ _id: id }, { messengerData }, { runValidators: true }); + } + + static createFormIntegration({ formData, ...mainDoc }) { + const doc = this.generateFormDoc(mainDoc, formData); + + if (Object.keys(formData || {}).length === 0) { + throw 'formData must be supplied'; + } + + return this.create(doc); + } + + static updateFormIntegration(id, { formData, ...mainDoc }) { + const doc = this.generateFormDoc(mainDoc, formData); + return this.update({ _id: id }, doc, { runValidators: true }); + } + + static async removeIntegration(id) { + // remove messages + // conversations + const conversations = await Conversations.find({ integrationId: id }, { _id: true }); + + const conversationIds = []; + conversations.forEach(c => { + conversationIds.push(c._id); + }); + + await Messages.remove({ conversationId: { $in: conversationIds } }); + + // remove conversations + await Conversations.remove({ integrationId: id }); + + // remove customers + await Customers.remove({ integrationId: id }); + + return this.remove(id); + } +} -export default Integrations; +IntegrationSchema.loadClass(Integration); +export default mongoose.model('integrations', IntegrationSchema); diff --git a/test.config.json b/test.config.json new file mode 100644 index 000000000..dac0394ee --- /dev/null +++ b/test.config.json @@ -0,0 +1,4 @@ +{ + "testRegex": ".*test.js$", + "testEnvironment": "node" +} diff --git a/yarn.lock b/yarn.lock index 5fe574607..508f02936 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3998,6 +3998,10 @@ shellwords@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" +shortid@^2.2.8: + version "2.2.8" + resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.8.tgz#033b117d6a2e975804f6f0969dbe7d3d0b355131" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" From 3174c133fb22521be50b4dccc6d80b5403f299aa Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 7 Oct 2017 06:40:15 +0800 Subject: [PATCH 010/318] changes made related to last review comments --- src/__tests__/channelMutations.test.js | 30 ++++++++++++- src/__tests__/formMutations.test.js | 38 ++++++++-------- src/data/resolvers/mutations/channel.js | 35 +++++++++------ src/data/resolvers/mutations/form.js | 58 +++++++++++-------------- src/data/schema/channel.js | 4 +- src/data/schema/form.js | 18 ++++---- src/db/models/Channels.js | 8 ++-- src/db/models/Forms.js | 48 +++++++++----------- 8 files changed, 132 insertions(+), 107 deletions(-) diff --git a/src/__tests__/channelMutations.test.js b/src/__tests__/channelMutations.test.js index 48deeb56f..94a7716bc 100644 --- a/src/__tests__/channelMutations.test.js +++ b/src/__tests__/channelMutations.test.js @@ -44,15 +44,22 @@ describe('channel creation tests', () => { const doc = { name: 'Channel test', + description: 'test channel descripion', userId: _user._id, memberIds: [_user2._id], integrationIds: [_integration._id], }; const channel = await Channels.createChannel(doc); + + expect(channel.name).toEqual(doc.name); + expect(channel.description).toEqual(doc.description); expect(channel.memberIds.length).toBe(2); - expect(channel.conversationCount).toBe(0); - expect(channel.openConversationCount).toBe(0); + expect(channel.integrationIds.length).toEqual(1); + expect(channel.integrationIds[0]).toEqual(_integration._id); + expect(channel.userId).toEqual(doc.userId); + expect(channel.conversationCount).toEqual(0); + expect(channel.openConversationCount).toEqual(0); }); }); @@ -60,6 +67,7 @@ describe('channel update tests', () => { let _user; let _user2; let _integration; + /** * Before each test create test data * containing 2 users and an integration @@ -82,6 +90,7 @@ describe('channel update tests', () => { test('update channel tests', async () => { const doc = { name: 'Channel test', + description: 'Channel test description', userId: _user._id, memberIds: [_user2._id], integrationIds: [_integration._id], @@ -92,7 +101,16 @@ describe('channel update tests', () => { doc.memberIds = [_user2._id]; await Channels.updateChannel(channel._id, doc); channel = await Channels.findOne({ _id: channel._id }); + expect(channel.name).toEqual(doc.name); + expect(channel.description).toEqual(doc.description); expect(channel.memberIds.length).toBe(2); + expect(channel.memberIds[0]).toBe(_user2._id); + expect(channel.memberIds[1]).toBe(_user._id); + expect(channel.integrationIds.length).toEqual(1); + expect(channel.integrationIds[0]).toEqual(_integration._id); + expect(channel.userId).toEqual(doc.userId); + expect(channel.conversationCount).toEqual(0); + expect(channel.openConversationCount).toEqual(0); doc.memberIds = [_user._id]; await Channels.updateChannel(channel._id, doc); @@ -104,6 +122,7 @@ describe('channel update tests', () => { describe('channel remove test', () => { let _channel; + /** * Before each test create test data * containing 2 users and an integration @@ -116,6 +135,13 @@ describe('channel remove test', () => { }); }); + /** + * Remove test data + */ + afterEach(async () => { + await Channels.remove({}); + }); + test('channel remove test', async () => { await Channels.removeChannel(_channel._id); const channelCount = await Channels.find({}).count(); diff --git a/src/__tests__/formMutations.test.js b/src/__tests__/formMutations.test.js index f1c3ae510..d88c5d3dc 100644 --- a/src/__tests__/formMutations.test.js +++ b/src/__tests__/formMutations.test.js @@ -33,25 +33,26 @@ describe('form creation tests', () => { description: 'Test form description', }); } catch (e) { - expect(e.message).toEqual('createdUserId must be supplied'); + expect(e).toEqual('createdUserId must be supplied'); } - }), - test('form creating tests', async () => { - let form = await Forms.createForm({ - title: 'Test form', - description: 'Test form description', - createdUserId: _user._id, - }); - - form = await Forms.findOne({ _id: form._id }); + }); - expect(form.title).toBe('Test form'); - expect(form.description).toBe('Test form description'); - expect(typeof form.code).toBe('string'); - expect(form.code.length).toEqual(6); - expect(typeof form.createdDate).toBe('object'); - expect(form.createdUserId).toBe(_user._id); + test('form creating tests', async () => { + let form = await Forms.createForm({ + title: 'Test form', + description: 'Test form description', + createdUserId: _user._id, }); + + form = await Forms.findOne({ _id: form._id }); + + expect(form.title).toBe('Test form'); + expect(form.description).toBe('Test form description'); + expect(typeof form.code).toBe('string'); + expect(form.code.length).toEqual(6); + expect(typeof form.createdDate).toBe('object'); + expect(form.createdUserId).toBe(_user._id); + }); }); describe('form update tests', () => { @@ -78,8 +79,7 @@ describe('form update tests', () => { createdUserId: _user._id, }); - await Forms.updateForm({ - id: form._id, + await Forms.updateForm(form._id, { title: 'Test form 2', description: 'Test form description 2', }); @@ -314,6 +314,7 @@ describe('test of form duplication', async () => { test('test of form duplication', async () => { const duplicatedForm = await Forms.duplicate(_form._id); + expect(duplicatedForm.title).toBe(`${_form.title} duplicated`); expect(duplicatedForm.description).toBe(_form.description); expect(typeof duplicatedForm.code).toBe('string'); @@ -322,6 +323,7 @@ describe('test of form duplication', async () => { const formFieldsCount = await FormFields.find({}).count(); const duplicateFormFieldsCount = await FormFields.find({ formId: duplicatedForm._id }).count(); + expect(formFieldsCount).toEqual(6); expect(duplicateFormFieldsCount).toEqual(3); }); diff --git a/src/data/resolvers/mutations/channel.js b/src/data/resolvers/mutations/channel.js index 65505091f..129cccedb 100644 --- a/src/data/resolvers/mutations/channel.js +++ b/src/data/resolvers/mutations/channel.js @@ -1,27 +1,36 @@ import { Channels } from '../../../db/models'; + export default { /** * Create a new channel and send notifications to its members bar the creator * @param {Object} - * @param {Object} args - * @return {Promise} returns true + * @param {String} doc.name + * @param {String} doc.description + * @param {Array} doc.memberIds + * @param {Array} doc.integrationIds + * @param {String} doc.userId + * @return {Promise} returns channel object + * @throws {Error} throws apollo level validation errors */ - channelsCreate(root, args) { + channelsCreate(root, doc) { // TODO: sendNotifications method should here - return Channels.createChannel(args); + return Channels.createChannel(doc); }, /** * Update channel data * @param {Object} - * @param {String} args.id - * @param {Object} args - * @return {Promise} returns mongoose model update method return value + * @param {String} doc._id + * @param {String} doc.name + * @param {String} doc.description + * @param {Array} doc.memberIds + * @param {Array} doc.integrationIds + * @param {String} doc.userId + * @return {Promise} returns null + * @throws {Error} throws apollo level validation errors */ - channelsUpdate(root, args) { - const { id } = args; - delete args.id; - Channels.updateChannel(id, args); + channelsUpdate(root, { _id, ...doc }) { + Channels.updateChannel(_id, doc); // TODO: sendNotifications method shoul be here return; }, @@ -32,7 +41,7 @@ export default { * @param {String} id * @return {Promise} null */ - channelsRemove(root, id) { - return Channels.remove(id); + channelsRemove(root, { _id }) { + return Channels.remove(_id); }, }; diff --git a/src/data/resolvers/mutations/form.js b/src/data/resolvers/mutations/form.js index c31f63756..ada519467 100644 --- a/src/data/resolvers/mutations/form.js +++ b/src/data/resolvers/mutations/form.js @@ -3,39 +3,38 @@ export default { /** * Create a new form * @param {Object} - * @param {String} args.title - * @param {String} args.description - * @param {String} args.userId + * @param {String} doc.title + * @param {String} doc.description + * @param {String} doc.userId * @return {Promise} returns the form * @throws {Error} apollo level error based on validation */ - formsCreate(root, args) { - return Forms.createForm(args); + formsCreate(root, doc) { + return Forms.createForm(doc); }, /** * Update form data * @param {Object} - * @param {String} args.id - * @param {String} args.title - * @param {String} args.description + * @param {String} doc._id + * @param {String} doc.title + * @param {String} doc.description * @return {Promise} returns null * @throws {Error} apollo level error based on validation */ - async formsUpdate(root, args) { - await Forms.updateForm(args); - return; + formsEdit(root, { _id, ...doc }) { + return Forms.updateForm(_id, doc); }, /** * Remove a form * @param {Object} - * @param {String} id + * @param {String} _id * @return {Promise} null * @throws apollo level error based on validation */ - formsRemove(root, { id }) { - return Forms.removeForm(id); + formsRemove(root, { _id }) { + return Forms.removeForm(_id); }, /** @@ -51,14 +50,12 @@ export default { * @return {Promise} return Promise(null) * @throws {Error} throws apollo error based on validation */ - formsAddFormField(root, args) { - const { formId } = args; - delete args.formId; - return FormFields.createFormField(formId, args); + formsAddFormField(root, { formId, ...formFieldDoc }) { + return FormFields.createFormField(formId, formFieldDoc); }, /** - * @param {String} args.id form field id + * @param {String} args._id form field id * @param {String} args.type * @param {String} args.validation * @param {String} args.text @@ -68,28 +65,26 @@ export default { * @return {Promise} return Promise(null) * @throws {Error} throws apollo error based on validation */ - formsUpdateFormField(root, args) { - const { id } = args; - delete args.id; - return FormFields.updateFormField(id, args); + formsEditFormField(root, { _id, ...formFieldDoc }) { + return FormFields.updateFormField(_id, formFieldDoc); }, /** * Remove a channel * @param {Object} - * @param {String} id + * @param {String} _id * @return {Promise} null * @throws {Error} throws apollo error based on validation */ - formsRemoveFormField(root, args) { - const { id } = args; - return FormFields.removeFormField(id); + formsRemoveFormField(root, { _id }) { + return FormFields.removeFormField(_id); }, /** * Rearranges order based on given value * @param {Object} - * @param {Array} args.orderDics + * @param {String} args.orderDics.id + * @param {String} args.orderDics.order * @return {Promise} null * @throws {Error} throws apollo error based on validation */ @@ -100,12 +95,11 @@ export default { /** * Duplicates the form and its fields * @param {Object} - * @param {String} id + * @param {String} args._id * @return {Promise} returns form object * @throws {Error} throws apollo error based on validation */ - formsDuplicate(root, args) { - const { id } = args; - return Forms.duplicate(id); + formsDuplicate(root, { _id }) { + return Forms.duplicate(_id); }, }; diff --git a/src/data/schema/channel.js b/src/data/schema/channel.js index 12bd304e8..fcb28c58b 100644 --- a/src/data/schema/channel.js +++ b/src/data/schema/channel.js @@ -26,12 +26,12 @@ export const mutations = ` userId: String!): Channel channelsUpdate( - id: String!, + _id: String!, name: String!, description: String, memberIds: [String], integrationIds: [String], userId: String!): Boolean - channelsRemove(id: String!): Boolean + channelsRemove(_id: String!): Boolean `; diff --git a/src/data/schema/form.js b/src/data/schema/form.js index a71b0c181..cec0b7422 100644 --- a/src/data/schema/form.js +++ b/src/data/schema/form.js @@ -22,7 +22,7 @@ export const types = ` order: Int } - input DicItem { + input OrderDicItem { id: String! order: Int! } @@ -34,12 +34,12 @@ export const mutations = ` description: String, createdUserId: String!): Form - formsUpdate( - id: String!, + formsEdit( + _id: String!, title: String!, description: String): Boolean - formsRemove(id: String!): Boolean + formsRemove(_id: String!): Boolean formsAddFormField( formId: String!, @@ -50,8 +50,8 @@ export const mutations = ` options: [String], isRequired: Boolean): FormField - formsUpdateFormField( - id: String!, + formsEditFormField( + _id: String!, type: String!, validation: String, text: String, @@ -59,11 +59,11 @@ export const mutations = ` options: [String], isRequired: Boolean): Boolean - formsRemoveFormField(id: String!): Boolean + formsRemoveFormField(_id: String!): Boolean - formsUpdateFormFieldsOrder(orderDics: [DicItem]): Boolean + formsUpdateFormFieldsOrder(orderDics: [OrderDicItem]): Boolean - formsDuplicate(id: String!): Form + formsDuplicate(_id: String!): Form `; export const queries = ` diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index f301094fc..34279ef7b 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -68,17 +68,17 @@ class Channel { return this.create(doc); } - static updateChannel(id, doc) { + static updateChannel(_id, doc) { if (doc && doc._id) { delete doc._id; } this.preSave(doc); - return this.update({ _id: id }, doc); + return this.update({ _id }, doc); } - static removeChannel(id) { - return this.remove({ _id: id }); + static removeChannel(_id) { + return this.remove({ _id }); } } diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index 4df2b2be0..36b9f0b2e 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -32,48 +32,44 @@ class Form { const { createdUserId } = doc; if (!createdUserId) { - return Promise.reject(new Error('createdUserId must be supplied')); + throw 'createdUserId must be supplied'; } doc.code = await this.generateCode(); doc.createdDate = new Date(); + return this.create(doc); } - static updateForm(doc) { - let { id } = doc; - if (doc && doc.id) { - delete doc.id; - } - - return this.update({ _id: id }, doc); + static updateForm(_id, doc) { + return this.update({ _id }, doc); } - static async removeForm(id) { - const fieldCount = await FormFields.find({ formId: id }).count(); + static async removeForm(_id) { + const fieldCount = await FormFields.find({ formId: _id }).count(); if (fieldCount > 0) { throw 'You cannot delete this form. This form has some fields.'; } - const integrationCount = await Integrations.find({ formId: id }).count(); + const integrationCount = await Integrations.find({ formId: _id }).count(); if (integrationCount > 0) { throw 'You cannot delete this form. This form used in integration.'; } - return this.remove({ _id: id }); + return this.remove({ _id }); } - static updateFormFieldsOrder(orderDics) { + static async updateFormFieldsOrder(orderDics) { // update each field's order - orderDics.forEach(async ({ id, order }) => { - await FormFields.updateFormField(id, { order }); - }); + for (let orderDic of orderDics) { + await FormFields.updateFormField(orderDic.id, { order: orderDic.order }); + } } - static async duplicate(id) { - const form = await this.findOne({ _id: id }); + static async duplicate(_id) { + const form = await this.findOne({ _id }); // duplicate form const newForm = await this.createForm({ @@ -83,10 +79,10 @@ class Form { }); // duplicate fields - const formFields = await FormFields.find({ formId: id }); + const formFields = await FormFields.find({ formId: _id }); - const promiseArray = formFields.map(field => { - const content = FormFields.createFormField(newForm._id, { + for (let field of formFields) { + await FormFields.createFormField(newForm._id, { type: field.type, validation: field.validation, text: field.text, @@ -95,9 +91,8 @@ class Form { isRequired: field.isRequired, order: field.order, }); - return content; - }); - await Promise.all(promiseArray); + } + return newForm; } } @@ -126,16 +121,15 @@ const FormFieldSchema = mongoose.Schema({ class FormField { static async createFormField(formId, doc) { const lastField = await FormFields.findOne({}, { order: 1 }, { sort: { order: -1 } }); - doc.formId = formId; + // if there is no field then start with 0 let order = 0; - if (lastField) { order = lastField.order + 1; } - doc.order = order; + return this.create(doc); } From 47692fbf68b6f4d41217a2235bc0a942ebb92ea7 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 7 Oct 2017 14:41:09 +0800 Subject: [PATCH 011/318] Add segment mutations & tests --- src/__tests__/segmentMutations.test.js | 121 +++++++++++++++++++++++ src/data/resolvers/mutations/index.js | 2 + src/data/resolvers/mutations/segments.js | 33 +++++++ src/data/resolvers/queries/customers.js | 26 ----- src/data/resolvers/queries/index.js | 2 + src/data/resolvers/queries/segments.js | 29 ++++++ src/data/schema/customer.js | 16 --- src/data/schema/index.js | 9 ++ src/data/schema/segment.js | 43 ++++++++ src/db/factories.js | 25 ++++- src/db/models/Customers.js | 18 +--- src/db/models/Segments.js | 77 +++++++++++++++ src/db/models/index.js | 3 +- 13 files changed, 344 insertions(+), 60 deletions(-) create mode 100644 src/__tests__/segmentMutations.test.js create mode 100644 src/data/resolvers/mutations/segments.js create mode 100644 src/data/resolvers/queries/segments.js create mode 100644 src/data/schema/segment.js create mode 100644 src/db/models/Segments.js diff --git a/src/__tests__/segmentMutations.test.js b/src/__tests__/segmentMutations.test.js new file mode 100644 index 000000000..40d45dec1 --- /dev/null +++ b/src/__tests__/segmentMutations.test.js @@ -0,0 +1,121 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Segments, Users } from '../db/models'; +import { userFactory, segmentFactory } from '../db/factories'; +import segmentMutations from '../data/resolvers/mutations/segments'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +/* + * Generate test data + */ +const generateData = () => ({ + name: 'New users', + description: 'New users', + subOf: 'DFSAFDSAFDFFFD', + color: '#fdfdfd', + connector: 'any', + conditions: [ + { + field: 'messengerData.sessionCount', + operator: 'e', + value: '10', + dateUnit: 'days', + type: 'string', + }, + ], +}); + +/* + * Check values + */ +const checkValues = (segmentObj, doc) => { + expect(segmentObj.name).toBe(doc.name); + expect(segmentObj.description).toBe(doc.description); + expect(segmentObj.subOf).toBe(doc.subOf); + expect(segmentObj.color).toBe(doc.color); + expect(segmentObj.connector).toBe(doc.connector); + + expect(segmentObj.conditions.field).toEqual(doc.conditions.field); + expect(segmentObj.conditions.operator).toEqual(doc.conditions.operator); + expect(segmentObj.conditions.value).toEqual(doc.conditions.value); + expect(segmentObj.conditions.dateUnit).toEqual(doc.conditions.dateUnit); + expect(segmentObj.conditions.type).toEqual(doc.conditions.type); +}; + +describe('Segments mutations', () => { + let _user; + let _segment; + + beforeEach(async () => { + // Creating test data + _user = await userFactory(); + _segment = await segmentFactory(); + }); + + afterEach(async () => { + // Clearing test data + await Segments.remove({}); + await Users.remove({}); + }); + + test('Create segment', async () => { + // Login required + expect(() => segmentMutations.segmentsAdd({}, {}, {})).toThrowError('Login required'); + + // valid + const data = generateData(); + + const segmentObj = await segmentMutations.segmentsAdd({}, data, { user: _user }); + + checkValues(segmentObj, data); + }); + + test('Edit segment login required', async () => { + expect.assertions(1); + + try { + await segmentMutations.segmentsEdit({}, { _id: _segment.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Edit segment valid', async () => { + const data = generateData(); + + const segmentObj = await segmentMutations.segmentsEdit( + {}, + { _id: _segment._id, ...data }, + { user: _user }, + ); + + checkValues(segmentObj, data); + }); + + test('Remove segment login required', async () => { + expect.assertions(1); + + try { + await segmentMutations.segmentsRemove({}, { _id: _segment.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Remove segment valid', async () => { + const segmentDeletedObj = await segmentMutations.segmentsRemove( + {}, + { _id: _segment.id }, + { user: _user }, + ); + expect(segmentDeletedObj.id).toBe(_segment.id); + + const segmentObj = await Segments.findOne({ _id: _segment.id }); + expect(segmentObj).toBeNull(); + }); +}); diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index 121e8170b..6382a9e33 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -2,10 +2,12 @@ import conversation from './conversation'; import brands from './brands'; import emailTemplate from './emailTemplate'; import responseTemplate from './responseTemplate'; +import segments from './segments'; export default { ...conversation, ...brands, ...emailTemplate, ...responseTemplate, + ...segments, }; diff --git a/src/data/resolvers/mutations/segments.js b/src/data/resolvers/mutations/segments.js new file mode 100644 index 000000000..92f82cd37 --- /dev/null +++ b/src/data/resolvers/mutations/segments.js @@ -0,0 +1,33 @@ +import { Segments } from '../../../db/models'; + +export default { + /** + * Create new segment + * @return {Promise} segment object + */ + segmentsAdd(root, doc, { user }) { + if (!user) throw new Error('Login required'); + + return Segments.createSegment(doc); + }, + + /** + * Update segment + * @return {Promise} segment object + */ + async segmentsEdit(root, { _id, ...doc }, { user }) { + if (!user) throw new Error('Login required'); + + return Segments.updateSegment(_id, doc); + }, + + /** + * Delete segment + * @return {Promise} + */ + async segmentsRemove(root, { _id }, { user }) { + if (!user) throw new Error('Login required'); + + return Segments.removeSegment(_id); + }, +}; diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index 8933140f1..4caef345f 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -150,30 +150,4 @@ export default { customersTotalCount() { return Customers.find({}).count(); }, - - /** - * Segments list - * @return {Promise} segment objects - */ - segments() { - return Segments.find({}); - }, - - /** - * Only segment that has no sub segments - * @return {Promise} segment objects - */ - headSegments() { - return Segments.find({ subOf: { $exists: false } }); - }, - - /** - * Get one segment - * @param {Object} args - * @param {String} args._id - * @return {Promise} found segment - */ - segmentDetail(root, { _id }) { - return Segments.findOne({ _id }); - }, }; diff --git a/src/data/resolvers/queries/index.js b/src/data/resolvers/queries/index.js index acf540310..5c1c35bb2 100644 --- a/src/data/resolvers/queries/index.js +++ b/src/data/resolvers/queries/index.js @@ -8,6 +8,7 @@ import emailTemplates from './emailTemplates'; import engages from './engages'; import tags from './tags'; import customers from './customers'; +import segments from './segments'; import conversations from './conversations'; import insights from './insights'; import knowledgeBase from './knowledgeBase'; @@ -23,6 +24,7 @@ export default { ...engages, ...tags, ...customers, + ...segments, ...conversations, ...insights, ...knowledgeBase, diff --git a/src/data/resolvers/queries/segments.js b/src/data/resolvers/queries/segments.js new file mode 100644 index 000000000..21934382d --- /dev/null +++ b/src/data/resolvers/queries/segments.js @@ -0,0 +1,29 @@ +import { Segments } from '../../../db/models'; + +export default { + /** + * Segments list + * @return {Promise} segment objects + */ + segments() { + return Segments.find({}); + }, + + /** + * Only segment that has no sub segments + * @return {Promise} segment objects + */ + headSegments() { + return Segments.find({ subOf: { $exists: false } }); + }, + + /** + * Get one segment + * @param {Object} args + * @param {String} args._id + * @return {Promise} found segment + */ + segmentDetail(root, { _id }) { + return Segments.findOne({ _id }); + }, +}; diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index 9336e4829..f66fd9cec 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -28,19 +28,6 @@ export const types = ` getMessengerCustomData: JSON getTags: [Tag] } - - type Segment { - _id: String! - name: String - description: String - subOf: String - color: String - connector: String - conditions: JSON - - getParentSegment: Segment - getSubSegments: [Segment] - } `; export const queries = ` @@ -49,7 +36,4 @@ export const queries = ` customerDetail(_id: String!): Customer customerListForSegmentPreview(segment: JSON, limit: Int): [Customer] customersTotalCount: Int - segments: [Segment] - headSegments: [Segment] - segmentDetail(_id: String): Segment `; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index f14464736..19cf7b883 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -26,6 +26,12 @@ import { types as TagTypes, queries as TagQueries } from './tag'; import { types as CustomerTypes, queries as CustomerQueries } from './customer'; +import { + types as SegmentTypes, + queries as SegmentQueries, + mutations as SegmentMutations, +} from './segment'; + import { types as InsightTypes, queries as InsightQueries } from './insight'; import { types as KnowledgeBaseTypes, queries as KnowledgeBaseQueries } from './knowledgeBase'; @@ -50,6 +56,7 @@ export const types = ` ${TagTypes} ${FormTypes} ${CustomerTypes} + ${SegmentTypes} ${ConversationTypes} ${InsightTypes} ${KnowledgeBaseTypes} @@ -67,6 +74,7 @@ export const queries = ` ${EngageQueries} ${TagQueries} ${CustomerQueries} + ${SegmentQueries} ${ConversationQueries} ${InsightQueries} ${KnowledgeBaseQueries} @@ -79,6 +87,7 @@ export const mutations = ` ${BrandMutations} ${ResponseTemplateMutations} ${EmailTemplateMutations} + ${SegmentMutations} } `; diff --git a/src/data/schema/segment.js b/src/data/schema/segment.js new file mode 100644 index 000000000..18da7eea9 --- /dev/null +++ b/src/data/schema/segment.js @@ -0,0 +1,43 @@ +export const types = ` + input SegmentCondition { + field: String, + operator: String, + value: String, + dateUnit: String, + type: String, + } + + type Segment { + _id: String! + name: String + description: String + subOf: String + color: String + connector: String + conditions: JSON + + getParentSegment: Segment + getSubSegments: [Segment] + } +`; + +export const queries = ` + segments: [Segment] + headSegments: [Segment] + segmentDetail(_id: String): Segment +`; + +const commonFields = ` + name: String!, + description: String, + subOf: String, + color: String, + connector: String, + conditions: SegmentCondition +`; + +export const mutations = ` + segmentsAdd(${commonFields}): Segment + segmentsEdit(_id: String!, ${commonFields}): Segment + segmentsRemove(_id: String!): Segment +`; diff --git a/src/db/factories.js b/src/db/factories.js index 585f1ebd6..9c8f6264e 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -1,7 +1,7 @@ import faker from 'faker'; import Random from 'meteor-random'; -import { Users, Brands, EmailTemplates, ResponseTemplates } from './models'; +import { Users, Brands, EmailTemplates, ResponseTemplates, Segments } from './models'; export const userFactory = (params = {}) => { const user = new Users({ @@ -48,3 +48,26 @@ export const responseTemplateFactory = (params = {}) => { return responseTemplate.save(); }; + +export const segmentFactory = (params = {}) => { + const defaultConditions = [ + { + field: 'messengerData.sessionCount', + operator: 'e', + value: '10', + dateUnit: 'days', + type: 'string', + }, + ]; + + const segment = new Segments({ + name: faker.random.word(), + description: params.description || faker.random.word(), + subOf: params.subOf || 'DFSAFDFDSFDSF', + color: params.color || '#ffff', + connector: params.connector || 'any', + conditions: params.conditions || defaultConditions, + }); + + return segment.save(); +}; diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index 7969bf49b..a29f2d3a1 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -42,20 +42,6 @@ class Customer { CustomerSchema.loadClass(Customer); -export const Customers = mongoose.model('customers', CustomerSchema); +const Customers = mongoose.model('customers', CustomerSchema); -const SegmentSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - name: String, - description: String, - subOf: String, - color: String, - connector: String, - conditions: Object, -}); - -export const Segments = mongoose.model('segments', SegmentSchema); +export default Customers; diff --git a/src/db/models/Segments.js b/src/db/models/Segments.js new file mode 100644 index 000000000..dc9d5807e --- /dev/null +++ b/src/db/models/Segments.js @@ -0,0 +1,77 @@ +import mongoose from 'mongoose'; +import Random from 'meteor-random'; + +const ConditionSchema = mongoose.Schema( + { + field: String, + operator: String, + type: String, + + value: { + type: String, + optional: true, + }, + + dateUnit: { + type: String, + optional: true, + }, + }, + { _id: false }, +); + +const SegmentSchema = mongoose.Schema({ + _id: { + type: String, + unique: true, + default: () => Random.id(), + }, + name: String, + description: String, + subOf: String, + color: String, + connector: String, + conditions: [ConditionSchema], +}); + +class Segment { + /** + * Create a segment + * @param {Object} segmentObj object + * @return {Promise} Newly created segment object + */ + static createSegment(doc) { + return this.create(doc); + } + + /* + * Update segment + * @param {String} _id segment id to update + * @param {Object} doc field values to update + * @return {Promise} updated segment object + */ + static async updateSegment(_id, doc) { + await this.update({ _id }, { $set: doc }); + + return this.findOne({ _id }); + } + + /* + * Remove segment + * @param {String} _id segment id to remove + * @return {Promise} + */ + static async removeSegment(_id) { + const segmentObj = await this.findOne({ _id }); + + if (!segmentObj) throw new Error(`Segment not found with id ${_id}`); + + return segmentObj.remove(); + } +} + +SegmentSchema.loadClass(Segment); + +const Segments = mongoose.model('segments', SegmentSchema); + +export default Segments; diff --git a/src/db/models/index.js b/src/db/models/index.js index a0b07d4ed..523723994 100644 --- a/src/db/models/index.js +++ b/src/db/models/index.js @@ -7,7 +7,8 @@ import Integrations from './Integrations'; import EngageMessages from './Engages'; import Tags from './Tags'; import { Forms, FormFields } from './Forms'; -import { Customers, Segments } from './Customers'; +import Customers from './Customers'; +import Segments from './Segments'; import { Conversations, Messages as ConversationMessages } from './Conversations'; import { KnowledgeBaseArticles, From 132b4a23b9cc28374b064d18e7007cd821cdc9f3 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 7 Oct 2017 16:02:18 +0800 Subject: [PATCH 012/318] Add company mutations & tests --- src/__tests__/companyMutations.test.js | 105 ++++++++++++++++++++++ src/data/resolvers/mutations/companies.js | 33 +++++++ src/data/resolvers/mutations/index.js | 2 + src/db/factories.js | 13 ++- src/db/models/Companies.js | 85 ++++++++++++++++++ src/db/models/Customers.js | 6 +- src/db/models/index.js | 2 + 7 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/companyMutations.test.js create mode 100644 src/data/resolvers/mutations/companies.js create mode 100644 src/db/models/Companies.js diff --git a/src/__tests__/companyMutations.test.js b/src/__tests__/companyMutations.test.js new file mode 100644 index 000000000..9b3a04806 --- /dev/null +++ b/src/__tests__/companyMutations.test.js @@ -0,0 +1,105 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Companies, Users } from '../db/models'; +import { userFactory, companyFactory } from '../db/factories'; +import companyMutations from '../data/resolvers/mutations/companies'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +/* + * Generate test data + */ +const generateData = () => ({ + name: 'New company', + size: 10, + industry: 'Mining', + website: 'https://www.mining.com', +}); + +/* + * Check values + */ +const checkValues = (companyObj, doc) => { + expect(companyObj.name).toBe(doc.name); + expect(companyObj.size).toBe(doc.size); + expect(companyObj.industry).toBe(doc.industry); + expect(companyObj.website).toBe(doc.website); +}; + +describe('Companies mutations', () => { + let _user; + let _company; + + beforeEach(async () => { + // Creating test data + _user = await userFactory(); + _company = await companyFactory(); + }); + + afterEach(async () => { + // Clearing test data + await Companies.remove({}); + await Users.remove({}); + }); + + test('Create company', async () => { + // Login required + expect(() => companyMutations.companiesAdd({}, {}, {})).toThrowError('Login required'); + + // valid + const data = generateData(); + + const companyObj = await companyMutations.companiesAdd({}, data, { user: _user }); + + checkValues(companyObj, data); + }); + + test('Edit company login required', async () => { + expect.assertions(1); + + try { + await companyMutations.companiesEdit({}, { _id: _company.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Edit company valid', async () => { + const data = generateData(); + + const companyObj = await companyMutations.companiesEdit( + {}, + { _id: _company._id, ...data }, + { user: _user }, + ); + + checkValues(companyObj, data); + }); + + test('Remove company login required', async () => { + expect.assertions(1); + + try { + await companyMutations.companiesRemove({}, { _id: _company.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Remove company valid', async () => { + const companyDeletedObj = await companyMutations.companiesRemove( + {}, + { _id: _company.id }, + { user: _user }, + ); + + expect(companyDeletedObj.id).toBe(_company.id); + + const companyObj = await Companies.findOne({ _id: _company.id }); + expect(companyObj).toBeNull(); + }); +}); diff --git a/src/data/resolvers/mutations/companies.js b/src/data/resolvers/mutations/companies.js new file mode 100644 index 000000000..97149b3ea --- /dev/null +++ b/src/data/resolvers/mutations/companies.js @@ -0,0 +1,33 @@ +import { Companies } from '../../../db/models'; + +export default { + /** + * Create new company + * @return {Promise} company object + */ + companiesAdd(root, doc, { user }) { + if (!user) throw new Error('Login required'); + + return Companies.createCompany(doc); + }, + + /** + * Update company + * @return {Promise} company object + */ + async companiesEdit(root, { _id, ...doc }, { user }) { + if (!user) throw new Error('Login required'); + + return Companies.updateCompany(_id, doc); + }, + + /** + * Delete company + * @return {Promise} + */ + async companiesRemove(root, { _id }, { user }) { + if (!user) throw new Error('Login required'); + + return Companies.removeCompany(_id); + }, +}; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index 6382a9e33..7cd6cb7d0 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -3,6 +3,7 @@ import brands from './brands'; import emailTemplate from './emailTemplate'; import responseTemplate from './responseTemplate'; import segments from './segments'; +import companies from './companies'; export default { ...conversation, @@ -10,4 +11,5 @@ export default { ...emailTemplate, ...responseTemplate, ...segments, + ...companies, }; diff --git a/src/db/factories.js b/src/db/factories.js index 9c8f6264e..a752ee514 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -1,7 +1,7 @@ import faker from 'faker'; import Random from 'meteor-random'; -import { Users, Brands, EmailTemplates, ResponseTemplates, Segments } from './models'; +import { Users, Brands, EmailTemplates, ResponseTemplates, Segments, Companies } from './models'; export const userFactory = (params = {}) => { const user = new Users({ @@ -71,3 +71,14 @@ export const segmentFactory = (params = {}) => { return segment.save(); }; + +export const companyFactory = (params = {}) => { + const company = new Companies({ + name: faker.random.word(), + size: params.size || faker.random.number(), + industry: params.industry || Random.id(), + website: params.website || Random.id(), + }); + + return company.save(); +}; diff --git a/src/db/models/Companies.js b/src/db/models/Companies.js new file mode 100644 index 000000000..3d4913073 --- /dev/null +++ b/src/db/models/Companies.js @@ -0,0 +1,85 @@ +import mongoose from 'mongoose'; +import Random from 'meteor-random'; + +const CompanySchema = mongoose.Schema({ + _id: { + type: String, + unique: true, + default: () => Random.id(), + }, + + name: { + type: String, + optional: true, + }, + + size: { + type: Number, + optional: true, + }, + + industry: { + type: String, + optional: true, + }, + + website: { + type: String, + optional: true, + }, + + plan: { + type: String, + optional: true, + }, + + lastSeenAt: Date, + sessionCount: Number, + + tagIds: { + type: [String], + optional: true, + }, +}); + +class Company { + /** + * Create a company + * @param {Object} companyObj object + * @return {Promise} Newly created company object + */ + static createCompany(doc) { + return this.create(doc); + } + + /* + * Update company + * @param {String} _id company id to update + * @param {Object} doc field values to update + * @return {Promise} updated company object + */ + static async updateCompany(_id, doc) { + await this.update({ _id }, { $set: doc }); + + return this.findOne({ _id }); + } + + /* + * Remove company + * @param {String} _id company id to remove + * @return {Promise} + */ + static async removeCompany(_id) { + const companyObj = await this.findOne({ _id }); + + if (!companyObj) throw new Error(`Company not found with id ${_id}`); + + return companyObj.remove(); + } +} + +CompanySchema.loadClass(Company); + +const Companies = mongoose.model('companies', CompanySchema); + +export default Companies; diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index a29f2d3a1..867b15e4e 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -7,14 +7,18 @@ const CustomerSchema = mongoose.Schema({ unique: true, default: () => Random.id(), }, - integrationId: String, + name: String, email: String, phone: String, isUser: Boolean, + + integrationId: String, createdAt: Date, + internalNotes: Object, tagIds: [String], + messengerData: Object, twitterData: Object, facebookData: Object, diff --git a/src/db/models/index.js b/src/db/models/index.js index 523723994..253d11beb 100644 --- a/src/db/models/index.js +++ b/src/db/models/index.js @@ -8,6 +8,7 @@ import EngageMessages from './Engages'; import Tags from './Tags'; import { Forms, FormFields } from './Forms'; import Customers from './Customers'; +import Companies from './Companies'; import Segments from './Segments'; import { Conversations, Messages as ConversationMessages } from './Conversations'; import { @@ -29,6 +30,7 @@ export { Tags, Segments, Customers, + Companies, Conversations, ConversationMessages, KnowledgeBaseArticles, From 2a4d997d04db02af3a4e5e07001f48fd8cc477c8 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 7 Oct 2017 16:15:27 +0800 Subject: [PATCH 013/318] Add company schema --- src/data/schema/company.js | 30 ++++++++++++++++++++++++++++++ src/data/schema/index.js | 4 ++++ 2 files changed, 34 insertions(+) create mode 100644 src/data/schema/company.js diff --git a/src/data/schema/company.js b/src/data/schema/company.js new file mode 100644 index 000000000..905c502f8 --- /dev/null +++ b/src/data/schema/company.js @@ -0,0 +1,30 @@ +export const types = ` + type Company { + _id: String! + name: String + size: Int + website: String + industry: String + plan: String + lastSeenAt: Date + sessionCount: Int + tagIds: [String], + } +`; + +const commonFields = ` + name: String!, + size: Int, + website: String, + industry: String, + plan: String, + lastSeenAt: Date, + sessionCount: Int, + tagIds: [String] +`; + +export const mutations = ` + companiesAdd(${commonFields}): Company + companiesEdit(_id: String!, ${commonFields}): Company + companiesRemove(_id: String!): Company +`; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index 19cf7b883..a99841c06 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -1,5 +1,7 @@ import { types as UserTypes, queries as UserQueries } from './user'; +import { types as CompanyTypes, mutations as CompanyMutations } from './company'; + import { types as ChannelTypes, queries as ChannelQueries } from './channel'; import { types as BrandTypes, queries as BrandQueries, mutations as BrandMutations } from './brand'; @@ -47,6 +49,7 @@ export const types = ` scalar Date ${UserTypes} + ${CompanyTypes} ${ChannelTypes} ${BrandTypes} ${IntegrationTypes} @@ -83,6 +86,7 @@ export const queries = ` export const mutations = ` type Mutation { + ${CompanyMutations} ${ConversationMutations} ${BrandMutations} ${ResponseTemplateMutations} From c9ee5de971bbcedae4bbd8d63f55d0e58eb3eb2b Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 8 Oct 2017 03:42:04 +0800 Subject: [PATCH 014/318] #13 fixes related to review comments --- package.json | 5 +- src/__tests__/formMutations.test.js | 61 ++++++++++++++++++++++++- src/data/resolvers/mutations/channel.js | 2 +- src/data/schema/channel.js | 2 +- src/db/factories.js | 21 +++++---- src/db/models/Channels.js | 4 +- src/db/models/Forms.js | 16 ++++--- src/db/models/Integrations.js | 6 +-- 8 files changed, 89 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 2a81def97..85aaaefa9 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "cors": "^2.8.1", "dotenv": "^4.0.0", "express": "^4.15.2", - "faker": "^4.1.0", "graphql": "^0.10.1", "graphql-server-core": "^0.8.2", "graphql-server-express": "^0.8.2", @@ -58,8 +57,6 @@ "husky": "^0.13.4", "jest": "^21.2.1", "lint-staged": "^3.6.0", - "nodemon": "^1.11.0", - "prettier": "^1.4.4", - "shortid": "^2.2.8" + "nodemon": "^1.11.0" } } diff --git a/src/__tests__/formMutations.test.js b/src/__tests__/formMutations.test.js index d88c5d3dc..3f8ecc79e 100644 --- a/src/__tests__/formMutations.test.js +++ b/src/__tests__/formMutations.test.js @@ -2,7 +2,7 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { userFactory, formFactory, formFieldFactory } from '../db/factories'; +import { userFactory, formFactory, formFieldFactory, integrationFactory } from '../db/factories'; import { Forms, Users, FormFields } from '../db/models'; beforeAll(() => connect()); @@ -95,6 +95,7 @@ describe('form update tests', () => { describe('form remove tests', async () => { let _user; + /** * Testing with an _user object */ @@ -123,6 +124,64 @@ describe('form remove tests', async () => { }); }); +describe('test exception in form remove', async () => { + let _user; + + /** + * Testing with an _user object + */ + beforeEach(async () => { + _user = await userFactory({}); + }); + + /** + * Deleting the data that was used in test + */ + afterEach(async () => { + await Users.remove({}); + await Forms.remove({}); + await FormFields.remove({}); + }); + + test('try to remove form with fields in it', async () => { + expect.assertions(2); + const form = await Forms.createForm({ + title: 'Test form', + description: 'Test form description', + createdUserId: _user._id, + }); + + await FormFields.createFormField(form._id, { + type: 'shoutbox', + validation: 'number', + text: 'form field text', + description: 'form field description', + }); + + try { + await Forms.removeForm(form._id); + } catch (e) { + expect(e.message).toEqual('You cannot delete this form. This form has some fields.'); + } + + await FormFields.remove({}); + + await integrationFactory({ + formId: form._id, + formData: { + loadType: 'shoutbox', + fromEmail: 'test@erxes.io', + }, + }); + + try { + await Forms.removeForm(form._id); + } catch (e) { + expect(e.message).toEqual('You cannot delete this form. This form used in integration.'); + } + }); +}); + describe('add form field test', async () => { let _user; let _form; diff --git a/src/data/resolvers/mutations/channel.js b/src/data/resolvers/mutations/channel.js index 129cccedb..a18fb703f 100644 --- a/src/data/resolvers/mutations/channel.js +++ b/src/data/resolvers/mutations/channel.js @@ -29,7 +29,7 @@ export default { * @return {Promise} returns null * @throws {Error} throws apollo level validation errors */ - channelsUpdate(root, { _id, ...doc }) { + channelsEdit(root, { _id, ...doc }) { Channels.updateChannel(_id, doc); // TODO: sendNotifications method shoul be here return; diff --git a/src/data/schema/channel.js b/src/data/schema/channel.js index fcb28c58b..3f6810309 100644 --- a/src/data/schema/channel.js +++ b/src/data/schema/channel.js @@ -25,7 +25,7 @@ export const mutations = ` integrationIds: [String], userId: String!): Channel - channelsUpdate( + channelsEdit( _id: String!, name: String!, description: String, diff --git a/src/db/factories.js b/src/db/factories.js index dc4897d6e..220e163c0 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -1,5 +1,6 @@ import shortid from 'shortid'; import faker from 'faker'; +import Random from 'meteor-random'; import { Users, @@ -28,9 +29,13 @@ export const integrationFactory = params => { return Integrations.create({ name: faker.random.word(), kind: kind, - brandId: params.brandId || shortid.generate(), - formId: params.formId || shortid.generate(), - messengerData: { welcomeMessage: 'welcome' } || {}, + brandId: params.brandId || Random.id(), + formId: params.formId || Random.id(), + messengerData: params.messengerData || { welcomeMessage: 'welcome' }, + formData: + params.formData === 'form' + ? params.formData + : kind === 'form' ? { thankContent: 'thankContent' } : null, }); }; @@ -38,7 +43,7 @@ export const brandFactory = (params = {}) => { const brand = new Brands({ name: faker.random.word(), code: params.code || faker.random.word(), - userId: shortid.generate(), + userId: Random.id(), description: params.description || faker.random.word(), emailConfig: { type: 'simple', @@ -62,7 +67,7 @@ export const responseTemplateFactory = (params = {}) => { const responseTemplate = new ResponseTemplates({ name: faker.random.word(), content: params.content || faker.random.word(), - brandId: params.brandId || shortid.generate(), + brandId: params.brandId || Random.id(), files: [faker.random.image()], }); @@ -73,8 +78,8 @@ export const tagsFactory = (params = {}) => { const tag = new Tags({ name: faker.random.word(), type: params.type || faker.random.word(), - colorCode: params.colorCode || shortid.generate(), - userId: shortid.generate(), + colorCode: params.colorCode || Random.id(), + userId: Random.id(), }); return tag.save(); @@ -84,7 +89,7 @@ export const formFactory = ({ title, code, createdUserId }) => { return Forms.createForm({ title: title || faker.random.word(), description: faker.random.word(), - code: code || shortid.generate(), + code: code || Random.id(), createdUserId, }); }; diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index 34279ef7b..2c78e2d9e 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -1,5 +1,5 @@ import mongoose from 'mongoose'; -import shortid from 'shortid'; +import Random from 'meteor-random'; import { createdAtModifier } from '../plugins'; function ChannelCreationException(message) { @@ -11,7 +11,7 @@ function ChannelCreationException(message) { const ChannelSchema = mongoose.Schema({ _id: { type: String, - default: shortid.generate, + default: () => Random.id(), }, name: { type: String, diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index 36b9f0b2e..25cd8be49 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -1,11 +1,11 @@ import mongoose from 'mongoose'; -import shortid from 'shortid'; +import Random from 'meteor-random'; import Integrations from './Integrations'; const FormSchema = mongoose.Schema({ _id: { type: String, - default: shortid.generate, + default: () => Random.id(), }, title: String, description: String, @@ -17,11 +17,11 @@ const FormSchema = mongoose.Schema({ class Form { static async generateCode() { // generate code automatically - let code = shortid.generate().substr(0, 6); + let code = Random.id().substr(0, 6); let foundForm = await Forms.findOne({ code }); while (foundForm) { - code = shortid.generate().substr(0, 6); + code = Random.id().substr(0, 6); foundForm = await Forms.findOne({ code }); } @@ -49,13 +49,13 @@ class Form { const fieldCount = await FormFields.find({ formId: _id }).count(); if (fieldCount > 0) { - throw 'You cannot delete this form. This form has some fields.'; + throw new Error('You cannot delete this form. This form has some fields.'); } const integrationCount = await Integrations.find({ formId: _id }).count(); if (integrationCount > 0) { - throw 'You cannot delete this form. This form used in integration.'; + throw new Error('You cannot delete this form. This form used in integration.'); } return this.remove({ _id }); @@ -103,7 +103,7 @@ export const Forms = mongoose.model('forms', FormSchema); const FormFieldSchema = mongoose.Schema({ _id: { type: String, - default: shortid.generate, + default: () => Random.id(), }, type: String, validation: String, @@ -125,9 +125,11 @@ class FormField { // if there is no field then start with 0 let order = 0; + if (lastField) { order = lastField.order + 1; } + doc.order = order; return this.create(doc); diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index 18c7244cc..8714c2c46 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -1,5 +1,5 @@ import mongoose from 'mongoose'; -import shortid from 'shortid'; +import Random from 'meteor-random'; import { Messages, Conversations } from './Conversations'; import { Customers } from './Customers'; import { KIND_CHOICES, FORM_SUCCESS_ACTIONS, FORM_LOAD_TYPES } from '../constants'; @@ -91,7 +91,7 @@ const UiOptionsSchema = mongoose.Schema({ const IntegrationSchema = mongoose.Schema({ _id: { type: String, - default: shortid.generate, + default: () => Random.id(), }, kind: String, name: String, @@ -157,8 +157,6 @@ class Integration { } static async removeIntegration(id) { - // remove messages - // conversations const conversations = await Conversations.find({ integrationId: id }, { _id: true }); const conversationIds = []; From e107e6486ea2066e42e86a0b77bfada7fe7f13f9 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 8 Oct 2017 10:44:12 +0800 Subject: [PATCH 015/318] #13 added mutation calls on form mutations --- src/__tests__/formMutations.test.js | 189 ++++++++++++++++++++- src/__tests__/integrationMutations.test.js | 9 +- src/data/resolvers/mutations/form.js | 2 +- src/db/models/Forms.js | 10 +- src/db/models/Integrations.js | 5 +- 5 files changed, 201 insertions(+), 14 deletions(-) diff --git a/src/__tests__/formMutations.test.js b/src/__tests__/formMutations.test.js index 3f8ecc79e..6a93d3f57 100644 --- a/src/__tests__/formMutations.test.js +++ b/src/__tests__/formMutations.test.js @@ -4,6 +4,7 @@ import { connect, disconnect } from '../db/connection'; import { userFactory, formFactory, formFieldFactory, integrationFactory } from '../db/factories'; import { Forms, Users, FormFields } from '../db/models'; +import mutations from '../data/resolvers/mutations'; beforeAll(() => connect()); afterAll(() => disconnect()); @@ -348,7 +349,7 @@ describe('test of update order of form fields', async () => { }); }); -describe('test of form duplication', async () => { +describe('test of form duplication', () => { let _user; let _form; /** @@ -387,3 +388,189 @@ describe('test of form duplication', async () => { expect(duplicateFormFieldsCount).toEqual(3); }); }); + +describe('mutations', () => { + afterEach(async () => { + await Users.remove({}); + await Forms.remove({}); + await FormFields.remove({}); + }); + + test('mutation tests ', async () => { + const _user = await userFactory({}); + + // mutations.formsCreate + let form = await mutations.formsCreate(null, { + title: 'Test form', + description: 'Test form description', + createdUserId: _user._id, + }); + + form = await Forms.findOne({ _id: form._id }); + + expect(form.title).toBe('Test form'); + expect(form.description).toBe('Test form description'); + expect(typeof form.code).toBe('string'); + expect(form.code.length).toEqual(6); + expect(typeof form.createdDate).toBe('object'); + expect(form.createdUserId).toBe(_user._id); + + // mutations.formsUpdate + await mutations.formsEdit(null, { + _id: form._id, + title: 'Test form 2', + description: 'Test form description 2', + }); + + const formAfterUpdate = await Forms.findOne({ _id: form._id }); + expect(formAfterUpdate.title).toBe('Test form 2'); + expect(formAfterUpdate.description).toBe('Test form description 2'); + expect(form.createdUserId).toBe(formAfterUpdate.createdUserId); + expect(form.code).toBe(formAfterUpdate.code); + expect(typeof form.createdDate).toBe('object'); + + // mutations.formsAddFormField + const newFormField = await mutations.formsAddFormField(null, { + formId: form._id, + type: 'input', + validation: 'number', + text: 'How old are you?', + description: 'Form field description', + options: ['This', 'should', 'not', 'be', 'here', 'tho'], + isRequired: false, + }); + + expect(newFormField.formId).toEqual(form._id); + expect(newFormField.order).toEqual(0); + expect(newFormField.type).toEqual('input'); + expect(newFormField.validation).toEqual('number'); + expect(newFormField.text).toEqual('How old are you?'); + expect(newFormField.description).toEqual('Form field description'); + expect.arrayContaining(newFormField.options); + expect(newFormField.isRequired).toEqual(false); + + // mutations.formsAddFormField + await mutations.formsEditFormField(null, { + _id: newFormField._id, + type: 'mutation input 1', + validation: 'mutation number 1', + text: 'mutation - How old are you? 1', + description: 'mutation - Form field description 1', + options: ['This', 'should', 'not', 'be', 'here', 'tho', '1'], + isRequired: true, + }); + + const updatedFormField = await FormFields.findOne({ _id: newFormField._id }); + expect(updatedFormField.formId).toEqual(form._id); + expect(updatedFormField.type).toEqual('mutation input 1'); + expect(updatedFormField.validation).toEqual('mutation number 1'); + expect(updatedFormField.text).toEqual('mutation - How old are you? 1'); + expect(updatedFormField.description).toEqual('mutation - Form field description 1'); + expect.arrayContaining(updatedFormField.options); + expect(updatedFormField.options.length).toEqual(7); + expect(updatedFormField.isRequired).toEqual(true); + + // formsRemoveFormField + await mutations.formsRemoveFormField(null, { _id: newFormField._id }); + + expect(await FormFields.find({}).count()).toEqual(0); + + // mutations.formsRemove + await mutations.formsRemove(null, { _id: form._id }); + + expect(await Forms.find({}).count()).toEqual(0); + }); +}); + +describe('mutations 2', async () => { + let _user; + let _form; + let _form_field; + let _form_field2; + let _form_field3; + /** + * Testing with an _user object and a _form object with 3 fields in it + * to test the setting the new order + */ + beforeEach(async () => { + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + _form_field = await formFieldFactory(_form._id, {}); + _form_field2 = await formFieldFactory(_form._id, {}); + _form_field3 = await formFieldFactory(_form._id, {}); + }); + + /** + * Deleting the data that was used in test + */ + afterEach(async () => { + await Users.remove({}); + await FormFields.remove({}); + await Forms.remove({}); + }); + + test('test of update order of form fields', async () => { + expect(_form_field.order).toBe(0); + expect(_form_field2.order).toBe(1); + expect(_form_field3.order).toBe(2); + + const orderDictArray = [ + { id: _form_field3._id, order: 10 }, + { id: _form_field2._id, order: 9 }, + { id: _form_field._id, order: 8 }, + ]; + + await Forms.updateFormFieldsOrder(orderDictArray); + const ff1 = await FormFields.findOne({ _id: _form_field3._id }); + expect(ff1.order).toBe(10); + expect(ff1.text).toBe(_form_field3.text); + + const ff2 = await FormFields.findOne({ _id: _form_field2._id }); + expect(ff2.order).toBe(9); + expect(ff2.text).toBe(_form_field2.text); + + const ff3 = await FormFields.findOne({ _id: _form_field._id }); + expect(ff3.order).toBe(8); + expect(ff3.text).toBe(_form_field.text); + }); +}); + +describe('mutations 3', () => { + let _user; + let _form; + /** + * Testing with an _user object and a _form object with 3 fields in it + */ + beforeEach(async () => { + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + await formFieldFactory(_form._id, {}); + await formFieldFactory(_form._id, {}); + await formFieldFactory(_form._id, {}); + }); + + /** + * Deleting the data that was used in test + */ + afterEach(async () => { + await Users.remove({}); + await FormFields.remove({}); + await Forms.remove({}); + }); + + test('test of form duplication', async () => { + const duplicatedForm = await mutations.formsDuplicate(null, { _id: _form._id }); + + expect(duplicatedForm.title).toBe(`${_form.title} duplicated`); + expect(duplicatedForm.description).toBe(_form.description); + expect(typeof duplicatedForm.code).toBe('string'); + expect(duplicatedForm.code.length).toEqual(6); + expect(duplicatedForm.createdUserId).toBe(_form.createdUserId); + + const formFieldsCount = await FormFields.find({}).count(); + const duplicateFormFieldsCount = await FormFields.find({ formId: duplicatedForm._id }).count(); + + expect(formFieldsCount).toEqual(6); + expect(duplicateFormFieldsCount).toEqual(3); + }); +}); diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js index 146ab3fe5..96df93cad 100644 --- a/src/__tests__/integrationMutations.test.js +++ b/src/__tests__/integrationMutations.test.js @@ -128,7 +128,7 @@ describe('create form integration test', () => { await Forms.remove({}); }); - test('create form integration test wihtout formData', async () => { + test('create form integration test without formData', async () => { const mainDoc = { name: 'form integration test', brandId: _brand._id, @@ -522,10 +522,9 @@ describe('mutation test', () => { expect(integration.messengerData.thankYouMessage).toEqual('Thank you'); const integrations = await Integrations.find({}, { _id: 1 }); - const promiseArray = integrations.map(i => { - return mutations.integrationsRemove(null, { id: i._id }); - }); - await Promise.all(promiseArray); + for (let i of integrations) { + await mutations.integrationsRemove(null, { id: i._id }); + } const integrationCount = await Integrations.find({}).count(); expect(integrationCount).toEqual(0); diff --git a/src/data/resolvers/mutations/form.js b/src/data/resolvers/mutations/form.js index ada519467..80645faf2 100644 --- a/src/data/resolvers/mutations/form.js +++ b/src/data/resolvers/mutations/form.js @@ -5,7 +5,7 @@ export default { * @param {Object} * @param {String} doc.title * @param {String} doc.description - * @param {String} doc.userId + * @param {String} doc.createdUserId * @return {Promise} returns the form * @throws {Error} apollo level error based on validation */ diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index 25cd8be49..fd12598a3 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -42,7 +42,7 @@ class Form { } static updateForm(_id, doc) { - return this.update({ _id }, doc); + return this.update({ _id }, doc, { runValidators: true }); } static async removeForm(_id) { @@ -135,12 +135,12 @@ class FormField { return this.create(doc); } - static updateFormField(id, doc) { - return this.update({ _id: id }, doc); + static updateFormField(_id, doc) { + return this.update({ _id }, doc, { runValidators: true }); } - static removeFormField(id) { - return this.remove({ _id: id }); + static removeFormField(_id) { + return this.remove({ _id }); } } diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index 8714c2c46..984679b43 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -164,12 +164,13 @@ class Integration { conversationIds.push(c._id); }); + // Remove messages await Messages.remove({ conversationId: { $in: conversationIds } }); - // remove conversations + // Remove conversations await Conversations.remove({ integrationId: id }); - // remove customers + // Remove customers await Customers.remove({ integrationId: id }); return this.remove(id); From d3ef2389901ab88bc550e285ecc71ce8c87a5738 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 8 Oct 2017 11:06:14 +0800 Subject: [PATCH 016/318] #13 channel mutations --- src/__tests__/channelMutations.test.js | 110 ++++++++++++++++++++++++ src/data/resolvers/mutations/channel.js | 5 +- src/db/models/Channels.js | 6 +- test.config.json | 2 +- 4 files changed, 114 insertions(+), 9 deletions(-) diff --git a/src/__tests__/channelMutations.test.js b/src/__tests__/channelMutations.test.js index 94a7716bc..729349a79 100644 --- a/src/__tests__/channelMutations.test.js +++ b/src/__tests__/channelMutations.test.js @@ -4,6 +4,7 @@ import { connect, disconnect } from '../db/connection'; import { userFactory, integrationFactory } from '../db/factories'; import { Channels, Users, Integrations } from '../db/models'; +import mutations from '../data/resolvers/mutations'; beforeAll(() => connect()); afterAll(() => disconnect()); @@ -148,3 +149,112 @@ describe('channel remove test', () => { expect(channelCount).toBe(0); }); }); + +describe('mutations', () => { + let _user; + let _user2; + let _integration; + + /** + * Before each test create test data + * containing 2 users and an integration + */ + beforeEach(async () => { + _user = await userFactory({}); + _integration = await integrationFactory({}); + _user2 = await userFactory({}); + }); + + /** + * After each test remove the test data + */ + afterEach(async () => { + await Channels.remove({}); + await Users.remove({}); + await Integrations.remove({}); + }); + + test('mutations', async () => { + // mutations.chanelsCreate + let doc = { + name: 'Channel test', + description: 'test channel descripion', + userId: _user._id, + memberIds: [_user2._id], + integrationIds: [_integration._id], + }; + + let channel = await Channels.createChannel(doc); + + expect(channel.name).toEqual(doc.name); + expect(channel.description).toEqual(doc.description); + expect(channel.memberIds.length).toBe(2); + expect(channel.integrationIds.length).toEqual(1); + expect(channel.integrationIds[0]).toEqual(_integration._id); + expect(channel.userId).toEqual(doc.userId); + expect(channel.conversationCount).toEqual(0); + expect(channel.openConversationCount).toEqual(0); + + // mutations.channelsUpdate + doc = { + name: 'Channel test 1', + description: 'Channel test description 1', + userId: _user._id, + memberIds: [_user2._id], + integrationIds: [_integration._id], + }; + + doc.memberIds = [_user2._id]; + await mutations.channelsEdit(null, { ...doc, _id: channel._id }); + channel = await Channels.findOne({ _id: channel._id }); + expect(channel.name).toEqual(doc.name); + expect(channel.description).toEqual(doc.description); + expect(channel.memberIds.length).toBe(2); + expect(channel.memberIds[0]).toBe(_user2._id); + expect(channel.memberIds[1]).toBe(_user._id); + expect(channel.integrationIds.length).toEqual(1); + expect(channel.integrationIds[0]).toEqual(_integration._id); + expect(channel.userId).toEqual(doc.userId); + expect(channel.conversationCount).toEqual(0); + expect(channel.openConversationCount).toEqual(0); + + doc.memberIds = [_user._id]; + await mutations.channelsEdit(null, { ...doc, _id: channel._id }); + channel = await Channels.findOne({ _id: channel._id }); + expect(channel.memberIds.length).toBe(1); + expect(channel.memberIds[0]).toBe(_user._id); + + await mutations.channelsRemove(null, { _id: channel._id }); + const channelCount = await Channels.find({}).count(); + expect(channelCount).toBe(0); + }); +}); + +// describe('channel remove test', () => { +// let _channel; +// +// /** +// * Before each test create test data +// * containing 2 users and an integration +// */ +// beforeEach(async () => { +// const user = await userFactory({}); +// _channel = await Channels.createChannel({ +// name: 'Channel test', +// userId: user._id, +// }); +// }); +// +// /** +// * Remove test data +// */ +// afterEach(async () => { +// await Channels.remove({}); +// }); +// +// test('channel remove test', async () => { +// await Channels.removeChannel(_channel._id); +// const channelCount = await Channels.find({}).count(); +// expect(channelCount).toBe(0); +// }); +// }); diff --git a/src/data/resolvers/mutations/channel.js b/src/data/resolvers/mutations/channel.js index a18fb703f..de98af0e1 100644 --- a/src/data/resolvers/mutations/channel.js +++ b/src/data/resolvers/mutations/channel.js @@ -30,9 +30,8 @@ export default { * @throws {Error} throws apollo level validation errors */ channelsEdit(root, { _id, ...doc }) { - Channels.updateChannel(_id, doc); // TODO: sendNotifications method shoul be here - return; + return Channels.updateChannel(_id, doc); }, /** @@ -42,6 +41,6 @@ export default { * @return {Promise} null */ channelsRemove(root, { _id }) { - return Channels.remove(_id); + return Channels.removeChannel(_id); }, }; diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index 2c78e2d9e..fb6a70738 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -69,12 +69,8 @@ class Channel { } static updateChannel(_id, doc) { - if (doc && doc._id) { - delete doc._id; - } - this.preSave(doc); - return this.update({ _id }, doc); + return this.update({ _id }, doc, { runValidators: true }); } static removeChannel(_id) { diff --git a/test.config.json b/test.config.json index dac0394ee..3b84d84d5 100644 --- a/test.config.json +++ b/test.config.json @@ -1,4 +1,4 @@ { - "testRegex": ".*test.js$", + "testRegex": ".*channelMutations.test.js$", "testEnvironment": "node" } From 552bfb01522ef0db08fa124167227e88608b47e1 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 8 Oct 2017 11:18:25 +0800 Subject: [PATCH 017/318] #13 merged --- src/__tests__/brandMutations.test.js | 114 ++++++++++++++++++ src/__tests__/emailTemplateMutations.test.js | 88 ++++++++++++++ src/__tests__/index.js | 0 .../responseTemplateMutations.test.js | 104 ++++++++++++++++ src/data/resolvers/mutations/brands.js | 51 ++++++++ src/data/resolvers/mutations/emailTemplate.js | 40 ++++++ src/data/resolvers/mutations/index.js | 6 + .../resolvers/mutations/responseTemplate.js | 40 ++++++ src/data/schema/brand.js | 12 +- src/data/schema/emailTemplate.js | 6 + src/data/schema/index.js | 17 ++- src/data/schema/responseTemplate.js | 8 ++ src/db/factories.js | 33 +++-- src/db/models/Brands.js | 26 +++- src/db/models/ResponseTemplates.js | 4 +- test.config.json | 2 +- 16 files changed, 522 insertions(+), 29 deletions(-) create mode 100644 src/__tests__/brandMutations.test.js create mode 100644 src/__tests__/emailTemplateMutations.test.js delete mode 100644 src/__tests__/index.js create mode 100644 src/__tests__/responseTemplateMutations.test.js create mode 100644 src/data/resolvers/mutations/brands.js create mode 100644 src/data/resolvers/mutations/emailTemplate.js create mode 100644 src/data/resolvers/mutations/responseTemplate.js diff --git a/src/__tests__/brandMutations.test.js b/src/__tests__/brandMutations.test.js new file mode 100644 index 000000000..e35d6fd25 --- /dev/null +++ b/src/__tests__/brandMutations.test.js @@ -0,0 +1,114 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Brands, Users } from '../db/models'; +import { brandFactory, userFactory } from '../db/factories'; +import brandMutations from '../data/resolvers/mutations/brands'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Brands mutations', () => { + let _brand; + let _user; + + beforeEach(async () => { + // Creating test data + _brand = await brandFactory(); + _user = await userFactory(); + }); + + afterEach(async () => { + // Clearing test data + await Brands.remove({}); + await Users.remove({}); + }); + + test('Create brand', async () => { + const brandObj = await brandMutations.brandsAdd( + {}, + { code: _brand.code, name: _brand.name, description: _brand.description }, + { user: _user }, + ); + expect(brandObj).toBeDefined(); + expect(brandObj.code).toBe(_brand.code); + expect(brandObj.name).toBe(_brand.name); + expect(brandObj.userId).toBe(_user._id); + + // invalid data + expect(() => + brandMutations.brandsAdd({}, { code: '', name: _brand.name }, { user: _user }), + ).toThrowError('Code is required field'); + + // Login required + expect(() => + brandMutations.brandsAdd({}, { code: _brand.code, name: brandObj.name }, {}), + ).toThrowError('Login required'); + }); + + test('Update brand', async () => { + // get new brand object + const _brand_update = await brandFactory(); + + // update brand object + const brandObj = await brandMutations.brandsEdit( + {}, + { + _id: _brand.id, + code: _brand_update.code, + name: _brand_update.name, + description: _brand_update.description, + }, + { user: _user }, + ); + + // check changes + expect(brandObj.code).toBe(_brand_update.code); + expect(brandObj.name).toBe(_brand_update.name); + expect(brandObj.description).toBe(_brand_update.description); + }); + + test('Update brand login required', async () => { + expect.assertions(1); + try { + await brandMutations.brandsEdit({}, { _id: _brand.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Delete brand', async () => { + const brandDeletedObj = await brandMutations.brandsRemove( + {}, + { _id: _brand.id }, + { user: _user }, + ); + expect(brandDeletedObj.id).toBe(_brand.id); + + const brandObj = await Brands.findOne({ _id: _brand.id }); + expect(brandObj).toBeNull(); + }); + + test('Delete brand login required', async () => { + expect.assertions(1); + try { + await brandMutations.brandsRemove({}, { _id: _brand.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Update brand email config', async () => { + const brandObj = await brandMutations.brandsConfigEmail( + {}, + { _id: _brand.id, emailConfig: _brand.emailConfig }, + { user: _brand.userId }, + ); + + expect(brandObj).toBeDefined(); + expect(brandObj.emailConfig.type).toBe(_brand.emailConfig.type); + expect(brandObj.emailConfig.template).toBe(_brand.emailConfig.template); + }); +}); diff --git a/src/__tests__/emailTemplateMutations.test.js b/src/__tests__/emailTemplateMutations.test.js new file mode 100644 index 000000000..025fb345f --- /dev/null +++ b/src/__tests__/emailTemplateMutations.test.js @@ -0,0 +1,88 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { EmailTemplates, Users } from '../db/models'; +import { emailTemplateFactory, userFactory } from '../db/factories'; +import emailTemplateMutations from '../data/resolvers/mutations/emailTemplate'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Email template mutations', () => { + let _emailTemplate; + let _user; + + beforeEach(async () => { + // Creating test data + _emailTemplate = await emailTemplateFactory(); + _user = await userFactory(); + }); + + afterEach(async () => { + // Clearing test data + await EmailTemplates.remove({}); + await Users.remove({}); + }); + + test('Create email template', async () => { + const emailTemplateObj = await emailTemplateMutations.emailTemplateAdd( + {}, + { name: _emailTemplate.name, content: _emailTemplate.content }, + { user: _user }, + ); + expect(emailTemplateObj).toBeDefined(); + expect(emailTemplateObj.name).toBe(_emailTemplate.name); + expect(emailTemplateObj.content).toBe(_emailTemplate.content); + + // Login required test + expect(() => + emailTemplateMutations.emailTemplateAdd( + {}, + { name: _emailTemplate.name, content: _emailTemplate.content }, + {}, + ), + ).toThrowError('Login required'); + }); + + test('Update email template', async () => { + const emailTemplateObj = await emailTemplateMutations.emailTemplateEdit( + {}, + { _id: _emailTemplate.id, name: _emailTemplate.name, content: _emailTemplate.content }, + { user: _user }, + ); + expect(emailTemplateObj).toBeDefined(); + expect(emailTemplateObj.id).toBe(_emailTemplate.id); + expect(emailTemplateObj.name).toBe(_emailTemplate.name); + expect(emailTemplateObj.content).toBe(_emailTemplate.content); + }); + + test('Update email template login required', async () => { + expect.assertions(1); + try { + await emailTemplateMutations.emailTemplateEdit({}, { _id: _emailTemplate.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Delete email template', async () => { + await emailTemplateMutations.emailTemplateRemove( + {}, + { _id: _emailTemplate.id }, + { user: _user }, + ); + const count = await EmailTemplates.find({ _id: _emailTemplate.id }).count(); + expect(count).toBe(0); + }); + + test('Delete email template login required', async () => { + expect.assertions(1); + try { + await emailTemplateMutations.emailTemplateRemove({}, { _id: _emailTemplate.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); +}); diff --git a/src/__tests__/index.js b/src/__tests__/index.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/__tests__/responseTemplateMutations.test.js b/src/__tests__/responseTemplateMutations.test.js new file mode 100644 index 000000000..a26f36784 --- /dev/null +++ b/src/__tests__/responseTemplateMutations.test.js @@ -0,0 +1,104 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { ResponseTemplates, Users } from '../db/models'; +import { responseTemplateFactory, userFactory } from '../db/factories'; +import responseTemplateMutations from '../data/resolvers/mutations/responseTemplate'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Response template mutations', () => { + let _responseTemplate; + let _user; + + beforeEach(async () => { + // Creating test data + _responseTemplate = await responseTemplateFactory(); + _user = await userFactory(); + }); + + afterEach(async () => { + // Clearing test data + await ResponseTemplates.remove({}); + await Users.remove({}); + }); + + test('Create response template', async () => { + const responseTemplateObj = await responseTemplateMutations.responseTemplateAdd( + {}, + { + name: _responseTemplate.name, + content: _responseTemplate.content, + brandId: _responseTemplate.brandId, + files: _responseTemplate.files, + }, + { user: _user }, + ); + expect(responseTemplateObj).toBeDefined(); + expect(responseTemplateObj.name).toBe(_responseTemplate.name); + expect(responseTemplateObj.content).toBe(_responseTemplate.content); + expect(responseTemplateObj.brandId).toBe(_responseTemplate.brandId); + expect(responseTemplateObj.files[0]).toBe(_responseTemplate.files[0]); + + // login required test + expect(() => + responseTemplateMutations.responseTemplateAdd( + {}, + { name: _responseTemplate.name, content: _responseTemplate.content }, + {}, + ), + ).toThrowError('Login required'); + }); + + test('Update response template', async () => { + const responseTemplateObj = await responseTemplateMutations.responseTemplateEdit( + {}, + { + _id: _responseTemplate.id, + name: _responseTemplate.name, + content: _responseTemplate.content, + brandId: _responseTemplate.brandId, + files: _responseTemplate.files, + }, + { user: _user }, + ); + expect(responseTemplateObj).toBeDefined(); + expect(responseTemplateObj.id).toBe(_responseTemplate.id); + expect(responseTemplateObj.name).toBe(_responseTemplate.name); + expect(responseTemplateObj.content).toBe(_responseTemplate.content); + expect(responseTemplateObj.brandId).toBe(_responseTemplate.brandId); + expect(responseTemplateObj.files[0]).toBe(_responseTemplate.files[0]); + }); + + test('Update response template login required', async () => { + expect.assertions(1); + try { + await responseTemplateMutations.responseTemplateEdit({}, { _id: _responseTemplate.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Delete response template', async () => { + const deletedObj = await responseTemplateMutations.responseTemplateRemove( + {}, + { _id: _responseTemplate.id }, + { user: _user }, + ); + expect(deletedObj.id).toBe(_responseTemplate.id); + const emailTemplateObj = await ResponseTemplates.findOne({ _id: _responseTemplate.id }); + expect(emailTemplateObj).toBeNull(); + }); + + test('Delete response template login required', async () => { + expect.assertions(1); + try { + await responseTemplateMutations.responseTemplateRemove({}, { _id: _responseTemplate.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); +}); diff --git a/src/data/resolvers/mutations/brands.js b/src/data/resolvers/mutations/brands.js new file mode 100644 index 000000000..9248db359 --- /dev/null +++ b/src/data/resolvers/mutations/brands.js @@ -0,0 +1,51 @@ +import { Brands } from '../../../db/models'; + +export default { + /** + * Create new brand + * @return {Promise} brand object + */ + brandsAdd(root, doc, { user }) { + if (!user) throw new Error('Login required'); + + if (!doc.code) throw new Error('Code is required field'); + + return Brands.createBrand({ userId: user._id, ...doc }); + }, + + /** + * Update brand + * @return {Promise} brand object + */ + async brandsEdit(root, { _id, ...fields }, { user }) { + if (!user) throw new Error('Login required'); + + await Brands.update({ _id }, { $set: { ...fields } }); + return Brands.findOne({ _id }); + }, + + /** + * Delete brand + * @return {Promise} + */ + async brandsRemove(root, { _id }, { user }) { + if (!user) throw new Error('Login required'); + + const brandObj = await Brands.findOne({ _id }); + + if (!brandObj) throw new Error(`Brand not found with id ${_id}`); + + return brandObj.remove(); + }, + + /** + * Update brands email config + * @return {Promise} brand object + */ + async brandsConfigEmail(root, { _id, emailConfig }, { user }) { + if (!user) throw new Error('Login required'); + + await Brands.update({ _id }, { emailConfig }); + return Brands.findOne({ _id }); + }, +}; diff --git a/src/data/resolvers/mutations/emailTemplate.js b/src/data/resolvers/mutations/emailTemplate.js new file mode 100644 index 000000000..0d5c2cde7 --- /dev/null +++ b/src/data/resolvers/mutations/emailTemplate.js @@ -0,0 +1,40 @@ +import { EmailTemplates } from '../../../db/models'; + +export default { + /** + * Create new email template + * @return {Promise} email template object + */ + emailTemplateAdd(root, doc, { user }) { + if (!user) throw new Error('Login required'); + + return EmailTemplates.create(doc); + }, + + /** + * Update email template + * @return {Promise} email template object + */ + async emailTemplateEdit(root, { _id, ...fields }, { user }) { + if (!user) throw new Error('Login required'); + + await EmailTemplates.update({ _id }, { $set: { ...fields } }); + return EmailTemplates.findOne({ _id }); + }, + + /** + * Delete email template + * @return {Promise} + */ + async emailTemplateRemove(root, { _id }, { user }) { + if (!user) throw new Error('Login required'); + + const emailTemplateObj = await EmailTemplates.findOne({ _id }); + + if (!emailTemplateObj) { + throw new Error(`Email template not found with id ${_id}`); + } + + return emailTemplateObj.remove(); + }, +}; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index d7f7503f6..18ec272db 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -1,10 +1,16 @@ import conversation from './conversation'; +import brands from './brands'; +import emailTemplate from './emailTemplate'; +import responseTemplate from './responseTemplate'; import channel from './channel'; import form from './form'; import integration from './integration'; export default { ...conversation, + ...brands, + ...emailTemplate, + ...responseTemplate, ...channel, ...form, ...integration, diff --git a/src/data/resolvers/mutations/responseTemplate.js b/src/data/resolvers/mutations/responseTemplate.js new file mode 100644 index 000000000..b18b4ddf4 --- /dev/null +++ b/src/data/resolvers/mutations/responseTemplate.js @@ -0,0 +1,40 @@ +import { ResponseTemplates } from '../../../db/models'; + +export default { + /** + * Create new response template + * @return {Promise} response template object + */ + responseTemplateAdd(root, doc, { user }) { + if (!user) throw new Error('Login required'); + + return ResponseTemplates.create(doc); + }, + + /** + * Update response template + * @return {Promise} response template object + */ + async responseTemplateEdit(root, { _id, ...fields }, { user }) { + if (!user) throw new Error('Login required'); + + await ResponseTemplates.update({ _id }, { $set: { ...fields } }); + return ResponseTemplates.findOne({ _id }); + }, + + /** + * Delete response template + * @return {Promise} + */ + async responseTemplateRemove(root, { _id }, { user }) { + if (!user) throw new Error('Login required'); + + const responseTemplateObj = await ResponseTemplates.findOne({ _id }); + + if (!responseTemplateObj) { + throw new Error(`Response template not found with id ${_id}`); + } + + return responseTemplateObj.remove(); + }, +}; diff --git a/src/data/schema/brand.js b/src/data/schema/brand.js index b093cc4bc..2f67b7096 100644 --- a/src/data/schema/brand.js +++ b/src/data/schema/brand.js @@ -10,15 +10,15 @@ export const types = ` } `; +export const queries = ` + brands(limit: Int): [Brand] + brandDetail(_id: String!): Brand + brandsTotalCount: Int +`; + export const mutations = ` brandsAdd(code: String!, name: String, description: String): Brand brandsEdit(_id: String!, code: String, name: String, description: String): Brand brandsRemove(_id: String!): Brand brandsConfigEmail(_id: String!, emailConfig: JSON): Brand `; - -export const queries = ` - brands(limit: Int): [Brand] - brandDetail(_id: String!): Brand - brandsTotalCount: Int -`; diff --git a/src/data/schema/emailTemplate.js b/src/data/schema/emailTemplate.js index 9402c3144..89f1482d1 100644 --- a/src/data/schema/emailTemplate.js +++ b/src/data/schema/emailTemplate.js @@ -10,3 +10,9 @@ export const queries = ` emailTemplates(limit: Int): [EmailTemplate] emailTemplatesTotalCount: Int `; + +export const mutations = ` + emailTemplateAdd(name: String, content: String): EmailTemplate + emailTemplateEdit(_id: String!, name: String, content: String): EmailTemplate + emailTemplateRemove(_id: String!): EmailTemplate +`; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index 6fb2eac29..22a741981 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -6,7 +6,7 @@ import { mutations as ChannelMutations, } from './channel'; -import { types as BrandTypes, queries as BrandQueries } from './brand'; +import { types as BrandTypes, queries as BrandQueries, mutations as BrandMutations } from './brand'; import { types as IntegrationTypes, @@ -14,9 +14,17 @@ import { mutations as IntegrationMutations, } from './integration'; -import { types as ResponseTemplate, queries as ResponseTemplateQueries } from './responseTemplate'; +import { + types as ResponseTemplate, + queries as ResponseTemplateQueries, + mutations as ResponseTemplateMutations, +} from './responseTemplate'; -import { types as EmailTemplate, queries as EmailTemplateQueries } from './emailTemplate'; +import { + types as EmailTemplate, + queries as EmailTemplateQueries, + mutations as EmailTemplateMutations, +} from './emailTemplate'; import { types as FormTypes, queries as FormQueries, mutations as FormMutatons } from './form'; @@ -76,6 +84,9 @@ export const queries = ` export const mutations = ` type Mutation { ${ConversationMutations} + ${BrandMutations} + ${ResponseTemplateMutations} + ${EmailTemplateMutations} ${ChannelMutations} ${FormMutatons} ${IntegrationMutations} diff --git a/src/data/schema/responseTemplate.js b/src/data/schema/responseTemplate.js index b8d2c151e..cdfe1b2e9 100644 --- a/src/data/schema/responseTemplate.js +++ b/src/data/schema/responseTemplate.js @@ -13,3 +13,11 @@ export const queries = ` responseTemplates(limit: Int): [ResponseTemplate] responseTemplatesTotalCount: Int `; + +export const mutations = ` + responseTemplateAdd(name: String, content: String, brandId: String, files: JSON): + ResponseTemplate + responseTemplateEdit(_id: String!, name: String, content: String, brandId: String, files: JSON): + ResponseTemplate + responseTemplateRemove(_id: String!): ResponseTemplate +`; diff --git a/src/db/factories.js b/src/db/factories.js index 220e163c0..621a02a05 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -1,4 +1,3 @@ -import shortid from 'shortid'; import faker from 'faker'; import Random from 'meteor-random'; @@ -24,26 +23,11 @@ export const userFactory = (params = {}) => { return user.save(); }; -export const integrationFactory = params => { - const kind = params.kind || 'messenger'; - return Integrations.create({ - name: faker.random.word(), - kind: kind, - brandId: params.brandId || Random.id(), - formId: params.formId || Random.id(), - messengerData: params.messengerData || { welcomeMessage: 'welcome' }, - formData: - params.formData === 'form' - ? params.formData - : kind === 'form' ? { thankContent: 'thankContent' } : null, - }); -}; - export const brandFactory = (params = {}) => { const brand = new Brands({ name: faker.random.word(), code: params.code || faker.random.word(), - userId: Random.id(), + userId: () => Random.id(), description: params.description || faker.random.word(), emailConfig: { type: 'simple', @@ -74,6 +58,21 @@ export const responseTemplateFactory = (params = {}) => { return responseTemplate.save(); }; +export const integrationFactory = params => { + const kind = params.kind || 'messenger'; + return Integrations.create({ + name: faker.random.word(), + kind: kind, + brandId: params.brandId || Random.id(), + formId: params.formId || Random.id(), + messengerData: params.messengerData || { welcomeMessage: 'welcome' }, + formData: + params.formData === 'form' + ? params.formData + : kind === 'form' ? { thankContent: 'thankContent' } : null, + }); +}; + export const tagsFactory = (params = {}) => { const tag = new Tags({ name: faker.random.word(), diff --git a/src/db/models/Brands.js b/src/db/models/Brands.js index f1b521f4e..ee8a1bc9f 100644 --- a/src/db/models/Brands.js +++ b/src/db/models/Brands.js @@ -1,6 +1,14 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +const BrandEmailConfigSchema = mongoose.Schema({ + type: { + type: String, + allowedValues: ['simple', 'custom'], + }, + template: String, +}); + const BrandSchema = mongoose.Schema({ _id: { type: String, @@ -12,9 +20,25 @@ const BrandSchema = mongoose.Schema({ description: String, userId: String, createdAt: Date, - emailConfig: Object, + emailConfig: BrandEmailConfigSchema, }); +class Brand { + /** + * Create a brand + * @param {Object} brandObj object + * @return {Promise} Newly created brand object + */ + static createBrand(doc) { + return this.create({ + ...doc, + createdAt: new Date(), + }); + } +} + +BrandSchema.loadClass(Brand); + const Brands = mongoose.model('brands', BrandSchema); export default Brands; diff --git a/src/db/models/ResponseTemplates.js b/src/db/models/ResponseTemplates.js index ae27cd10d..a3628c761 100644 --- a/src/db/models/ResponseTemplates.js +++ b/src/db/models/ResponseTemplates.js @@ -10,7 +10,9 @@ const ResponseTemplateSchema = mongoose.Schema({ name: String, content: String, brandId: String, - files: [Object], + files: { + type: Array, + }, }); const ResponseTemplates = mongoose.model('response_templates', ResponseTemplateSchema); diff --git a/test.config.json b/test.config.json index 3b84d84d5..e1eedd291 100644 --- a/test.config.json +++ b/test.config.json @@ -1,4 +1,4 @@ { - "testRegex": ".*channelMutations.test.js$", + "testRegex": ".*.test.js$", "testEnvironment": "node" } From c58de0eb2dc1fedbbb2b9964714270895b87ccf8 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 8 Oct 2017 11:24:07 +0800 Subject: [PATCH 018/318] Add sendEmail util --- package.json | 1 + src/data/utils.js | 27 +++++++++++++++++++++++++++ yarn.lock | 4 ++++ 3 files changed, 32 insertions(+) create mode 100644 src/data/utils.js diff --git a/package.json b/package.json index 561cf99eb..696770edd 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "meteor-random": "^0.0.3", "moment": "^2.18.1", "mongoose": "^4.9.2", + "nodemailer": "^4.1.3", "passport": "^0.4.0", "passport-anonymous": "^1.0.1", "passport-http-bearer": "^1.0.1", diff --git a/src/data/utils.js b/src/data/utils.js new file mode 100644 index 000000000..64d0a98e3 --- /dev/null +++ b/src/data/utils.js @@ -0,0 +1,27 @@ +import nodemailer from 'nodemailer'; + +export const sendEmail = ({ toEmails, fromEmail, title, content }) => { + const { MAIL_SERVICE, MAIL_USER, MAIL_PASS } = process.env; + + const transporter = nodemailer.createTransport({ + service: MAIL_SERVICE, + auth: { + user: MAIL_USER, + pass: MAIL_PASS, + }, + }); + + toEmails.forEach(toEmail => { + const mailOptions = { + from: fromEmail, + to: toEmail, + subject: title, + text: content, + }; + + transporter.sendMail(mailOptions, (error, info) => { + console.log(error); // eslint-disable-line + console.log(info); // eslint-disable-line + }); + }); +}; diff --git a/yarn.lock b/yarn.lock index 5fe574607..c5521ba4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3175,6 +3175,10 @@ node-pre-gyp@^0.6.36: tar "^2.2.1" tar-pack "^3.4.0" +nodemailer@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.1.3.tgz#4125a6ef79ecfb68357a65c34e4810f210ae120c" + nodemon@^1.11.0: version "1.12.1" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.12.1.tgz#996a56dc49d9f16bbf1b78a4de08f13634b3878d" From 9ec15980da4e46810489a2920cb7735b094f00d2 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 8 Oct 2017 12:53:15 +0800 Subject: [PATCH 019/318] #13 changes based on reviews -email --- src/__tests__/channelMutations.test.js | 35 +++++--------------------- src/db/models/Channels.js | 14 +++++------ src/db/models/Forms.js | 4 +-- src/db/models/Integrations.js | 16 ++++++------ 4 files changed, 23 insertions(+), 46 deletions(-) diff --git a/src/__tests__/channelMutations.test.js b/src/__tests__/channelMutations.test.js index 729349a79..7fae7080b 100644 --- a/src/__tests__/channelMutations.test.js +++ b/src/__tests__/channelMutations.test.js @@ -118,6 +118,12 @@ describe('channel update tests', () => { channel = await Channels.findOne({ _id: channel._id }); expect(channel.memberIds.length).toBe(1); expect(channel.memberIds[0]).toBe(_user._id); + + await Channels.updateChannel(channel._id, { + name: 'Channel test 2', + }); + + expect(channel.description).toBe('Channel test description'); }); }); @@ -229,32 +235,3 @@ describe('mutations', () => { expect(channelCount).toBe(0); }); }); - -// describe('channel remove test', () => { -// let _channel; -// -// /** -// * Before each test create test data -// * containing 2 users and an integration -// */ -// beforeEach(async () => { -// const user = await userFactory({}); -// _channel = await Channels.createChannel({ -// name: 'Channel test', -// userId: user._id, -// }); -// }); -// -// /** -// * Remove test data -// */ -// afterEach(async () => { -// await Channels.remove({}); -// }); -// -// test('channel remove test', async () => { -// await Channels.removeChannel(_channel._id); -// const channelCount = await Channels.find({}).count(); -// expect(channelCount).toBe(0); -// }); -// }); diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index fb6a70738..7bd3b6a39 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -42,12 +42,6 @@ class Channel { * */ static preSave(doc) { - const { userId } = doc; - - if (!userId) { - throw new ChannelCreationException('userId must be supplied'); - } - doc.memberIds = doc.memberIds || []; if (!doc.memberIds.includes(doc.userId)) { @@ -62,6 +56,12 @@ class Channel { * @return {Promise} Newly created channel obj */ static createChannel(doc) { + const { userId } = doc; + + if (!userId) { + throw new ChannelCreationException('userId must be supplied'); + } + this.preSave(doc); doc.conversationCount = 0; doc.openConversationCount = 0; @@ -70,7 +70,7 @@ class Channel { static updateChannel(_id, doc) { this.preSave(doc); - return this.update({ _id }, doc, { runValidators: true }); + return this.update({ _id }, { $set: doc }, { runValidators: true }); } static removeChannel(_id) { diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index fd12598a3..d1599dd43 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -42,7 +42,7 @@ class Form { } static updateForm(_id, doc) { - return this.update({ _id }, doc, { runValidators: true }); + return this.update({ _id }, { $set: doc }, { runValidators: true }); } static async removeForm(_id) { @@ -136,7 +136,7 @@ class FormField { } static updateFormField(_id, doc) { - return this.update({ _id }, doc, { runValidators: true }); + return this.update({ _id }, { $set: doc }, { runValidators: true }); } static removeFormField(_id) { diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index 984679b43..d6d735a5c 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -125,20 +125,20 @@ class Integration { }); } - static updateMessengerIntegration(id, { name, brandId }) { - return this.update({ _id: id }, { name, brandId }, { runValidators: true }); + static updateMessengerIntegration(_id, { name, brandId }) { + return this.update({ _id }, { $set: { name, brandId } }, { runValidators: true }); } - static saveMessengerAppearanceData(id, { color, wallpaper, logo }) { + static saveMessengerAppearanceData(_id, { color, wallpaper, logo }) { return this.update( - { _id: id }, - { uiOptions: { color, wallpaper, logo } }, + { _id }, + { $set: { uiOptions: { color, wallpaper, logo } } }, { runValdatiors: true }, ); } - static saveMessengerConfigs(id, messengerData) { - return this.update({ _id: id }, { messengerData }, { runValidators: true }); + static saveMessengerConfigs(_id, messengerData) { + return this.update({ _id }, { $set: { messengerData } }, { runValidators: true }); } static createFormIntegration({ formData, ...mainDoc }) { @@ -153,7 +153,7 @@ class Integration { static updateFormIntegration(id, { formData, ...mainDoc }) { const doc = this.generateFormDoc(mainDoc, formData); - return this.update({ _id: id }, doc, { runValidators: true }); + return this.update({ _id: id }, { $set: doc }, { runValidators: true }); } static async removeIntegration(id) { From 48219a75fb24f8f34208f38ae424b0ca5ceb5103 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 8 Oct 2017 13:22:19 +0800 Subject: [PATCH 020/318] #13 code standard changes, test.config.json removed --- package.json | 2 +- src/__tests__/integrationMutations.test.js | 2 ++ test.config.json | 4 ---- 3 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 test.config.json diff --git a/package.json b/package.json index 8ba58f44c..0eabb22c5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "lint": "eslint src", "format": "prettier --write --print-width 100 --single-quote --trailing-comma all 'src/**/*.js'", "precommit": "lint-staged", - "test": "jest --config=test.config.json" + "test": "jest" }, "lint-staged": { "*.js": [ diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js index 96df93cad..888e2edf2 100644 --- a/src/__tests__/integrationMutations.test.js +++ b/src/__tests__/integrationMutations.test.js @@ -280,6 +280,7 @@ describe('save integration messenger configurations test', () => { let _integration; /** + * Create integration object to be used with messenger data configurations */ beforeEach(async () => { _brand = await brandFactory({}); @@ -291,6 +292,7 @@ describe('save integration messenger configurations test', () => { }); /** + * Delete test data */ afterEach(async () => { await Brands.remove({}); diff --git a/test.config.json b/test.config.json deleted file mode 100644 index e1eedd291..000000000 --- a/test.config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "testRegex": ".*.test.js$", - "testEnvironment": "node" -} From a292292937cf01ca917558922394244bf6976979 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 8 Oct 2017 13:24:55 +0800 Subject: [PATCH 021/318] Add runInBand option in jest command --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 696770edd..d629e8f27 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "build": "babel src --out-dir dist --ignore __tests__,tests --copy-files", "lint": "eslint src", "format": "prettier --write --print-width 100 --single-quote --trailing-comma all 'src/**/*.js'", - "precommit": "lint-staged" + "precommit": "lint-staged", + "test": "jest --runInBand" }, "lint-staged": { "*.js": [ From 75acbe6955ccab7f60b8cf8838ef88799c43a471 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 8 Oct 2017 17:18:15 +0800 Subject: [PATCH 022/318] Add customersEdit mutation --- src/__tests__/customerMutations.test.js | 56 +++++++++++++++++++++++ src/data/resolvers/mutations/customers.js | 13 ++++++ src/data/resolvers/mutations/index.js | 2 + src/data/schema/customer.js | 10 ++++ src/data/schema/index.js | 7 ++- src/db/factories.js | 20 +++++++- src/db/models/Customers.js | 12 +++++ 7 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/customerMutations.test.js create mode 100644 src/data/resolvers/mutations/customers.js diff --git a/src/__tests__/customerMutations.test.js b/src/__tests__/customerMutations.test.js new file mode 100644 index 000000000..50be8759a --- /dev/null +++ b/src/__tests__/customerMutations.test.js @@ -0,0 +1,56 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Customers, Users } from '../db/models'; +import { userFactory, customerFactory } from '../db/factories'; +import customerMutations from '../data/resolvers/mutations/customers'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Customers mutations', () => { + let _user; + let _customer; + + beforeEach(async () => { + // Creating test data + _user = await userFactory(); + _customer = await customerFactory(); + }); + + afterEach(async () => { + // Clearing test data + await Customers.remove({}); + await Users.remove({}); + }); + + test('Edit customer login required', async () => { + expect.assertions(1); + + try { + await customerMutations.customersEdit({}, { _id: _customer.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Edit customer valid', async () => { + const doc = { + name: 'Dombo', + email: 'dombo@yahoo.com', + phone: '242442200', + }; + + const customerObj = await customerMutations.customersEdit( + {}, + { _id: _customer._id, ...doc }, + { user: _user }, + ); + + expect(customerObj.name).toBe(doc.name); + expect(customerObj.email).toBe(doc.email); + expect(customerObj.phone).toBe(doc.phone); + }); +}); diff --git a/src/data/resolvers/mutations/customers.js b/src/data/resolvers/mutations/customers.js new file mode 100644 index 000000000..1c6b19044 --- /dev/null +++ b/src/data/resolvers/mutations/customers.js @@ -0,0 +1,13 @@ +import { Customers } from '../../../db/models'; + +export default { + /** + * Update customer + * @return {Promise} customer object + */ + async customersEdit(root, { _id, ...doc }, { user }) { + if (!user) throw new Error('Login required'); + + return Customers.updateCustomer(_id, doc); + }, +}; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index 7cd6cb7d0..f8ecc4a17 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -2,6 +2,7 @@ import conversation from './conversation'; import brands from './brands'; import emailTemplate from './emailTemplate'; import responseTemplate from './responseTemplate'; +import customers from './customers'; import segments from './segments'; import companies from './companies'; @@ -10,6 +11,7 @@ export default { ...brands, ...emailTemplate, ...responseTemplate, + ...customers, ...segments, ...companies, }; diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index f66fd9cec..0a058715a 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -37,3 +37,13 @@ export const queries = ` customerListForSegmentPreview(segment: JSON, limit: Int): [Customer] customersTotalCount: Int `; + +const fields = ` + name: String + email: String + phone: String +`; + +export const mutations = ` + customersEdit(_id: String!, ${fields}): Customer +`; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index a99841c06..e9a5f16d1 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -26,7 +26,11 @@ import { types as EngageTypes, queries as EngageQueries } from './engage'; import { types as TagTypes, queries as TagQueries } from './tag'; -import { types as CustomerTypes, queries as CustomerQueries } from './customer'; +import { + types as CustomerTypes, + queries as CustomerQueries, + mutations as CustomerMutations, +} from './customer'; import { types as SegmentTypes, @@ -91,6 +95,7 @@ export const mutations = ` ${BrandMutations} ${ResponseTemplateMutations} ${EmailTemplateMutations} + ${CustomerMutations} ${SegmentMutations} } `; diff --git a/src/db/factories.js b/src/db/factories.js index a752ee514..1c6f40b0f 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -1,7 +1,15 @@ import faker from 'faker'; import Random from 'meteor-random'; -import { Users, Brands, EmailTemplates, ResponseTemplates, Segments, Companies } from './models'; +import { + Users, + Brands, + EmailTemplates, + ResponseTemplates, + Customers, + Segments, + Companies, +} from './models'; export const userFactory = (params = {}) => { const user = new Users({ @@ -82,3 +90,13 @@ export const companyFactory = (params = {}) => { return company.save(); }; + +export const customerFactory = (params = {}) => { + const customer = new Customers({ + name: params.name || faker.random.word(), + email: params.email || faker.internet.email(), + phone: params.phone || faker.random.word(), + }); + + return customer.save(); +}; diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index 867b15e4e..049ba5d67 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -25,6 +25,18 @@ const CustomerSchema = mongoose.Schema({ }); class Customer { + /* + * Update customer + * @param {String} _id customer id to update + * @param {Object} doc field values to update + * @return {Promise} updated customer object + */ + static async updateCustomer(_id, doc) { + await this.update({ _id }, { $set: doc }); + + return this.findOne({ _id }); + } + /** * Mark customer as inactive * @param {String} customerId From 770ee68c4bc94b562012cfbe6bad4c17dc2d47eb Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 9 Oct 2017 15:43:23 +0800 Subject: [PATCH 023/318] Add field mutations --- src/__tests__/fieldDb.test.js | 77 +++++++++++++++++ src/__tests__/fieldMutations.test.js | 110 +++++++++++++++++++++++++ src/constants.js | 6 ++ src/data/resolvers/mutations/fields.js | 33 ++++++++ src/data/resolvers/mutations/index.js | 2 + src/data/schema/field.js | 30 +++++++ src/data/schema/index.js | 4 + src/db/factories.js | 24 ++++++ src/db/models/Fields.js | 109 ++++++++++++++++++++++++ src/db/models/index.js | 2 + 10 files changed, 397 insertions(+) create mode 100644 src/__tests__/fieldDb.test.js create mode 100644 src/__tests__/fieldMutations.test.js create mode 100644 src/constants.js create mode 100644 src/data/resolvers/mutations/fields.js create mode 100644 src/data/schema/field.js create mode 100644 src/db/models/Fields.js diff --git a/src/__tests__/fieldDb.test.js b/src/__tests__/fieldDb.test.js new file mode 100644 index 000000000..ab63ca2c3 --- /dev/null +++ b/src/__tests__/fieldDb.test.js @@ -0,0 +1,77 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Fields } from '../db/models'; +import { formFactory, fieldFactory } from '../db/factories'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +/** + * Field related tests + */ +describe('Fields', () => { + beforeEach(async () => { + // creating field with contentType other than customer + await fieldFactory({ contentType: 'form', order: 1 }); + }); + + afterEach(() => { + // Clearing test fields + return Fields.remove({}); + }); + + test('createField() without contentTypeId', async () => { + // first attempt + let field = await Fields.createField({ contentType: 'customer' }); + expect(field.order).toBe(0); + + // second attempt + field = await Fields.createField({ contentType: 'customer' }); + expect(field.order).toBe(1); + + // third attempt + field = await Fields.createField({ contentType: 'customer' }); + expect(field.order).toBe(2); + }); + + test('createField() with contentTypeId', async () => { + const contentType = 'form'; + const form1 = await formFactory({}); + const form2 = await formFactory({}); + + // first attempt + let field = await Fields.createField({ contentType, contentTypeId: form1._id }); + expect(field.order).toBe(0); + + // second attempt + field = await Fields.createField({ contentType, contentTypeId: form1._id }); + expect(field.order).toBe(1); + + // must create new order + field = await Fields.createField({ contentType, contentTypeId: form2._id }); + expect(field.order).toBe(0); + }); + + test('createField() required contentTypeId when form', async () => { + expect.assertions(1); + + try { + await Fields.createField({ contentType: 'form' }); + } catch (e) { + expect(e.message).toEqual('Content type id is required'); + } + }); + + test('createField() check contentTypeId existence', async () => { + expect.assertions(1); + + try { + await Fields.createField({ contentType: 'form', contentTypeId: 'DFAFDFADS' }); + } catch (e) { + expect(e.message).toEqual('Form not found with _id of DFAFDFADS'); + } + }); +}); diff --git a/src/__tests__/fieldMutations.test.js b/src/__tests__/fieldMutations.test.js new file mode 100644 index 000000000..b4ccf2159 --- /dev/null +++ b/src/__tests__/fieldMutations.test.js @@ -0,0 +1,110 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import faker from 'faker'; +import { connect, disconnect } from '../db/connection'; +import { Fields, Users } from '../db/models'; +import { userFactory, fieldFactory } from '../db/factories'; +import fieldMutations from '../data/resolvers/mutations/fields'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +/* + * Generate test data + */ +const generateData = () => ({ + type: 'input', + validation: 'number', + text: faker.random.word(), + description: faker.random.word(), + isRequired: false, + order: 0, +}); + +/* + * Check values + */ +const checkValues = (fieldObj, doc) => { + expect(fieldObj.type).toBe(doc.type); + expect(fieldObj.validation).toBe(doc.validation); + expect(fieldObj.text).toBe(doc.text); + expect(fieldObj.description).toBe(doc.description); + expect(fieldObj.isRequired).toBe(doc.isRequired); + expect(fieldObj.order).toBe(doc.order); +}; + +describe('Fields mutations', () => { + let _user; + let _field; + + beforeEach(async () => { + // Creating test data + _user = await userFactory(); + _field = await fieldFactory(); + }); + + afterEach(async () => { + // Clearing test data + await Fields.remove({}); + await Users.remove({}); + }); + + test('Create field', async () => { + // Login required + expect(() => fieldMutations.fieldsAdd({}, {}, {})).toThrowError('Login required'); + + // valid + const doc = generateData(); + + const fieldObj = await fieldMutations.fieldsAdd({}, doc, { user: _user }); + + checkValues(fieldObj, doc); + }); + + test('Edit field login required', async () => { + expect.assertions(1); + + try { + await fieldMutations.fieldsEdit({}, { _id: _field.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Edit field valid', async () => { + const doc = generateData(); + + const fieldObj = await fieldMutations.fieldsEdit( + {}, + { _id: _field._id, ...doc }, + { user: _user }, + ); + + checkValues(fieldObj, doc); + }); + + test('Remove field login required', async () => { + expect.assertions(1); + + try { + await fieldMutations.fieldsRemove({}, { _id: _field.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Remove field valid', async () => { + const fieldDeletedObj = await fieldMutations.fieldsRemove( + {}, + { _id: _field.id }, + { user: _user }, + ); + + expect(fieldDeletedObj.id).toBe(_field.id); + + const fieldObj = await Fields.findOne({ _id: _field.id }); + expect(fieldObj).toBeNull(); + }); +}); diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 000000000..8afc58ad1 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,6 @@ +export const FIELD_CONTENT_TYPES = { + FORM: 'form', + CUSTOMER: 'customer', + COMPANY: 'company', + ALL_LIST: ['form', 'customer', 'company'], +}; diff --git a/src/data/resolvers/mutations/fields.js b/src/data/resolvers/mutations/fields.js new file mode 100644 index 000000000..309347a9d --- /dev/null +++ b/src/data/resolvers/mutations/fields.js @@ -0,0 +1,33 @@ +import { Fields } from '../../../db/models'; + +export default { + /** + * Adds field object + * @return {Promise} + */ + fieldsAdd(root, args, { user }) { + if (!user) throw new Error('Login required'); + + return Fields.createField(args); + }, + + /** + * Updates field object + * @return {Promise} return Promise(null) + */ + fieldsEdit(root, { _id, ...doc }, { user }) { + if (!user) throw new Error('Login required'); + + return Fields.updateField(_id, doc); + }, + + /** + * Remove a channel + * @return {Promise} + */ + fieldsRemove(root, { _id }, { user }) { + if (!user) throw new Error('Login required'); + + return Fields.removeField(_id); + }, +}; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index f8ecc4a17..69ea1b6e4 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -5,6 +5,7 @@ import responseTemplate from './responseTemplate'; import customers from './customers'; import segments from './segments'; import companies from './companies'; +import fields from './fields'; export default { ...conversation, @@ -14,4 +15,5 @@ export default { ...customers, ...segments, ...companies, + ...fields, }; diff --git a/src/data/schema/field.js b/src/data/schema/field.js new file mode 100644 index 000000000..f66b45a59 --- /dev/null +++ b/src/data/schema/field.js @@ -0,0 +1,30 @@ +export const types = ` + type Field { + _id: String! + contentType: String! + contentTypeId: String + type: String + validation: String + text: String + description: String + options: [String] + isRequired: Boolean + order: Int + } +`; + +const commonFields = ` + type: String + validation: String + text: String + description: String + options: [String] + isRequired: Boolean + order: Int +`; + +export const mutations = ` + fieldsAdd(contentType: String!, contentTypeId: String, ${commonFields}): Field + fieldsEdit(_id: String!, ${commonFields}): Field + fieldsRemove(_id: String!): Field +`; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index e9a5f16d1..b3d23dfc4 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -20,6 +20,8 @@ import { mutations as EmailTemplateMutations, } from './emailTemplate'; +import { types as FieldTypes, mutations as FieldMutations } from './field'; + import { types as FormTypes, queries as FormQueries } from './form'; import { types as EngageTypes, queries as EngageQueries } from './engage'; @@ -61,6 +63,7 @@ export const types = ` ${EmailTemplate} ${EngageTypes} ${TagTypes} + ${FieldTypes} ${FormTypes} ${CustomerTypes} ${SegmentTypes} @@ -97,6 +100,7 @@ export const mutations = ` ${EmailTemplateMutations} ${CustomerMutations} ${SegmentMutations} + ${FieldMutations} } `; diff --git a/src/db/factories.js b/src/db/factories.js index 1c6f40b0f..14a13c611 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -7,6 +7,8 @@ import { EmailTemplates, ResponseTemplates, Customers, + Forms, + Fields, Segments, Companies, } from './models'; @@ -100,3 +102,25 @@ export const customerFactory = (params = {}) => { return customer.save(); }; + +export const fieldFactory = (params = {}) => { + const field = new Fields({ + type: params.type || 'input', + validation: params.validation || 'number', + text: params.text || faker.random.word(), + description: params.description || faker.random.word(), + isRequired: params.isRequired || false, + order: params.order || 0, + }); + + return field.save(); +}; + +export const formFactory = ({ title, code, createdUserId }) => { + return Forms.create({ + title: title || faker.random.word(), + description: faker.random.word(), + code: code || Random.id(), + createdUserId, + }); +}; diff --git a/src/db/models/Fields.js b/src/db/models/Fields.js new file mode 100644 index 000000000..8c8ec966c --- /dev/null +++ b/src/db/models/Fields.js @@ -0,0 +1,109 @@ +/* + * Extra fields for form, customer, company + */ + +import mongoose from 'mongoose'; +import Random from 'meteor-random'; +import { FIELD_CONTENT_TYPES } from '../../constants'; +import { Forms } from './'; + +const FieldSchema = mongoose.Schema({ + _id: { + type: String, + unique: true, + default: () => Random.id(), + }, + + // form, customer, company + contentType: String, + + // formId when contentType is form + contentTypeId: String, + + type: String, + validation: String, + text: String, + description: String, + options: [String], + isRequired: Boolean, + order: Number, +}); + +class Field { + /* Create new field + * + * @param {String} contentType form, customer, company + * @param {String} contentTypeId when contentType is form, it will be + * formId + * + * @return {Promise} newly created field object + */ + static async createField({ contentType, contentTypeId, ...fields }) { + const query = { contentType }; + + if (contentTypeId) { + query.contentTypeId = contentTypeId; + } + + // form checks + if (contentType === FIELD_CONTENT_TYPES.FORM) { + if (!contentTypeId) { + throw new Error('Content type id is required'); + } + + const form = await Forms.findOne({ _id: contentTypeId }); + + if (!form) { + throw new Error(`Form not found with _id of ${contentTypeId}`); + } + } + + // Generate order + // if there is no field then start with 0 + let order = 0; + + const lastField = await Fields.findOne(query).sort({ order: -1 }); + + if (lastField) { + order = lastField.order + 1; + } + + return this.create({ + contentType, + contentTypeId, + order, + ...fields, + }); + } + + /* + * Update field + * @param {String} _id field id to update + * @param {Object} doc field values to update + * @return {Promise} updated field object + */ + static async updateField(_id, doc) { + await this.update({ _id }, { $set: doc }); + + return this.findOne({ _id }); + } + + /* + * Remove field + * @param {String} _id field id to remove + * @return {Promise} + */ + static async removeField(_id) { + const fieldObj = await this.findOne({ _id }); + + if (!fieldObj) throw new Error(`Field not found with id ${_id}`); + + return fieldObj.remove(); + } +} + +FieldSchema.loadClass(Field); + +const Fields = mongoose.model('fields', FieldSchema); + +export default Fields; diff --git a/src/db/models/index.js b/src/db/models/index.js index 253d11beb..5f088575d 100644 --- a/src/db/models/index.js +++ b/src/db/models/index.js @@ -6,6 +6,7 @@ import Brands from './Brands'; import Integrations from './Integrations'; import EngageMessages from './Engages'; import Tags from './Tags'; +import Fields from './Fields'; import { Forms, FormFields } from './Forms'; import Customers from './Customers'; import Companies from './Companies'; @@ -28,6 +29,7 @@ export { FormFields, EngageMessages, Tags, + Fields, Segments, Customers, Companies, From d12e4970f82e49fd2e128b489814f4535c72ca61 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 9 Oct 2017 19:29:36 +0800 Subject: [PATCH 024/318] Add updateOrders mutation & test --- src/__tests__/fieldDb.test.js | 13 +++++++++++++ src/data/resolvers/mutations/fields.js | 11 +++++++++++ src/data/resolvers/queries/fields.js | 18 ++++++++++++++++++ src/data/resolvers/queries/index.js | 2 ++ src/data/schema/field.js | 10 ++++++++++ src/data/schema/index.js | 3 ++- src/db/models/Fields.js | 24 ++++++++++++++++++++++++ 7 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/data/resolvers/queries/fields.js diff --git a/src/__tests__/fieldDb.test.js b/src/__tests__/fieldDb.test.js index ab63ca2c3..f2695579d 100644 --- a/src/__tests__/fieldDb.test.js +++ b/src/__tests__/fieldDb.test.js @@ -74,4 +74,17 @@ describe('Fields', () => { expect(e.message).toEqual('Form not found with _id of DFAFDFADS'); } }); + + test('updateOrder()', async () => { + const field1 = await fieldFactory(); + const field2 = await fieldFactory(); + + const [updatedField1, updatedField2] = await Fields.updateOrder([ + { _id: field1._id, order: 10 }, + { _id: field2._id, order: 11 }, + ]); + + expect(updatedField1.order).toBe(10); + expect(updatedField2.order).toBe(11); + }); }); diff --git a/src/data/resolvers/mutations/fields.js b/src/data/resolvers/mutations/fields.js index 309347a9d..3c5726f13 100644 --- a/src/data/resolvers/mutations/fields.js +++ b/src/data/resolvers/mutations/fields.js @@ -30,4 +30,15 @@ export default { return Fields.removeField(_id); }, + + /** + * Update field orders + * @param [OrderItem] [{ _id: [field id], order: [order value] }] + * @return {Promise} updated fields + */ + fieldsUpdateOrder(root, { orders }, { user }) { + if (!user) throw new Error('Login required'); + + return Fields.updateOrder(orders); + }, }; diff --git a/src/data/resolvers/queries/fields.js b/src/data/resolvers/queries/fields.js new file mode 100644 index 000000000..c4805f6f8 --- /dev/null +++ b/src/data/resolvers/queries/fields.js @@ -0,0 +1,18 @@ +import { Fields } from '../../../db/models'; + +export default { + /** + * Fields list + * @param {Object} args + * @return {Promise} sorted fields list + */ + fields(root, { contentType, contentTypeId }) { + const query = { contentType }; + + if (contentTypeId) { + query.contentTypeId = contentTypeId; + } + + return Fields.find(query).sort({ order: 1 }); + }, +}; diff --git a/src/data/resolvers/queries/index.js b/src/data/resolvers/queries/index.js index 5c1c35bb2..ab10c3251 100644 --- a/src/data/resolvers/queries/index.js +++ b/src/data/resolvers/queries/index.js @@ -2,6 +2,7 @@ import users from './users'; import channels from './channels'; import brands from './brands'; import integrations from './integrations'; +import fields from './fields'; import forms from './forms'; import responseTemplates from './responseTemplates'; import emailTemplates from './emailTemplates'; @@ -18,6 +19,7 @@ export default { ...channels, ...brands, ...integrations, + ...fields, ...forms, ...responseTemplates, ...emailTemplates, diff --git a/src/data/schema/field.js b/src/data/schema/field.js index f66b45a59..3d6355975 100644 --- a/src/data/schema/field.js +++ b/src/data/schema/field.js @@ -11,6 +11,15 @@ export const types = ` isRequired: Boolean order: Int } + + input OrderItem { + _id: String! + order: Int! + } +`; + +export const queries = ` + fields(contentType: String!, contentTypeId: String): [Field] `; const commonFields = ` @@ -27,4 +36,5 @@ export const mutations = ` fieldsAdd(contentType: String!, contentTypeId: String, ${commonFields}): Field fieldsEdit(_id: String!, ${commonFields}): Field fieldsRemove(_id: String!): Field + fieldsUpdateOrder(orders: [OrderItem]): [Field] `; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index b3d23dfc4..59f3f6222 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -20,7 +20,7 @@ import { mutations as EmailTemplateMutations, } from './emailTemplate'; -import { types as FieldTypes, mutations as FieldMutations } from './field'; +import { types as FieldTypes, queries as FieldQueries, mutations as FieldMutations } from './field'; import { types as FormTypes, queries as FormQueries } from './form'; @@ -80,6 +80,7 @@ export const queries = ` ${IntegrationQueries} ${ResponseTemplateQueries} ${EmailTemplateQueries} + ${FieldQueries} ${FormQueries} ${EngageQueries} ${TagQueries} diff --git a/src/db/models/Fields.js b/src/db/models/Fields.js index 8c8ec966c..dc6adaeac 100644 --- a/src/db/models/Fields.js +++ b/src/db/models/Fields.js @@ -100,6 +100,30 @@ class Field { return fieldObj.remove(); } + + /* + * Update given fields orders + * + * @param [OrderItem] orders + * [{ + * _id: {String} field id + * order: {Number} order + * }] + * + * @return [Field] updated fields + */ + static async updateOrder(orders) { + const ids = []; + + for (let { _id, order } of orders) { + ids.push(_id); + + // update each fields order + await this.update({ _id }, { order }); + } + + return this.find({ _id: { $in: ids } }).sort({ order: 1 }); + } } FieldSchema.loadClass(Field); From d8985eca7c9f9258d8d46437e68c4010cae99e8d Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 10 Oct 2017 16:56:06 +0800 Subject: [PATCH 025/318] Add customFieldsData field --- src/data/schema/customer.js | 3 +++ src/db/models/Customers.js | 1 + 2 files changed, 4 insertions(+) diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index 0a058715a..ad72112ae 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -19,6 +19,8 @@ export const types = ` createdAt: Date tagIds: [String] internalNotes: JSON + + customFieldsData: JSON messengerData: JSON twitterData: JSON facebookData: JSON @@ -42,6 +44,7 @@ const fields = ` name: String email: String phone: String + customFieldsData: JSON `; export const mutations = ` diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index 049ba5d67..5a2c4d802 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -19,6 +19,7 @@ const CustomerSchema = mongoose.Schema({ internalNotes: Object, tagIds: [String], + customFieldsData: Object, messengerData: Object, twitterData: Object, facebookData: Object, From 8252a2f7eaf3c5ecd3cb5221e0b2e6e08c853de8 Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 10 Oct 2017 18:22:36 +0800 Subject: [PATCH 026/318] Implement segmentsGetFields query --- src/data/resolvers/queries/segments.js | 66 ++++++++++++++- src/data/schema/segment.js | 8 +- src/db/models/Customers.js | 112 ++++++++++++++++++++++--- src/db/models/Fields.js | 4 + 4 files changed, 177 insertions(+), 13 deletions(-) diff --git a/src/data/resolvers/queries/segments.js b/src/data/resolvers/queries/segments.js index 21934382d..be568ec63 100644 --- a/src/data/resolvers/queries/segments.js +++ b/src/data/resolvers/queries/segments.js @@ -1,4 +1,4 @@ -import { Segments } from '../../../db/models'; +import { Segments, Customers, Fields } from '../../../db/models'; export default { /** @@ -13,7 +13,7 @@ export default { * Only segment that has no sub segments * @return {Promise} segment objects */ - headSegments() { + segmentsGetHeads() { return Segments.find({ subOf: { $exists: false } }); }, @@ -26,4 +26,66 @@ export default { segmentDetail(root, { _id }) { return Segments.findOne({ _id }); }, + + /** + * Generates field choices base on given kind. For example if kind is customer + * then it will generate customer related fields + * + * @param {String} kind customer or company + * + * @return {[SegmentField]} + * [{ name: 'messengerData.isActive', text: 'Messenger: is Active' }] + */ + async segmentsGetFields() { + /* + * Generates fields using given schema + * @param {Schema} schema Customers.schema etc ... + * @param {namePrefix} sub field's prefix like messengerData. or empty str + * @return {Array} array of fields + */ + const generateFieldsFromSchema = (schema, namePrefix) => { + const fields = []; + + // field definations + const paths = schema.paths; + + schema.eachPath(name => { + const label = paths[name].options.label; + + // add to fields list + if (label) { + fields.push({ + name: `${namePrefix}${name}`, + label, + }); + } + }); + + return fields; + }; + + // generate list using customer schema + let fields = generateFieldsFromSchema(Customers.schema, ''); + + Customers.schema.eachPath(name => { + const path = Customers.schema.paths[name]; + + // extend fields list using sub schema fields + if (path.schema) { + fields = [...fields, ...generateFieldsFromSchema(path.schema, `${name}.`)]; + } + }); + + const customFields = await Fields.getCustomerFields(); + + // extend fields list using custom fields + customFields.forEach(customField => { + fields.push({ + name: `customFieldsData.${customField._id}`, + label: customField.text, + }); + }); + + return fields; + }, }; diff --git a/src/data/schema/segment.js b/src/data/schema/segment.js index 18da7eea9..7c36c0c20 100644 --- a/src/data/schema/segment.js +++ b/src/data/schema/segment.js @@ -19,12 +19,18 @@ export const types = ` getParentSegment: Segment getSubSegments: [Segment] } + + type SegmentField { + name: String! + label: String! + } `; export const queries = ` segments: [Segment] - headSegments: [Segment] segmentDetail(_id: String): Segment + segmentsGetHeads: [Segment] + segmentsGetFields(kind: String): [SegmentField] `; const commonFields = ` diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index 5a2c4d802..ff7d218af 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -1,6 +1,99 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +/* + * messenger schema + */ +const messengerSchema = mongoose.Schema( + { + lastSeenAt: { + type: Date, + label: 'Messenger: Last online', + }, + sessionCount: { + type: Number, + label: 'Messenger: Session count', + }, + isActive: { + type: Boolean, + label: 'Messenger: Is online', + }, + customData: { + type: Object, + blackbox: true, + optional: true, + }, + }, + { _id: false }, +); + +/* + * twitter schema + */ +const twitterSchema = mongoose.Schema( + { + id: { + type: Number, + label: 'Twitter: ID (Number)', + }, + idStr: { + type: String, + label: 'Twitter: ID (String)', + }, + name: { + type: String, + label: 'Twitter: Name', + }, + screenName: { + type: String, + label: 'Twitter: Screen name', + }, + profileImageUrl: { + type: String, + label: 'Twitter: Profile photo', + }, + }, + { _id: false }, +); + +/* + * facebook schema + */ +const facebookSchema = mongoose.Schema( + { + id: { + type: String, + label: 'Facebook: ID', + }, + profilePic: { + type: String, + optional: true, + label: 'Facebook: Profile photo', + }, + }, + { _id: false }, +); + +/* + * internal note schema + */ +const internalNoteSchema = mongoose.Schema({ + _id: { + type: String, + unique: true, + default: () => Random.id(), + }, + content: { + type: String, + }, + createdBy: { + type: String, + }, + createdDate: { + type: Date, + }, +}); + const CustomerSchema = mongoose.Schema({ _id: { type: String, @@ -8,21 +101,20 @@ const CustomerSchema = mongoose.Schema({ default: () => Random.id(), }, - name: String, - email: String, - phone: String, - isUser: Boolean, + name: { type: String, label: 'Name' }, + email: { type: String, label: 'Email' }, + phone: { type: String, label: 'Phone' }, + isUser: { type: Boolean, label: 'Is user' }, + createdAt: { type: Date, label: 'Created at' }, integrationId: String, - createdAt: Date, - - internalNotes: Object, tagIds: [String], customFieldsData: Object, - messengerData: Object, - twitterData: Object, - facebookData: Object, + internalNotes: [internalNoteSchema], + messengerData: messengerSchema, + twitterData: twitterSchema, + facebookData: facebookSchema, }); class Customer { diff --git a/src/db/models/Fields.js b/src/db/models/Fields.js index dc6adaeac..09b50b8d2 100644 --- a/src/db/models/Fields.js +++ b/src/db/models/Fields.js @@ -124,6 +124,10 @@ class Field { return this.find({ _id: { $in: ids } }).sort({ order: 1 }); } + + static getCustomerFields() { + return this.find({ contentType: FIELD_CONTENT_TYPES.CUSTOMER }); + } } FieldSchema.loadClass(Field); From e02b47ddf1fc285ec7ed0d8acc2206140d3d6d28 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Tue, 10 Oct 2017 22:40:23 +0800 Subject: [PATCH 027/318] Engage messages mutation --- src/__tests__/engage_messages.test.js | 192 ++++++++++++++++++++++++ src/data/resolvers/mutations/engages.js | 41 +++++ src/data/resolvers/mutations/index.js | 2 + src/data/schema/engage.js | 11 ++ src/data/schema/index.js | 7 +- src/db/factories.js | 10 +- src/db/models/Engages.js | 25 +++ 7 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/engage_messages.test.js create mode 100644 src/data/resolvers/mutations/engages.js diff --git a/src/__tests__/engage_messages.test.js b/src/__tests__/engage_messages.test.js new file mode 100644 index 000000000..caf555bf6 --- /dev/null +++ b/src/__tests__/engage_messages.test.js @@ -0,0 +1,192 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { userFactory, segmentsFactory } from '../db/factories'; +import { EngageMessages, Users, Segments } from '../db/models'; +import mutations from '../data/resolvers/mutations'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('engage messages models', () => { + let _user; + let _segment = segmentsFactory(); + + /** + * Before each test create test data + * containing 2 users and an integration + */ + beforeEach(async () => { + _user = await userFactory({}); + _segment = await segmentsFactory({}); + }); + + /** + * After each test remove the test data + */ + afterEach(async () => { + await Users.remove({}); + await Segments.remove({}); + await EngageMessages.remove({}); + }); + + test('create messages', async () => { + const doc = { + kind: 'manual', + title: 'Message test', + fromUserId: _user._id, + segmentId: _segment._id, + isLive: true, + isDraft: false, + }; + + const message = await EngageMessages.createMessage(doc); + expect(message.kind).toEqual(doc.kind); + expect(message.title).toEqual(doc.title); + expect(message.fromUserId).toEqual(_user._id); + expect(message.segmentId).toEqual(_segment._id); + expect(message.isLive).toEqual(doc.isLive); + expect(message.isDraft).toEqual(doc.isDraft); + }); + + test('update messages', async () => { + const doc = { + kind: 'manual', + title: 'Message test', + fromUserId: _user._id, + segmentId: _segment._id, + isLive: true, + isDraft: false, + }; + + let message = await EngageMessages.createMessage(doc); + + doc.title = 'Message test updated'; + doc.isLive = false; + doc.isDraft = true; + + await EngageMessages.updateMessage(message._id, doc); + message = await EngageMessages.findOne({ _id: message._id }); + + expect(message.kind).toEqual(doc.kind); + expect(message.title).toEqual(doc.title); + expect(message.fromUserId).toEqual(_user._id); + expect(message.segmentId).toEqual(_segment._id); + expect(message.isLive).toEqual(doc.isLive); + expect(message.isDraft).toEqual(doc.isDraft); + }); + + test('remove a message', async () => { + const _message = await EngageMessages.createMessage({ + kind: 'manual', + title: 'Message test', + fromUserId: _user._id, + }); + + await EngageMessages.removeMessage(_message._id); + const messagesCounts = await EngageMessages.find({}).count(); + expect(messagesCounts).toBe(0); + }); +}); + +describe('mutations', () => { + let _user; + let _segment = segmentsFactory(); + let _doc = null; + + /** + * Before each test create test data + * containing 2 users and an integration + */ + beforeEach(async () => { + _user = await userFactory({}); + _segment = await segmentsFactory({}); + _doc = { + kind: 'manual', + method: 'email', + title: 'Message test', + fromUserId: _user._id, + segmentId: _segment._id, + }; + }); + + /** + * After each test remove the test data + */ + afterEach(async () => { + _doc = null; + await Users.remove({}); + await Segments.remove({}); + await EngageMessages.remove({}); + }); + + test('messages add', async () => { + const _message = await mutations.messagesAdd(null, _doc); + expect(_message.kind).toEqual(_doc.kind); + expect(_message.title).toEqual(_doc.title); + expect(_message.fromUserId).toEqual(_user._id); + expect(_message.segmentId).toEqual(_segment._id); + expect(_message.isLive).toEqual(_doc.isLive); + expect(_message.isDraft).toEqual(_doc.isDraft); + }); + + test('messages edit', async () => { + let message = await EngageMessages.createMessage(_doc); + + _doc.title = 'Message test updated'; + _doc.isLive = false; + _doc.isDraft = true; + + await mutations.messageEdit(null, { _id: message._id, ..._doc }); + message = await EngageMessages.findOne({ _id: message._id }); + + expect(message.kind).toEqual(_doc.kind); + expect(message.title).toEqual(_doc.title); + expect(message.fromUserId).toEqual(_user._id); + expect(message.segmentId).toEqual(_segment._id); + expect(message.isLive).toEqual(_doc.isLive); + expect(message.isDraft).toEqual(_doc.isDraft); + }); + + test('messages remove', async () => { + const _message = await EngageMessages.createMessage(_doc); + + const removeResult = await mutations.messagesRemove(null, _message._id); + expect(removeResult).toBe(true); + + const messagesCounts = await EngageMessages.find({}).count(); + expect(messagesCounts).toBe(0); + }); + + test('set live', async () => { + _doc.isLive = false; + _doc.isDraft = true; + + let _message = await EngageMessages.createMessage(_doc); + + _message = await mutations.messagesSetLive(null, _message._id); + expect(_message.isLive).toEqual(true); + expect(_message.isDraft).toEqual(false); + }); + + test('set pause', async () => { + _doc.isLive = true; + + let _message = await EngageMessages.createMessage(_doc); + + _message = await mutations.messagesSetPause(null, _message._id); + expect(_message.isLive).toEqual(false); + }); + + test('set live manual', async () => { + _doc.isLive = false; + _doc.isDraft = true; + + let _message = await EngageMessages.createMessage(_doc); + + _message = await mutations.messagesSetLiveManual(null, _message._id); + expect(_message.isLive).toEqual(true); + expect(_message.isDraft).toEqual(false); + }); +}); diff --git a/src/data/resolvers/mutations/engages.js b/src/data/resolvers/mutations/engages.js new file mode 100644 index 000000000..30f1bd1fe --- /dev/null +++ b/src/data/resolvers/mutations/engages.js @@ -0,0 +1,41 @@ +import { EngageMessages } from '../../../db/models'; + +export default { + /** + * Create new message + * @return {Promise} message object + */ + async messagesAdd(root, doc) { + return await EngageMessages.createMessage(doc); + }, + + async messageEdit(root, { _id, ...doc }) { + await EngageMessages.updateMessage(_id, doc); + + return await EngageMessages.findOne({ _id }); + }, + + async messagesRemove(root, _id) { + await EngageMessages.removeMessage(_id); + + return true; + }, + + async messagesSetLive(root, _id) { + await EngageMessages.updateMessage(_id, { isLive: true, isDraft: false }); + + return await EngageMessages.findOne({ _id }); + }, + + async messagesSetPause(root, _id) { + await EngageMessages.updateMessage(_id, { isLive: false }); + + return await EngageMessages.findOne({ _id }); + }, + + async messagesSetLiveManual(root, _id) { + await EngageMessages.updateMessage(_id, { isLive: true, isDraft: false }); + + return await EngageMessages.findOne({ _id }); + }, +}; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index 476fbcbf5..34c7cc99b 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -1,7 +1,9 @@ import conversation from './conversation'; import tags from './tags'; +import engages from './engages'; export default { ...conversation, + ...engages, ...tags, }; diff --git a/src/data/schema/engage.js b/src/data/schema/engage.js index 2c8c1d0ec..5c0a93da2 100644 --- a/src/data/schema/engage.js +++ b/src/data/schema/engage.js @@ -29,3 +29,14 @@ export const queries = ` engageMessageCounts(name: String!, kind: String, status: String): JSON engageMessagesTotalCount: Int `; + +export const mutations = ` + messagesAdd(title: String!, kind: String!, + segmentId: String!, method: String!, fromUserId: String!): EngageMessage + messageEdit(_id: String!, title: String!, kind: String!, + segmentId: String!, method: String!, fromUserId: String!): EngageMessage + messagesRemove(ids: [String!]!): Boolean + messagesSetLive(_id: String!): EngageMessage + messagesSetPause(_id: String!): EngageMessage + messagesSetLiveManual(_id: String!): EngageMessage +`; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index 2bfe6aacc..9a53a3406 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -12,7 +12,11 @@ import { types as EmailTemplate, queries as EmailTemplateQueries } from './email import { types as FormTypes, queries as FormQueries } from './form'; -import { types as EngageTypes, queries as EngageQueries } from './engage'; +import { + types as EngageTypes, + queries as EngageQueries, + mutations as EngageMutations, +} from './engage'; import { types as TagTypes, queries as TagQueries, mutations as TagMutations } from './tag'; @@ -68,6 +72,7 @@ export const queries = ` export const mutations = ` type Mutation { ${ConversationMutations} + ${EngageMutations} ${TagMutations} } `; diff --git a/src/db/factories.js b/src/db/factories.js index 94641f815..d972bbe17 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -1,6 +1,6 @@ import shortid from 'shortid'; import faker from 'faker'; -import { Users, Tags } from './models'; +import { Users, Tags, Segments } from './models'; export const userFactory = (params = {}) => { const user = new Users({ @@ -23,3 +23,11 @@ export const tagsFactory = (params = {}) => { return tag.save(); }; + +export const segmentsFactory = (params = {}) => { + const segment = new Segments({ + name: faker.random.word(), + }); + + return segment.save(); +}; diff --git a/src/db/models/Engages.js b/src/db/models/Engages.js index 0b5cd4047..00987626a 100644 --- a/src/db/models/Engages.js +++ b/src/db/models/Engages.js @@ -21,6 +21,31 @@ const EngageMessageSchema = mongoose.Schema({ deliveryReports: Object, }); +class Message { + /** + * Create engage message + * @param {Object} doc object + * @return {Promise} Newly created message object + */ + static createMessage(doc) { + return this.create({ + ...doc, + deliveryReports: {}, + createdUserId: doc.userId, + createdDate: new Date(), + }); + } + + static updateMessage(_id, doc) { + return this.update({ _id }, { $set: doc }); + } + + static removeMessage(_id) { + return this.remove({ _id }); + } +} + +EngageMessageSchema.loadClass(Message); const EngageMessages = mongoose.model('engage_messages', EngageMessageSchema); export default EngageMessages; From 4b0c255eb409267750e177b5d71dd0d22e5a377d Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Wed, 11 Oct 2017 03:31:25 +0800 Subject: [PATCH 028/318] #13 notification model tests --- src/__tests__/notificationMutations.test.js | 110 ++++++++++++++++++++ src/data/constants.js | 4 + src/db/factories.js | 17 ++- src/db/models/Configurations.js | 25 ++--- src/db/models/Notifications.js | 24 +++-- 5 files changed, 156 insertions(+), 24 deletions(-) create mode 100644 src/__tests__/notificationMutations.test.js diff --git a/src/__tests__/notificationMutations.test.js b/src/__tests__/notificationMutations.test.js new file mode 100644 index 000000000..a61c409d1 --- /dev/null +++ b/src/__tests__/notificationMutations.test.js @@ -0,0 +1,110 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Notifications, Configurations, Users } from '../db/models'; +// import mutations from '../data/resolvers/mutations'; +import { userFactory, configurationFactory } from '../db/factories'; +// import { sendNotification } from '../data/utils'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('Notification model tests', () => { + let _user; + let _user2; + + beforeEach(async () => { + _user = await userFactory({}); + _user2 = await userFactory({}); + }); + + afterEach(async () => { + Notifications.remove({}); + Configurations.remove({}); + Users.remove({}); + }); + + test('exception', async () => { + expect.assertions(1); + + await configurationFactory({ + user: _user2, + notifType: 'channelMembersChange', + isAllowed: false, + }); + + // Create notification + let doc = { + notifType: 'channelMembersChange', + createdUser: _user._id, + title: 'new Notification title', + content: 'new Notification content', + link: 'new Notification link', + receiver: _user2._id, + }; + + try { + await Notifications.createNotification(doc); + } catch (e) { + expect(e.message).toEqual('Configuration does not exist'); + } + }); + + test('create and update', async () => { + await configurationFactory({ + user: _user2, + notifType: 'channelMembersChange', + }); + + // Create notification + let doc = { + notifType: 'channelMembersChange', + createdUser: _user._id, + title: 'new Notification title', + content: 'new Notification content', + link: 'new Notification link', + receiver: _user2, + }; + + let notification = await Notifications.createNotification(doc); + + expect(notification.notifType).toEqual(doc.notifType); + expect(notification.createdUser).toEqual(doc.createdUser); + expect(notification.title).toEqual(doc.title); + expect(notification.content).toEqual(doc.content); + expect(notification.link).toEqual(doc.link); + expect(notification.receivers).toEqual(doc.receivers); + + // Update notification + let user3 = await userFactory({}); + + doc = { + notifType: 'channelMembersChange 2', + title: 'new Notification title 2', + content: 'new Notification content 2', + link: 'new Notification link 2', + receiver: user3, + }; + + await Notifications.updateNotification(notification._id, doc); + + notification = await Notifications.findOne({ _id: notification._id }); + + expect(notification.notifType).toEqual(doc.notifType); + expect(notification.title).toEqual(doc.title); + expect(notification.content).toEqual(doc.content); + expect(notification.link).toEqual(doc.link); + expect(notification.receivers).toEqual(doc.receivers); + + // Mark as read + await Notifications.markAsRead([notification._id]); + notification = await Notifications.findOne({ _id: notification._id }); + expect(notification.isRead).toEqual(true); + + // Remove notification + await Notifications.removeNotification(notification._id); + + expect(await Notifications.find({}).count()).toEqual(0); + }); +}); diff --git a/src/data/constants.js b/src/data/constants.js index d444f2298..85853948b 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -22,4 +22,8 @@ export const TAG_TYPES = { export const MODULE_LIST = [ // TODO: need to collect modules + 'channelMembersChange', + 'conversationAddMessage', + 'conversationAssigneeChange', + 'conversationStateChange', ]; diff --git a/src/db/factories.js b/src/db/factories.js index 621a02a05..0f7044cbf 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -10,6 +10,7 @@ import { Tags, Forms, FormFields, + Configurations, } from './models'; export const userFactory = (params = {}) => { @@ -94,7 +95,7 @@ export const formFactory = ({ title, code, createdUserId }) => { }; export const formFieldFactory = (formId, params) => { - return FormFields.createFormField(formId || shortid.id(), { + return FormFields.createFormField(formId || Random.id(), { type: params.type || faker.random.word(), name: faker.random.word(), validation: params.validation || faker.random.word(), @@ -104,3 +105,17 @@ export const formFieldFactory = (formId, params) => { number: faker.random.word(), }); }; + +export const configurationFactory = params => { + let { isAllowed } = params; + if (isAllowed == null) { + isAllowed = true; + } + + return Configurations.createOrUpdateConfiguration({ + user: params.user || userFactory({}), + notifType: params.notifType || faker.random.word(), + // which module's type it is. For example: indocuments + isAllowed: isAllowed, + }); +}; diff --git a/src/db/models/Configurations.js b/src/db/models/Configurations.js index 146b8fde8..be0b48434 100644 --- a/src/db/models/Configurations.js +++ b/src/db/models/Configurations.js @@ -4,29 +4,24 @@ const ConfigSchema = new mongoose.Schema({ // to whom this config is related user: String, notifType: String, - // which module's type it is. For example: indocuments isAllowed: Boolean, }); class Configuration { - static saveConfig({ notifType, isAllowed, userId }) { - if (!userId) { - throw new Error('createdUserId must be supplied'); - } - - const selector = { user: userId, notifType }; + static async createOrUpdateConfiguration({ notifType, isAllowed, user }) { + const selector = { user, notifType }; - const oldOne = this.findOne(selector); + const oldOne = await this.findOne(selector); - // if already inserted then update isAllowed field + // If already inserted then raise error if (oldOne) { - this.update({ _id: oldOne._id }, { $set: { isAllowed } }); - - // if it is first time then insert - } else { - selector.isAllowed = isAllowed; - this.insert(selector); + await this.update({ _id: oldOne._id }, { $set: { isAllowed } }); + return await this.findOne({ _id: oldOne._id }); } + + // If it is first time then insert + selector.isAllowed = isAllowed; + return await this.create(selector); } } diff --git a/src/db/models/Notifications.js b/src/db/models/Notifications.js index 6acc9c305..2aeb8d616 100644 --- a/src/db/models/Notifications.js +++ b/src/db/models/Notifications.js @@ -1,5 +1,5 @@ import mongoose from 'mongoose'; -import { Configurations } from './Notifications'; +import Configurations from './Configurations'; // schemas const NotificationSchema = new mongoose.Schema({ @@ -18,26 +18,34 @@ class Notification { return this.update({ _id: { $in: ids } }, { $set: { isRead: true } }, { multi: true }); } - static createNotification({ userId, ...doc }) { - if (!userId) { - throw new Error('createdUserId must be supplied'); + static async createNotification({ createdUser, ...doc }) { + if (!createdUser) { + throw new Error('createdUser must be supplied'); } // Setting auto values - doc.userId = userId; + doc.createdUser = createdUser; // if receiver is configured to get this notification - const config = Configurations.findOne({ + const config = await Configurations.findOne({ user: doc.receiver, notifType: doc.notifType, }); // receiver disabled this notification if (config && !config.isAllowed) { - throw new Error('error'); + throw new Error('Configuration does not exist'); } - return this.insert(doc); + return await this.create(doc); + } + + static updateNotification(_id, doc) { + return this.update({ _id }, doc); + } + + static removeNotification(_id) { + return this.remove({ _id }); } } From dfada020afce7d0e22cf125dfc3a64f016aa8f46 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Wed, 11 Oct 2017 03:35:44 +0800 Subject: [PATCH 029/318] #13 a bit of refactoring --- src/__tests__/notificationMutations.test.js | 4 +-- src/data/resolvers/mutations/configuration.js | 4 +-- src/db/factories.js | 4 +-- src/db/models/Configurations.js | 29 ---------------- src/db/models/Notifications.js | 33 +++++++++++++++++-- src/db/models/index.js | 5 ++- 6 files changed, 38 insertions(+), 41 deletions(-) delete mode 100644 src/db/models/Configurations.js diff --git a/src/__tests__/notificationMutations.test.js b/src/__tests__/notificationMutations.test.js index a61c409d1..12a5121e6 100644 --- a/src/__tests__/notificationMutations.test.js +++ b/src/__tests__/notificationMutations.test.js @@ -2,7 +2,7 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { Notifications, Configurations, Users } from '../db/models'; +import { Notifications, NotificationConfigurations, Users } from '../db/models'; // import mutations from '../data/resolvers/mutations'; import { userFactory, configurationFactory } from '../db/factories'; // import { sendNotification } from '../data/utils'; @@ -21,7 +21,7 @@ describe('Notification model tests', () => { afterEach(async () => { Notifications.remove({}); - Configurations.remove({}); + NotificationConfigurations.remove({}); Users.remove({}); }); diff --git a/src/data/resolvers/mutations/configuration.js b/src/data/resolvers/mutations/configuration.js index 4adb8e202..95d2cabbe 100644 --- a/src/data/resolvers/mutations/configuration.js +++ b/src/data/resolvers/mutations/configuration.js @@ -1,4 +1,4 @@ -import { Configurations } from '../../../db/models'; +import { NotificationConfigurations } from '../../../db/models'; import { MODULE_LIST } from '../../constants'; export default { @@ -7,6 +7,6 @@ export default { }, configurationsSaveConfig(doc) { - return Configurations.saveConfig(doc); + return NotificationConfigurations.saveConfig(doc); }, }; diff --git a/src/db/factories.js b/src/db/factories.js index 0f7044cbf..e5178230b 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -10,7 +10,7 @@ import { Tags, Forms, FormFields, - Configurations, + NotificationConfigurations, } from './models'; export const userFactory = (params = {}) => { @@ -112,7 +112,7 @@ export const configurationFactory = params => { isAllowed = true; } - return Configurations.createOrUpdateConfiguration({ + return NotificationConfigurations.createOrUpdateConfiguration({ user: params.user || userFactory({}), notifType: params.notifType || faker.random.word(), // which module's type it is. For example: indocuments diff --git a/src/db/models/Configurations.js b/src/db/models/Configurations.js deleted file mode 100644 index be0b48434..000000000 --- a/src/db/models/Configurations.js +++ /dev/null @@ -1,29 +0,0 @@ -import mongoose from 'mongoose'; - -const ConfigSchema = new mongoose.Schema({ - // to whom this config is related - user: String, - notifType: String, - isAllowed: Boolean, -}); - -class Configuration { - static async createOrUpdateConfiguration({ notifType, isAllowed, user }) { - const selector = { user, notifType }; - - const oldOne = await this.findOne(selector); - - // If already inserted then raise error - if (oldOne) { - await this.update({ _id: oldOne._id }, { $set: { isAllowed } }); - return await this.findOne({ _id: oldOne._id }); - } - - // If it is first time then insert - selector.isAllowed = isAllowed; - return await this.create(selector); - } -} - -ConfigSchema.loadClass(Configuration); -export default mongoose.model('notification_configs', ConfigSchema); diff --git a/src/db/models/Notifications.js b/src/db/models/Notifications.js index 2aeb8d616..af7bd6465 100644 --- a/src/db/models/Notifications.js +++ b/src/db/models/Notifications.js @@ -1,5 +1,4 @@ import mongoose from 'mongoose'; -import Configurations from './Configurations'; // schemas const NotificationSchema = new mongoose.Schema({ @@ -27,7 +26,7 @@ class Notification { doc.createdUser = createdUser; // if receiver is configured to get this notification - const config = await Configurations.findOne({ + const config = await NotificationConfigurations.findOne({ user: doc.receiver, notifType: doc.notifType, }); @@ -50,4 +49,32 @@ class Notification { } NotificationSchema.loadClass(Notification); -export default mongoose.model('notifications', NotificationSchema); +export const Notifications = mongoose.model('notifications', NotificationSchema); + +const ConfigSchema = new mongoose.Schema({ + // to whom this config is related + user: String, + notifType: String, + isAllowed: Boolean, +}); + +class Configuration { + static async createOrUpdateConfiguration({ notifType, isAllowed, user }) { + const selector = { user, notifType }; + + const oldOne = await this.findOne(selector); + + // If already inserted then raise error + if (oldOne) { + await this.update({ _id: oldOne._id }, { $set: { isAllowed } }); + return await this.findOne({ _id: oldOne._id }); + } + + // If it is first time then insert + selector.isAllowed = isAllowed; + return await this.create(selector); + } +} + +ConfigSchema.loadClass(Configuration); +export const NotificationConfigurations = mongoose.model('notification_configs', ConfigSchema); diff --git a/src/db/models/index.js b/src/db/models/index.js index 7b3490db1..39c93fbbc 100644 --- a/src/db/models/index.js +++ b/src/db/models/index.js @@ -14,8 +14,7 @@ import { KnowledgeBaseCategories, KnowledgeBaseTopics, } from './KnowledgeBase'; -import Notifications from './Notifications'; -import Configurations from './Configurations'; +import { Notifications, NotificationConfigurations } from './Notifications'; export { Users, @@ -36,5 +35,5 @@ export { KnowledgeBaseCategories, KnowledgeBaseTopics, Notifications, - Configurations, + NotificationConfigurations, }; From 109187a0056146ee9de3a82c4345124cbb37bceb Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Wed, 11 Oct 2017 04:06:31 +0800 Subject: [PATCH 030/318] #13 notification configuration tests --- src/__tests__/notificationMutations.test.js | 39 +++++++++++++++++++-- src/data/constants.js | 23 +++++++++++- src/db/constants.js | 21 ----------- src/db/models/Integrations.js | 2 +- src/db/models/Notifications.js | 4 +++ 5 files changed, 64 insertions(+), 25 deletions(-) delete mode 100644 src/db/constants.js diff --git a/src/__tests__/notificationMutations.test.js b/src/__tests__/notificationMutations.test.js index 12a5121e6..ca080e3f5 100644 --- a/src/__tests__/notificationMutations.test.js +++ b/src/__tests__/notificationMutations.test.js @@ -29,7 +29,7 @@ describe('Notification model tests', () => { expect.assertions(1); await configurationFactory({ - user: _user2, + user: _user2._id, notifType: 'channelMembersChange', isAllowed: false, }); @@ -53,7 +53,7 @@ describe('Notification model tests', () => { test('create and update', async () => { await configurationFactory({ - user: _user2, + user: _user2._id, notifType: 'channelMembersChange', }); @@ -108,3 +108,38 @@ describe('Notification model tests', () => { expect(await Notifications.find({}).count()).toEqual(0); }); }); + +describe('NotificationConfiguration model tests', async () => { + test('model tests', async () => { + // New notification configuration + + const user = await userFactory({}); + const doc = { + notifType: 'conversationAddMessage', + isAllowed: true, + user: user._id, + }; + + let notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration( + doc, + ); + expect(notificationConfigurations.notifType).toEqual(doc.notifType); + expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); + expect(notificationConfigurations.user).toEqual(doc.user); + + // Another notification configuration + doc.notifType = 'conversationAssigneeChange'; + + notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration(doc); + expect(notificationConfigurations.notifType).toEqual(doc.notifType); + expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); + expect(notificationConfigurations.user).toEqual(doc.user); + + // Change notification + doc.isAllowed = false; + notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration(doc); + expect(notificationConfigurations.notifType).toEqual(doc.notifType); + expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); + expect(notificationConfigurations.user).toEqual(doc.user); + }); +}); diff --git a/src/data/constants.js b/src/data/constants.js index 85853948b..c504d8e00 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -20,8 +20,29 @@ export const TAG_TYPES = { ALL_LIST: ['conversation', 'customer', 'engageMessage'], }; +export const FORM_LOAD_TYPES = { + SHOUTBOX: 'shoutbox', + POPUP: 'popup', + EMBEDDED: 'embedded', + ALL_LIST: ['', 'shoutbox', 'popup', 'embedded'], +}; + +export const FORM_SUCCESS_ACTIONS = { + EMAIL: 'email', + REDIRECT: 'redirect', + ONPAGE: 'onPage', + ALL_LIST: ['', 'email', 'redirect', 'onPage'], +}; + +export const KIND_CHOICES = { + MESSENGER: 'messenger', + FORM: 'form', + TWITTER: 'twitter', + FACEBOOK: 'facebook', + ALL_LIST: ['messenger', 'form', 'twitter', 'facebook'], +}; + export const MODULE_LIST = [ - // TODO: need to collect modules 'channelMembersChange', 'conversationAddMessage', 'conversationAssigneeChange', diff --git a/src/db/constants.js b/src/db/constants.js deleted file mode 100644 index 392b8cbe9..000000000 --- a/src/db/constants.js +++ /dev/null @@ -1,21 +0,0 @@ -export const FORM_LOAD_TYPES = { - SHOUTBOX: 'shoutbox', - POPUP: 'popup', - EMBEDDED: 'embedded', - ALL_LIST: ['', 'shoutbox', 'popup', 'embedded'], -}; - -export const FORM_SUCCESS_ACTIONS = { - EMAIL: 'email', - REDIRECT: 'redirect', - ONPAGE: 'onPage', - ALL_LIST: ['', 'email', 'redirect', 'onPage'], -}; - -export const KIND_CHOICES = { - MESSENGER: 'messenger', - FORM: 'form', - TWITTER: 'twitter', - FACEBOOK: 'facebook', - ALL_LIST: ['messenger', 'form', 'twitter', 'facebook'], -}; diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index d6d735a5c..e17943b00 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -2,7 +2,7 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; import { Messages, Conversations } from './Conversations'; import { Customers } from './Customers'; -import { KIND_CHOICES, FORM_SUCCESS_ACTIONS, FORM_LOAD_TYPES } from '../constants'; +import { KIND_CHOICES, FORM_SUCCESS_ACTIONS, FORM_LOAD_TYPES } from '../../data/constants'; const MessengerOnlineHoursSchema = mongoose.Schema({ _id: { diff --git a/src/db/models/Notifications.js b/src/db/models/Notifications.js index af7bd6465..c234cbd0e 100644 --- a/src/db/models/Notifications.js +++ b/src/db/models/Notifications.js @@ -60,6 +60,10 @@ const ConfigSchema = new mongoose.Schema({ class Configuration { static async createOrUpdateConfiguration({ notifType, isAllowed, user }) { + if (!user) { + throw new Error('user must be supplied'); + } + const selector = { user, notifType }; const oldOne = await this.findOne(selector); From 818524da68d46e6aaed0e85fa4e91a2ecbd9b687 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Wed, 11 Oct 2017 07:55:17 +0800 Subject: [PATCH 031/318] notification and util.sendNotification tests --- src/__tests__/notificationMutations.test.js | 76 +++++++++++++++++-- src/__tests__/notificationQueries.test.js | 15 ++++ src/data/resolvers/mutations/configuration.js | 12 --- src/data/resolvers/mutations/index.js | 2 - src/data/resolvers/mutations/notification.js | 15 +++- src/data/resolvers/queries/index.js | 2 + src/data/resolvers/queries/notifications.js | 12 +++ src/data/utils.js | 61 +++++++++++++++ src/db/models/Notifications.js | 12 ++- 9 files changed, 181 insertions(+), 26 deletions(-) create mode 100644 src/__tests__/notificationQueries.test.js delete mode 100644 src/data/resolvers/mutations/configuration.js create mode 100644 src/data/resolvers/queries/notifications.js diff --git a/src/__tests__/notificationMutations.test.js b/src/__tests__/notificationMutations.test.js index ca080e3f5..84ef65db6 100644 --- a/src/__tests__/notificationMutations.test.js +++ b/src/__tests__/notificationMutations.test.js @@ -3,14 +3,15 @@ import { connect, disconnect } from '../db/connection'; import { Notifications, NotificationConfigurations, Users } from '../db/models'; -// import mutations from '../data/resolvers/mutations'; +import mutations from '../data/resolvers/mutations'; import { userFactory, configurationFactory } from '../db/factories'; -// import { sendNotification } from '../data/utils'; +import { MODULE_LIST } from '../data/constants'; +import { sendChannelNotifications, sendNotification } from '../data/utils'; beforeAll(() => connect()); afterAll(() => disconnect()); -describe('Notification model tests', () => { +describe('Notification tests', () => { let _user; let _user2; @@ -25,7 +26,7 @@ describe('Notification model tests', () => { Users.remove({}); }); - test('exception', async () => { + test('model exception', async () => { expect.assertions(1); await configurationFactory({ @@ -51,7 +52,7 @@ describe('Notification model tests', () => { } }); - test('create and update', async () => { + test('model create, update, remove', async () => { await configurationFactory({ user: _user2._id, notifType: 'channelMembersChange', @@ -64,7 +65,7 @@ describe('Notification model tests', () => { title: 'new Notification title', content: 'new Notification content', link: 'new Notification link', - receiver: _user2, + receiver: _user2._id, }; let notification = await Notifications.createNotification(doc); @@ -74,7 +75,7 @@ describe('Notification model tests', () => { expect(notification.title).toEqual(doc.title); expect(notification.content).toEqual(doc.content); expect(notification.link).toEqual(doc.link); - expect(notification.receivers).toEqual(doc.receivers); + expect(notification.receiver).toEqual(doc.receiver); // Update notification let user3 = await userFactory({}); @@ -107,12 +108,13 @@ describe('Notification model tests', () => { expect(await Notifications.find({}).count()).toEqual(0); }); + + test('sending notifications', () => {}); }); describe('NotificationConfiguration model tests', async () => { test('model tests', async () => { // New notification configuration - const user = await userFactory({}); const doc = { notifType: 'conversationAddMessage', @@ -143,3 +145,61 @@ describe('NotificationConfiguration model tests', async () => { expect(notificationConfigurations.user).toEqual(doc.user); }); }); + +describe('test mutations', () => { + beforeEach(() => {}); + + afterEach(async () => { + await Notifications.remove({}); + await NotificationConfigurations.remove({}); + }); + + test('mutations', async () => { + // notification confuration test + const user = await userFactory({}); + const doc = { + notifType: 'conversationAddMessage', + isAllowed: true, + user: user._id, + }; + + let notificationConfigurations = await mutations.notificationsSaveConfig(null, doc); + expect(notificationConfigurations.notifType).toEqual(doc.notifType); + expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); + expect(notificationConfigurations.user).toEqual(doc.user); + + // Another notification configuration + doc.notifType = 'conversationAssigneeChange'; + + notificationConfigurations = await mutations.notificationsSaveConfig(null, doc); + expect(notificationConfigurations.notifType).toEqual(doc.notifType); + expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); + expect(notificationConfigurations.user).toEqual(doc.user); + + // Change notification + doc.isAllowed = false; + notificationConfigurations = await mutations.notificationsSaveConfig(null, doc); + expect(notificationConfigurations.notifType).toEqual(doc.notifType); + expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); + expect(notificationConfigurations.user).toEqual(doc.user); + }); + + test('send notifications', async () => { + const _user = userFactory({}); + const _user2 = userFactory({}); + const _user3 = userFactory({}); + + // Create notification + const doc = { + notifType: 'channelMembersChange', + createdUser: _user._id, + title: 'new Notification title', + content: 'new Notification content', + link: 'new Notification link', + receivers: [_user, _user2, _user3], + }; + + sendNotification(doc); + expect(await Notifications.find({}).count()).toEqual(0); + }); +}); diff --git a/src/__tests__/notificationQueries.test.js b/src/__tests__/notificationQueries.test.js new file mode 100644 index 000000000..318cb33e5 --- /dev/null +++ b/src/__tests__/notificationQueries.test.js @@ -0,0 +1,15 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ +import { connect, disconnect } from '../db/connection'; +import { MODULE_LIST } from '../data/constants'; +import queries from '../data/resolvers/queries'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('notification query test', () => { + test('notification query test', async () => { + const modules = await queries.notifiationsModules(); + expect(modules).toEqual(MODULE_LIST); + }); +}); diff --git a/src/data/resolvers/mutations/configuration.js b/src/data/resolvers/mutations/configuration.js deleted file mode 100644 index 95d2cabbe..000000000 --- a/src/data/resolvers/mutations/configuration.js +++ /dev/null @@ -1,12 +0,0 @@ -import { NotificationConfigurations } from '../../../db/models'; -import { MODULE_LIST } from '../../constants'; - -export default { - configurationsGetModules() { - return MODULE_LIST; - }, - - configurationsSaveConfig(doc) { - return NotificationConfigurations.saveConfig(doc); - }, -}; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index 7095dc4f7..3e2349548 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -5,7 +5,6 @@ import responseTemplate from './responseTemplate'; import channel from './channel'; import form from './form'; import integration from './integration'; -import configuration from './configuration'; import notification from './notification'; export default { @@ -16,6 +15,5 @@ export default { ...channel, ...form, ...integration, - ...configuration, ...notification, }; diff --git a/src/data/resolvers/mutations/notification.js b/src/data/resolvers/mutations/notification.js index dd31429a2..43bbb8f1a 100644 --- a/src/data/resolvers/mutations/notification.js +++ b/src/data/resolvers/mutations/notification.js @@ -1,8 +1,17 @@ -import { Notifications } from '../../../db/models'; +import { NotificationConfigurations, Notifications } from '../../../db/models'; export default { - notificationsCreate(root, doc) { - return Notifications.createNotification(doc); + /** + * Save notification configuration + * @param {Object} + * @param {String.notifType} args.notifType + * @param {String.isAllowed} args.isAllowed + * @param {String.user} args.user + * @return {Promise} returns notification promise + * @throws {Error} apollo level error based on validation + */ + notificationsSaveConfig(root, doc) { + return NotificationConfigurations.createOrUpdateConfiguration(doc); }, /** diff --git a/src/data/resolvers/queries/index.js b/src/data/resolvers/queries/index.js index acf540310..8db8a0c9e 100644 --- a/src/data/resolvers/queries/index.js +++ b/src/data/resolvers/queries/index.js @@ -11,6 +11,7 @@ import customers from './customers'; import conversations from './conversations'; import insights from './insights'; import knowledgeBase from './knowledgeBase'; +import notifications from './notifications'; export default { ...users, @@ -26,4 +27,5 @@ export default { ...conversations, ...insights, ...knowledgeBase, + ...notifications, }; diff --git a/src/data/resolvers/queries/notifications.js b/src/data/resolvers/queries/notifications.js new file mode 100644 index 000000000..6bce67bcf --- /dev/null +++ b/src/data/resolvers/queries/notifications.js @@ -0,0 +1,12 @@ +import { MODULE_LIST } from '../../constants'; + +export default { + /** + * Module list used in notifications + * @param {Object} args + * @return {Promise} module list + */ + notifiationsModules() { + return MODULE_LIST; + }, +}; diff --git a/src/data/utils.js b/src/data/utils.js index 64d0a98e3..daa609bc5 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -1,4 +1,5 @@ import nodemailer from 'nodemailer'; +import { Channels, Notifications } from '../db/models'; export const sendEmail = ({ toEmails, fromEmail, title, content }) => { const { MAIL_SERVICE, MAIL_USER, MAIL_PASS } = process.env; @@ -25,3 +26,63 @@ export const sendEmail = ({ toEmails, fromEmail, title, content }) => { }); }); }; + +export const sendChannelNotifications = async ({ channelId, _memberIds, userId }) => { + const memberIds = _memberIds || []; + const channel = await Channels.findOne({ _id: channelId }); + + const content = `You have invited to '${channel.name}' channel.`; + + return sendNotification({ + createdUser: userId, + notifType: 'channelMembersChange', + title: content, + content, + link: `/inbox/${channel._id}`, + + // exclude current user + receivers: memberIds.filter(id => id !== userId), + }); +}; + +/** + * Send a notification + * @param {String} doc.notifType + * @param {String} doc.createdUser + * @param {String} doc.title + * @param {String} doc.content + * @param {String} doc.link + * @param {Array} doc.receivers Array of userIds + * @return null + */ +export const sendNotification = ({ receivers, ...doc }) => { + // Inserting entry to every receiver + for (const receiverId of receivers) { + doc.receiver = receiverId; + + try { + // create notification + Notifications.createNotificaton(doc); + + // TODO: implement sendEmail + // if receiver did not disable to get this notification + // const receiver = Users.findOne({ _id: receiverId }); + // const details = receiver.details; + // if receiver did not disable email notification then send email + // if (!(details && details.getNotificationByEmail === false)) { + // sendEmail({ + // to: receiver.emails[0].address, + // subject: 'Notification', + // template: { + // name: 'notification', + // data: { + // notification: doc, + // }, + // }, + // }); + // } + } catch (e) { + return new Error(e); + } + } +}; diff --git a/src/db/models/Notifications.js b/src/db/models/Notifications.js index c234cbd0e..06fccb55e 100644 --- a/src/db/models/Notifications.js +++ b/src/db/models/Notifications.js @@ -2,7 +2,7 @@ import mongoose from 'mongoose'; // schemas const NotificationSchema = new mongoose.Schema({ - notifType: String, + notifType: String, // TODO: type: enum title: String, link: String, content: String, @@ -17,6 +17,16 @@ class Notification { return this.update({ _id: { $in: ids } }, { $set: { isRead: true } }, { multi: true }); } + /** Create a notification + * @param {String} doc.notifType + * @param {String} doc.createdUser + * @param {String} doc.title + * @param {String} doc.content + * @param {String} doc.link + * @param {String} doc.receiver + * @return {Notification} Notification Object + * @throws {Exception} throws Exception if createdUser is not supplied + */ static async createNotification({ createdUser, ...doc }) { if (!createdUser) { throw new Error('createdUser must be supplied'); From fc7da183129f4e8e4f58823ab53c7f05ed02bb32 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Wed, 11 Oct 2017 10:51:56 +0800 Subject: [PATCH 032/318] #13 do notification mutation, query, model and util tests --- src/__tests__/notificationMutations.test.js | 93 ++++++++++++++++++-- src/__tests__/notificationQueries.test.js | 4 +- src/data/resolvers/mutations/channel.js | 21 ++++- src/data/resolvers/mutations/notification.js | 6 +- src/data/resolvers/queries/notifications.js | 2 +- src/data/schema/index.js | 9 ++ src/data/schema/integration.js | 2 +- src/data/schema/notification.js | 34 +++++++ src/data/utils.js | 27 ++---- src/db/models/Channels.js | 1 + src/db/models/Notifications.js | 6 +- 11 files changed, 164 insertions(+), 41 deletions(-) create mode 100644 src/data/schema/notification.js diff --git a/src/__tests__/notificationMutations.test.js b/src/__tests__/notificationMutations.test.js index 84ef65db6..66a0bd182 100644 --- a/src/__tests__/notificationMutations.test.js +++ b/src/__tests__/notificationMutations.test.js @@ -5,8 +5,7 @@ import { connect, disconnect } from '../db/connection'; import { Notifications, NotificationConfigurations, Users } from '../db/models'; import mutations from '../data/resolvers/mutations'; import { userFactory, configurationFactory } from '../db/factories'; -import { MODULE_LIST } from '../data/constants'; -import { sendChannelNotifications, sendNotification } from '../data/utils'; +import { sendNotification } from '../data/utils'; beforeAll(() => connect()); afterAll(() => disconnect()); @@ -125,6 +124,7 @@ describe('NotificationConfiguration model tests', async () => { let notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration( doc, ); + expect(notificationConfigurations.notifType).toEqual(doc.notifType); expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); expect(notificationConfigurations.user).toEqual(doc.user); @@ -133,13 +133,16 @@ describe('NotificationConfiguration model tests', async () => { doc.notifType = 'conversationAssigneeChange'; notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration(doc); + expect(notificationConfigurations.notifType).toEqual(doc.notifType); expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); expect(notificationConfigurations.user).toEqual(doc.user); // Change notification doc.isAllowed = false; + notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration(doc); + expect(notificationConfigurations.notifType).toEqual(doc.notifType); expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); expect(notificationConfigurations.user).toEqual(doc.user); @@ -157,6 +160,7 @@ describe('test mutations', () => { test('mutations', async () => { // notification confuration test const user = await userFactory({}); + const doc = { notifType: 'conversationAddMessage', isAllowed: true, @@ -165,6 +169,7 @@ describe('test mutations', () => { let notificationConfigurations = await mutations.notificationsSaveConfig(null, doc); expect(notificationConfigurations.notifType).toEqual(doc.notifType); + expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); expect(notificationConfigurations.user).toEqual(doc.user); @@ -172,34 +177,104 @@ describe('test mutations', () => { doc.notifType = 'conversationAssigneeChange'; notificationConfigurations = await mutations.notificationsSaveConfig(null, doc); + expect(notificationConfigurations.notifType).toEqual(doc.notifType); expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); expect(notificationConfigurations.user).toEqual(doc.user); // Change notification doc.isAllowed = false; + notificationConfigurations = await mutations.notificationsSaveConfig(null, doc); + expect(notificationConfigurations.notifType).toEqual(doc.notifType); expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); expect(notificationConfigurations.user).toEqual(doc.user); }); test('send notifications', async () => { - const _user = userFactory({}); - const _user2 = userFactory({}); - const _user3 = userFactory({}); + const _user = await userFactory({}); + const _user2 = await userFactory({}); + const _user3 = await userFactory({}); - // Create notification + // Try to send notifications when there is config not allowing it const doc = { notifType: 'channelMembersChange', createdUser: _user._id, title: 'new Notification title', content: 'new Notification content', link: 'new Notification link', - receivers: [_user, _user2, _user3], + receivers: [_user._id, _user2._id, _user3._id], }; - sendNotification(doc); - expect(await Notifications.find({}).count()).toEqual(0); + await NotificationConfigurations.createOrUpdateConfiguration({ + notifType: 'channelMembersChange', + isAllowed: false, + user: _user._id, + }); + + await NotificationConfigurations.createOrUpdateConfiguration({ + notifType: 'channelMembersChange', + isAllowed: false, + user: _user2._id, + }); + + await NotificationConfigurations.createOrUpdateConfiguration({ + notifType: 'channelMembersChange', + isAllowed: false, + user: _user3._id, + }); + + await sendNotification(doc); + + let notifications = await Notifications.find({}); + + expect(notifications.length).toEqual(0); + + // Send notifications when there is config allowing it + await NotificationConfigurations.createOrUpdateConfiguration({ + notifType: 'channelMembersChange', + isAllowed: true, + user: _user._id, + }); + + await NotificationConfigurations.createOrUpdateConfiguration({ + notifType: 'channelMembersChange', + isAllowed: true, + user: _user2._id, + }); + + await NotificationConfigurations.createOrUpdateConfiguration({ + notifType: 'channelMembersChange', + isAllowed: true, + user: _user3._id, + }); + + await sendNotification(doc); + + notifications = await Notifications.find({}); + + expect(notifications.length).toEqual(3); + + expect(notifications[0].notifType).toEqual(doc.notifType); + expect(notifications[0].createdUser).toEqual(doc.createdUser); + expect(notifications[0].title).toEqual(doc.title); + expect(notifications[0].content).toEqual(doc.content); + expect(notifications[0].link).toEqual(doc.link); + expect(notifications[0].receiver).toEqual(_user._id); + + expect(notifications[1].notifType).toEqual(doc.notifType); + expect(notifications[1].createdUser).toEqual(doc.createdUser); + expect(notifications[1].title).toEqual(doc.title); + expect(notifications[1].content).toEqual(doc.content); + expect(notifications[1].link).toEqual(doc.link); + expect(notifications[1].receiver).toEqual(_user2._id); + + expect(notifications[2].notifType).toEqual(doc.notifType); + expect(notifications[2].createdUser).toEqual(doc.createdUser); + expect(notifications[2].title).toEqual(doc.title); + expect(notifications[2].content).toEqual(doc.content); + expect(notifications[2].link).toEqual(doc.link); + expect(notifications[2].receiver).toEqual(_user3._id); }); }); diff --git a/src/__tests__/notificationQueries.test.js b/src/__tests__/notificationQueries.test.js index 318cb33e5..feae7083a 100644 --- a/src/__tests__/notificationQueries.test.js +++ b/src/__tests__/notificationQueries.test.js @@ -8,8 +8,8 @@ beforeAll(() => connect()); afterAll(() => disconnect()); describe('notification query test', () => { - test('notification query test', async () => { - const modules = await queries.notifiationsModules(); + test('notification query test', () => { + const modules = queries.notificationsModules(); expect(modules).toEqual(MODULE_LIST); }); }); diff --git a/src/data/resolvers/mutations/channel.js b/src/data/resolvers/mutations/channel.js index de98af0e1..ad692dfb5 100644 --- a/src/data/resolvers/mutations/channel.js +++ b/src/data/resolvers/mutations/channel.js @@ -1,4 +1,5 @@ import { Channels } from '../../../db/models'; +import { sendChannelNotifications } from '../../utils'; export default { /** @@ -12,9 +13,16 @@ export default { * @return {Promise} returns channel object * @throws {Error} throws apollo level validation errors */ - channelsCreate(root, doc) { - // TODO: sendNotifications method should here - return Channels.createChannel(doc); + async channelsCreate(root, doc) { + const channel = Channels.createChannel(doc); + + sendChannelNotifications({ + userId: doc.userId, + memberIds: doc.memberIds, + channelId: channel._id, + }); + + return channel; }, /** @@ -30,7 +38,12 @@ export default { * @throws {Error} throws apollo level validation errors */ channelsEdit(root, { _id, ...doc }) { - // TODO: sendNotifications method shoul be here + sendChannelNotifications({ + channelId: _id, + memberIds: doc.memberIds, + userId: doc.userId, + }); + return Channels.updateChannel(_id, doc); }, diff --git a/src/data/resolvers/mutations/notification.js b/src/data/resolvers/mutations/notification.js index 43bbb8f1a..37f24a030 100644 --- a/src/data/resolvers/mutations/notification.js +++ b/src/data/resolvers/mutations/notification.js @@ -4,9 +4,9 @@ export default { /** * Save notification configuration * @param {Object} - * @param {String.notifType} args.notifType - * @param {String.isAllowed} args.isAllowed - * @param {String.user} args.user + * @param {String} args.notifType + * @param {Boolean} args.isAllowed + * @param {String} args.user * @return {Promise} returns notification promise * @throws {Error} apollo level error based on validation */ diff --git a/src/data/resolvers/queries/notifications.js b/src/data/resolvers/queries/notifications.js index 6bce67bcf..0088ec6d8 100644 --- a/src/data/resolvers/queries/notifications.js +++ b/src/data/resolvers/queries/notifications.js @@ -6,7 +6,7 @@ export default { * @param {Object} args * @return {Promise} module list */ - notifiationsModules() { + notificationsModules() { return MODULE_LIST; }, }; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index 22a741981..a03542c11 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -38,6 +38,12 @@ import { types as InsightTypes, queries as InsightQueries } from './insight'; import { types as KnowledgeBaseTypes, queries as KnowledgeBaseQueries } from './knowledgeBase'; +import { + types as NotificationTypes, + queries as NotificationQueries, + mutations as NotificationMutations, +} from './notification'; + import { types as ConversationTypes, queries as ConversationQueries, @@ -61,6 +67,7 @@ export const types = ` ${ConversationTypes} ${InsightTypes} ${KnowledgeBaseTypes} + ${NotificationTypes} `; export const queries = ` @@ -78,6 +85,7 @@ export const queries = ` ${ConversationQueries} ${InsightQueries} ${KnowledgeBaseQueries} + ${NotificationQueries} } `; @@ -90,6 +98,7 @@ export const mutations = ` ${ChannelMutations} ${FormMutatons} ${IntegrationMutations} + ${NotificationMutations} } `; diff --git a/src/data/schema/integration.js b/src/data/schema/integration.js index 5df6fe3da..7bc4fb840 100644 --- a/src/data/schema/integration.js +++ b/src/data/schema/integration.js @@ -49,7 +49,7 @@ export const types = ` thankYouMessage: String } - input MessengerUIOptions { + input MessengerUiOptions { color: String wallpaper: String logo: String diff --git a/src/data/schema/notification.js b/src/data/schema/notification.js new file mode 100644 index 000000000..d6e75091a --- /dev/null +++ b/src/data/schema/notification.js @@ -0,0 +1,34 @@ +export const types = ` + type Notification { + _id: String! + notifType: String + title: String + link: String + content: String + createdUser: String + receiver: String + date: Date + isRead: Boolean + } + + type NotificationConfiguration { + _id: String! + user: String + notifType: String + isAllowed: Boolean + } +`; + +export const mutations = ` + notificationsSaveConfig ( + notifType: String, + isAllowed: Boolean, + user: String, + ): NotificationConfiguration + + notificationsMarkAsRead ( ids: [String]! ) : Boolean +`; + +export const queries = ` + notificationsModules(ids: [String]) : [String] +`; diff --git a/src/data/utils.js b/src/data/utils.js index daa609bc5..1adba1603 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -55,34 +55,21 @@ export const sendChannelNotifications = async ({ channelId, _memberIds, userId } * @param {Array} doc.receivers Array of userIds * @return null */ -export const sendNotification = ({ receivers, ...doc }) => { +export const sendNotification = async ({ receivers, ...doc }) => { // Inserting entry to every receiver for (const receiverId of receivers) { doc.receiver = receiverId; try { // create notification - Notifications.createNotificaton(doc); - + await Notifications.createNotification(doc); // TODO: implement sendEmail - // if receiver did not disable to get this notification - // const receiver = Users.findOne({ _id: receiverId }); - // const details = receiver.details; - // if receiver did not disable email notification then send email - // if (!(details && details.getNotificationByEmail === false)) { - // sendEmail({ - // to: receiver.emails[0].address, - // subject: 'Notification', - // template: { - // name: 'notification', - // data: { - // notification: doc, - // }, - // }, - // }); - // } } catch (e) { - return new Error(e); + if (e.message != 'Configuration does not exist') { + return e; + } } } + + return; }; diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index 7bd3b6a39..bc0b60c8e 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -65,6 +65,7 @@ class Channel { this.preSave(doc); doc.conversationCount = 0; doc.openConversationCount = 0; + return this.create(doc); } diff --git a/src/db/models/Notifications.js b/src/db/models/Notifications.js index 06fccb55e..c886c7609 100644 --- a/src/db/models/Notifications.js +++ b/src/db/models/Notifications.js @@ -1,8 +1,12 @@ import mongoose from 'mongoose'; +import { MODULE_LIST } from '../../data/constants'; // schemas const NotificationSchema = new mongoose.Schema({ - notifType: String, // TODO: type: enum + notifType: { + type: String, + enum: MODULE_LIST, + }, title: String, link: String, content: String, From a4c7d8e3c894b99844512df3b927312c0bf031b9 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Wed, 11 Oct 2017 10:59:10 +0800 Subject: [PATCH 033/318] #13 do small changes --- src/__tests__/channelMutations.test.js | 10 ++++++++++ src/__tests__/formMutations.test.js | 4 ++++ src/data/utils.js | 6 +++--- src/db/models/Forms.js | 2 +- src/db/models/Notifications.js | 3 +-- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/__tests__/channelMutations.test.js b/src/__tests__/channelMutations.test.js index 7fae7080b..897911e2e 100644 --- a/src/__tests__/channelMutations.test.js +++ b/src/__tests__/channelMutations.test.js @@ -100,8 +100,11 @@ describe('channel update tests', () => { let channel = await Channels.createChannel(doc); doc.memberIds = [_user2._id]; + await Channels.updateChannel(channel._id, doc); + channel = await Channels.findOne({ _id: channel._id }); + expect(channel.name).toEqual(doc.name); expect(channel.description).toEqual(doc.description); expect(channel.memberIds.length).toBe(2); @@ -211,8 +214,11 @@ describe('mutations', () => { }; doc.memberIds = [_user2._id]; + await mutations.channelsEdit(null, { ...doc, _id: channel._id }); + channel = await Channels.findOne({ _id: channel._id }); + expect(channel.name).toEqual(doc.name); expect(channel.description).toEqual(doc.description); expect(channel.memberIds.length).toBe(2); @@ -225,13 +231,17 @@ describe('mutations', () => { expect(channel.openConversationCount).toEqual(0); doc.memberIds = [_user._id]; + await mutations.channelsEdit(null, { ...doc, _id: channel._id }); + channel = await Channels.findOne({ _id: channel._id }); + expect(channel.memberIds.length).toBe(1); expect(channel.memberIds[0]).toBe(_user._id); await mutations.channelsRemove(null, { _id: channel._id }); const channelCount = await Channels.find({}).count(); + expect(channelCount).toBe(0); }); }); diff --git a/src/__tests__/formMutations.test.js b/src/__tests__/formMutations.test.js index 7d48c255e..dceaebbc5 100644 --- a/src/__tests__/formMutations.test.js +++ b/src/__tests__/formMutations.test.js @@ -28,6 +28,7 @@ describe('form creation tests', () => { test('form creation test without userId supplied', async () => { expect.assertions(1); + try { await Forms.createForm({ title: 'Test form', @@ -58,6 +59,7 @@ describe('form creation tests', () => { describe('form update tests', () => { let _user; + /** * Testing with an _user object */ @@ -423,6 +425,7 @@ describe('mutations', () => { }); const formAfterUpdate = await Forms.findOne({ _id: form._id }); + expect(formAfterUpdate.title).toBe('Test form 2'); expect(formAfterUpdate.description).toBe('Test form description 2'); expect(form.createdUserId).toBe(formAfterUpdate.createdUserId); @@ -461,6 +464,7 @@ describe('mutations', () => { }); const updatedFormField = await FormFields.findOne({ _id: newFormField._id }); + expect(updatedFormField.formId).toEqual(form._id); expect(updatedFormField.type).toEqual('mutation input 1'); expect(updatedFormField.validation).toEqual('mutation number 1'); diff --git a/src/data/utils.js b/src/data/utils.js index 1adba1603..c3ce2651d 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -40,7 +40,7 @@ export const sendChannelNotifications = async ({ channelId, _memberIds, userId } content, link: `/inbox/${channel._id}`, - // exclude current user + // Exclude current user receivers: memberIds.filter(id => id !== userId), }); }; @@ -61,9 +61,9 @@ export const sendNotification = async ({ receivers, ...doc }) => { doc.receiver = receiverId; try { - // create notification + // Create notification await Notifications.createNotification(doc); - // TODO: implement sendEmail + // TODO: Implement sendEmail } catch (e) { if (e.message != 'Configuration does not exist') { return e; diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index f67e11719..6a423edb3 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -123,7 +123,7 @@ class FormField { const lastField = await FormFields.findOne({}, { order: 1 }, { sort: { order: -1 } }); doc.formId = formId; - // if there is no field then start with 0 + // If there is no field then start with 0 let order = 0; if (lastField) { diff --git a/src/db/models/Notifications.js b/src/db/models/Notifications.js index c886c7609..4d810b932 100644 --- a/src/db/models/Notifications.js +++ b/src/db/models/Notifications.js @@ -1,7 +1,6 @@ import mongoose from 'mongoose'; import { MODULE_LIST } from '../../data/constants'; -// schemas const NotificationSchema = new mongoose.Schema({ notifType: { type: String, @@ -66,7 +65,7 @@ NotificationSchema.loadClass(Notification); export const Notifications = mongoose.model('notifications', NotificationSchema); const ConfigSchema = new mongoose.Schema({ - // to whom this config is related + // To whom this config is related user: String, notifType: String, isAllowed: Boolean, From 4633800b6c51d4190df42efc22620cbec41ec107 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 11 Oct 2017 11:17:50 +0800 Subject: [PATCH 034/318] Add fieldsCombinedByContentType mutation --- src/data/resolvers/queries/customers.js | 11 +++++ src/data/resolvers/queries/fields.js | 65 ++++++++++++++++++++++++- src/data/resolvers/queries/segments.js | 64 +----------------------- src/data/schema/customer.js | 7 +++ src/data/schema/field.js | 1 + src/data/schema/segment.js | 6 --- 6 files changed, 84 insertions(+), 70 deletions(-) diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index 4caef345f..a0bfd857a 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -150,4 +150,15 @@ export default { customersTotalCount() { return Customers.find({}).count(); }, + + /** + * Default list columns config + */ + customersListConfig() { + return [ + { name: 'name', label: 'Name', order: 1 }, + { name: 'email', label: 'Email', order: 2 }, + { name: 'phone', label: 'Phone', order: 3 }, + ]; + }, }; diff --git a/src/data/resolvers/queries/fields.js b/src/data/resolvers/queries/fields.js index c4805f6f8..736cc5191 100644 --- a/src/data/resolvers/queries/fields.js +++ b/src/data/resolvers/queries/fields.js @@ -1,4 +1,4 @@ -import { Fields } from '../../../db/models'; +import { Customers, Fields } from '../../../db/models'; export default { /** @@ -15,4 +15,67 @@ export default { return Fields.find(query).sort({ order: 1 }); }, + + /** + * Generates all field choices base on given kind. + * For example if kind is customer + * then it will generate customer related fields + * + * @param {String} kind customer or company + * + * @return {[JSON]} + * [{ name: 'messengerData.isActive', text: 'Messenger: is Active' }] + */ + async fieldsCombinedByContentType() { + /* + * Generates fields using given schema + * @param {Schema} schema Customers.schema etc ... + * @param {namePrefix} sub field's prefix like messengerData. or empty str + * @return {Array} array of fields + */ + const generateFieldsFromSchema = (schema, namePrefix) => { + const fields = []; + + // field definations + const paths = schema.paths; + + schema.eachPath(name => { + const label = paths[name].options.label; + + // add to fields list + if (label) { + fields.push({ + name: `${namePrefix}${name}`, + label, + }); + } + }); + + return fields; + }; + + // generate list using customer schema + let fields = generateFieldsFromSchema(Customers.schema, ''); + + Customers.schema.eachPath(name => { + const path = Customers.schema.paths[name]; + + // extend fields list using sub schema fields + if (path.schema) { + fields = [...fields, ...generateFieldsFromSchema(path.schema, `${name}.`)]; + } + }); + + const customFields = await Fields.getCustomerFields(); + + // extend fields list using custom fields + customFields.forEach(customField => { + fields.push({ + name: `customFieldsData.${customField._id}`, + label: customField.text, + }); + }); + + return fields; + }, }; diff --git a/src/data/resolvers/queries/segments.js b/src/data/resolvers/queries/segments.js index be568ec63..33368c131 100644 --- a/src/data/resolvers/queries/segments.js +++ b/src/data/resolvers/queries/segments.js @@ -1,4 +1,4 @@ -import { Segments, Customers, Fields } from '../../../db/models'; +import { Segments } from '../../../db/models'; export default { /** @@ -26,66 +26,4 @@ export default { segmentDetail(root, { _id }) { return Segments.findOne({ _id }); }, - - /** - * Generates field choices base on given kind. For example if kind is customer - * then it will generate customer related fields - * - * @param {String} kind customer or company - * - * @return {[SegmentField]} - * [{ name: 'messengerData.isActive', text: 'Messenger: is Active' }] - */ - async segmentsGetFields() { - /* - * Generates fields using given schema - * @param {Schema} schema Customers.schema etc ... - * @param {namePrefix} sub field's prefix like messengerData. or empty str - * @return {Array} array of fields - */ - const generateFieldsFromSchema = (schema, namePrefix) => { - const fields = []; - - // field definations - const paths = schema.paths; - - schema.eachPath(name => { - const label = paths[name].options.label; - - // add to fields list - if (label) { - fields.push({ - name: `${namePrefix}${name}`, - label, - }); - } - }); - - return fields; - }; - - // generate list using customer schema - let fields = generateFieldsFromSchema(Customers.schema, ''); - - Customers.schema.eachPath(name => { - const path = Customers.schema.paths[name]; - - // extend fields list using sub schema fields - if (path.schema) { - fields = [...fields, ...generateFieldsFromSchema(path.schema, `${name}.`)]; - } - }); - - const customFields = await Fields.getCustomerFields(); - - // extend fields list using custom fields - customFields.forEach(customField => { - fields.push({ - name: `customFieldsData.${customField._id}`, - label: customField.text, - }); - }); - - return fields; - }, }; diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index ad72112ae..fef74853d 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -30,6 +30,12 @@ export const types = ` getMessengerCustomData: JSON getTags: [Tag] } + + type CustomerListConfigItem { + name: String + label: String + order: Int + } `; export const queries = ` @@ -38,6 +44,7 @@ export const queries = ` customerDetail(_id: String!): Customer customerListForSegmentPreview(segment: JSON, limit: Int): [Customer] customersTotalCount: Int + customersListConfig: [CustomerListConfigItem] `; const fields = ` diff --git a/src/data/schema/field.js b/src/data/schema/field.js index 3d6355975..497d56b97 100644 --- a/src/data/schema/field.js +++ b/src/data/schema/field.js @@ -20,6 +20,7 @@ export const types = ` export const queries = ` fields(contentType: String!, contentTypeId: String): [Field] + fieldsCombinedByContentType: JSON `; const commonFields = ` diff --git a/src/data/schema/segment.js b/src/data/schema/segment.js index 7c36c0c20..6d8d8ef50 100644 --- a/src/data/schema/segment.js +++ b/src/data/schema/segment.js @@ -19,18 +19,12 @@ export const types = ` getParentSegment: Segment getSubSegments: [Segment] } - - type SegmentField { - name: String! - label: String! - } `; export const queries = ` segments: [Segment] segmentDetail(_id: String): Segment segmentsGetHeads: [Segment] - segmentsGetFields(kind: String): [SegmentField] `; const commonFields = ` From 70f3d0becd6b2410be22eb749a5c0da2805cc049 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Wed, 11 Oct 2017 15:26:00 +0800 Subject: [PATCH 035/318] Engage message mutation code comment --- src/__tests__/engage_messages.test.js | 14 -------- src/data/resolvers/mutations/engages.js | 46 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/__tests__/engage_messages.test.js b/src/__tests__/engage_messages.test.js index caf555bf6..f06d58d6e 100644 --- a/src/__tests__/engage_messages.test.js +++ b/src/__tests__/engage_messages.test.js @@ -13,18 +13,11 @@ describe('engage messages models', () => { let _user; let _segment = segmentsFactory(); - /** - * Before each test create test data - * containing 2 users and an integration - */ beforeEach(async () => { _user = await userFactory({}); _segment = await segmentsFactory({}); }); - /** - * After each test remove the test data - */ afterEach(async () => { await Users.remove({}); await Segments.remove({}); @@ -95,10 +88,6 @@ describe('mutations', () => { let _segment = segmentsFactory(); let _doc = null; - /** - * Before each test create test data - * containing 2 users and an integration - */ beforeEach(async () => { _user = await userFactory({}); _segment = await segmentsFactory({}); @@ -111,9 +100,6 @@ describe('mutations', () => { }; }); - /** - * After each test remove the test data - */ afterEach(async () => { _doc = null; await Users.remove({}); diff --git a/src/data/resolvers/mutations/engages.js b/src/data/resolvers/mutations/engages.js index 30f1bd1fe..46b00d746 100644 --- a/src/data/resolvers/mutations/engages.js +++ b/src/data/resolvers/mutations/engages.js @@ -3,36 +3,82 @@ import { EngageMessages } from '../../../db/models'; export default { /** * Create new message + * @param {String} doc.title + * @param {String} doc.fromUserId + * @param {String} doc.kind + * @param {String} doc.method + * @param {String} doc.email + * @param {[String]} doc.customerIds + * @param {String} doc.messenger + * @param {Boolean} doc.isDraft + * @param {Boolean} doc.isLive + * @param {Date} doc.stopDate + * @param {[String]} doc.tagIds * @return {Promise} message object */ async messagesAdd(root, doc) { return await EngageMessages.createMessage(doc); }, + /** + * Update message + * @param {String} doc.title + * @param {String} doc.fromUserId + * @param {String} doc.kind + * @param {String} doc.method + * @param {String} doc.email + * @param {[String]} doc.customerIds + * @param {String} doc.messenger + * @param {Boolean} doc.isDraft + * @param {Boolean} doc.isLive + * @param {Date} doc.stopDate + * @param {[String]} doc.tagIds + * @return {Promise} message object + */ async messageEdit(root, { _id, ...doc }) { await EngageMessages.updateMessage(_id, doc); return await EngageMessages.findOne({ _id }); }, + /** + * Remove message + * @param {String} id + * @return {Promise} null + */ async messagesRemove(root, _id) { await EngageMessages.removeMessage(_id); return true; }, + /** + * Update message + * @param {String} id + * @return {Promise} message object + */ async messagesSetLive(root, _id) { await EngageMessages.updateMessage(_id, { isLive: true, isDraft: false }); return await EngageMessages.findOne({ _id }); }, + /** + * Update message + * @param {String} id + * @return {Promise} message object + */ async messagesSetPause(root, _id) { await EngageMessages.updateMessage(_id, { isLive: false }); return await EngageMessages.findOne({ _id }); }, + /** + * Update message + * @param {String} id + * @return {Promise} message object + */ async messagesSetLiveManual(root, _id) { await EngageMessages.updateMessage(_id, { isLive: true, isDraft: false }); From 2b0bffc8591aa36945b76e3912dc69946720681e Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 11 Oct 2017 15:37:00 +0800 Subject: [PATCH 036/318] Add _id to combined field list --- src/data/resolvers/queries/fields.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/data/resolvers/queries/fields.js b/src/data/resolvers/queries/fields.js index 736cc5191..1f24ade44 100644 --- a/src/data/resolvers/queries/fields.js +++ b/src/data/resolvers/queries/fields.js @@ -45,6 +45,7 @@ export default { // add to fields list if (label) { fields.push({ + _id: Math.random(), name: `${namePrefix}${name}`, label, }); @@ -71,6 +72,7 @@ export default { // extend fields list using custom fields customFields.forEach(customField => { fields.push({ + _id: Math.random(), name: `customFieldsData.${customField._id}`, label: customField.text, }); From 8d172ddcf50c65a78f84743215515d23ab6afdb9 Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Wed, 11 Oct 2017 17:13:05 +0800 Subject: [PATCH 037/318] Add conversation mutations --- package.json | 1 + src/__tests__/brandMutations.test.js | 14 +- .../conversationMessageMutations.test.js | 192 ++++++++++++ src/data/constants.js | 14 + src/data/resolvers/mutations/conversation.js | 39 --- src/data/resolvers/mutations/conversations.js | 280 ++++++++++++++++++ src/data/resolvers/mutations/index.js | 4 +- src/data/schema/conversation.js | 22 +- src/db/factories.js | 40 ++- src/db/models/Conversations.js | 46 ++- yarn.lock | 77 +++++ 11 files changed, 676 insertions(+), 53 deletions(-) create mode 100644 src/__tests__/conversationMessageMutations.test.js delete mode 100644 src/data/resolvers/mutations/conversation.js create mode 100644 src/data/resolvers/mutations/conversations.js diff --git a/package.json b/package.json index 561cf99eb..ccf6640db 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "passport": "^0.4.0", "passport-anonymous": "^1.0.1", "passport-http-bearer": "^1.0.1", + "strip": "^3.0.0", "subscriptions-transport-ws": "^0.7.3", "underscore": "^1.8.3" }, diff --git a/src/__tests__/brandMutations.test.js b/src/__tests__/brandMutations.test.js index e35d6fd25..91dd84f40 100644 --- a/src/__tests__/brandMutations.test.js +++ b/src/__tests__/brandMutations.test.js @@ -50,24 +50,24 @@ describe('Brands mutations', () => { test('Update brand', async () => { // get new brand object - const _brand_update = await brandFactory(); + const _brandUpdate = await brandFactory(); // update brand object const brandObj = await brandMutations.brandsEdit( {}, { _id: _brand.id, - code: _brand_update.code, - name: _brand_update.name, - description: _brand_update.description, + code: _brandUpdate.code, + name: _brandUpdate.name, + description: _brandUpdate.description, }, { user: _user }, ); // check changes - expect(brandObj.code).toBe(_brand_update.code); - expect(brandObj.name).toBe(_brand_update.name); - expect(brandObj.description).toBe(_brand_update.description); + expect(brandObj.code).toBe(_brandUpdate.code); + expect(brandObj.name).toBe(_brandUpdate.name); + expect(brandObj.description).toBe(_brandUpdate.description); }); test('Update brand login required', async () => { diff --git a/src/__tests__/conversationMessageMutations.test.js b/src/__tests__/conversationMessageMutations.test.js new file mode 100644 index 000000000..a830ee393 --- /dev/null +++ b/src/__tests__/conversationMessageMutations.test.js @@ -0,0 +1,192 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Conversations, ConversationMessages, Users } from '../db/models'; +import { conversationFactory, conversationMessageFactory, userFactory } from '../db/factories'; +import conversationMutations from '../data/resolvers/mutations/conversations'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Conversation message mutations', () => { + let _conversation; + let _conversationMessage; + let _user; + let _doc; + + beforeEach(async () => { + // Creating test data + _conversation = await conversationFactory(); + _conversationMessage = await conversationMessageFactory(); + _user = await userFactory(); + _doc = { + content: _conversationMessage.content, + attachments: _conversationMessage.attachments, + status: _conversationMessage.status, + mentionedUserIds: _conversationMessage.mentionedUserIds, + conversationId: _conversation._id, + internal: _conversationMessage.internal, + customerId: _conversationMessage.customerId, + isCustomerRead: _conversationMessage.isCustomerRead, + engageData: _conversationMessage.engageData, + formWidgetData: _conversationMessage.formWidgetData, + facebookData: _conversationMessage.facebookData, + }; + }); + + afterEach(async () => { + // Clearing test data + await Conversations.remove({}); + await ConversationMessages.remove({}); + await Users.remove({}); + }); + + test('Create conversation message', async () => { + const conversationObj = await conversationMutations.conversationMessageAdd({}, _doc, { + user: _user, + }); + + expect(conversationObj).toBeDefined(); + expect(conversationObj.content).toBe(_conversationMessage.content); + expect(conversationObj.attachments).toBe(_conversationMessage.attachments); + expect(conversationObj.status).toBe(_conversationMessage.status); + expect(conversationObj.mentionedUserIds[0]).toBe(_conversationMessage.mentionedUserIds[0]); + expect(conversationObj.conversationId).toBe(_conversation._id); + expect(conversationObj.internal).toBe(_conversationMessage.internal); + expect(conversationObj.customerId).toBe(_conversationMessage.customerId); + expect(conversationObj.isCustomerRead).toBe(_conversationMessage.isCustomerRead); + expect(conversationObj.engageData).toBe(_conversationMessage.engageData); + expect(conversationObj.formWidgetData).toBe(_conversationMessage.formWidgetData); + expect(conversationObj.facebookData).toBe(_conversationMessage.facebookData); + expect(conversationObj.userId).toBe(_user._id); + }); + + // check conversation if integration doesn't found + test('Create conversation message without integration', async () => { + expect.assertions(1); + + _doc['internal'] = false; + try { + await conversationMutations.conversationMessageAdd({}, _doc, { user: _user }); + } catch (e) { + expect(e.message).toEqual('Integration not found'); + } + }); + + // if user assigned to conversation + test('Assign conversation to employee', async () => { + await conversationMutations.conversationsAssign( + {}, + { conversationIds: [_conversation._id], assignedUserId: _user._id }, + { user: _user }, + ); + + const conversation_list = await Conversations.find({ _id: { $in: [_conversation._id] } }); + conversation_list.forEach(conversationObj => { + expect(conversationObj.assignedUserId).toBe(_user._id); + }); + }); + + test('Unassign employee from conversation', async () => { + // assign employee before unassign + await conversationMutations.conversationsAssign( + {}, + { conversationIds: [_conversation._id], assignedUserId: _user._id }, + { user: _user }, + ); + + // unassign + await conversationMutations.conversationsUnassign( + {}, + { _ids: [_conversation._id] }, + { user: _user }, + ); + + const conversationObj = await Conversations.findOne({ _id: _conversation._id }); + expect(conversationObj.assignedUserId).toBe(undefined); + }); + + test('Change conversation status', async () => { + // assign employee before unassign + await conversationMutations.conversationsChangeStatus( + {}, + { _ids: [_conversation._id] }, + { user: _user }, + ); + + const conversationObj = await Conversations.findOne({ _id: _conversation._id }); + expect(conversationObj.status).toBe('new'); + }); + + test('Conversation star', async () => { + // assign employee before unassign + await conversationMutations.conversationsStar( + {}, + { _ids: [_conversation._id] }, + { user: _user }, + ); + + const user = await Users.findOne({ _id: _user._id }); + expect(user.details.starredConversationIds[0]).toBe(_conversation._id); + }); + + test('Conversation unstar', async () => { + const ids = [_conversation._id]; + + // star first before unstar + await Users.update( + { _id: _user.id }, + { + $addToSet: { + 'details.starredConversationIds': { $each: ids }, + }, + }, + ); + + // unstar + await conversationMutations.conversationsUnstar({}, { _ids: ids }, { user: _user }); + + const user = await Users.findOne({ _id: _user._id }); + expect(user.details.starredConversationIds.length).toBe(0); + }); + + test('Toggle participated users in conversation ', async () => { + // make sure participated users are empty + expect(_conversation.participatedUserIds.length).toBe(0); + await conversationMutations.conversationsToggleParticipate( + {}, + { _ids: [_conversation._id] }, + { user: _user }, + ); + + const conversationObj = await Conversations.findOne({ _id: _conversation.id }); + + // check if participated user is added + expect(conversationObj.participatedUserIds[0]).toBe(_user._id); + + await conversationMutations.conversationsToggleParticipate( + {}, + { _ids: [_conversation._id] }, + { user: _user }, + ); + + const conversationObjWithParticipatedUser = await Conversations.findOne({ + _id: _conversation.id, + }); + + // check if participated user is add + expect(conversationObjWithParticipatedUser.participatedUserIds.length).toBe(0); + }); + + test('Conversation mark as read', async () => { + await conversationMutations.conversationMarkAsRead( + {}, + { _id: _conversation._id }, + { user: _user }, + ); + const conversationObj = await Conversations.findOne({ _id: _conversation._id }); + expect(conversationObj.readUserIds[0]).toBe(_user._id); + }); +}); diff --git a/src/data/constants.js b/src/data/constants.js index c06f60b97..ab5b83cff 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -19,3 +19,17 @@ export const TAG_TYPES = { ENGAGE_MESSAGE: 'engageMessage', ALL_LIST: ['conversation', 'customer', 'engageMessage'], }; + +export const FACEBOOK_DATA_KINDS = { + FEED: 'feed', + MESSENGER: 'messenger', + ALL_LIST: ['feed', 'messenger'], +}; + +export const KIND_CHOICES = { + MESSENGER: 'messenger', + FORM: 'form', + TWITTER: 'twitter', + FACEBOOK: 'facebook', + ALL_LIST: ['messenger', 'form', 'twitter', 'facebook'], +}; diff --git a/src/data/resolvers/mutations/conversation.js b/src/data/resolvers/mutations/conversation.js deleted file mode 100644 index 0a0aea57a..000000000 --- a/src/data/resolvers/mutations/conversation.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Will implement actual db changes after removing meteor - */ - -import { Conversations, ConversationMessages } from '../../../db/models'; -import { pubsub } from '../subscriptions'; - -export default { - async conversationMessageInserted(root, { _id }) { - const message = await ConversationMessages.findOne({ _id }); - const conversationId = message.conversationId; - const conversation = await Conversations.findOne({ _id: conversationId }); - - pubsub.publish('conversationMessageInserted', { - conversationMessageInserted: message, - }); - - pubsub.publish('conversationsChanged', { - conversationsChanged: { customerId: conversation.customerId, type: 'newMessage' }, - }); - - return 'done'; - }, - - async conversationsChanged(root, { _ids, type }) { - for (let _id of _ids) { - const conversation = await Conversations.findOne({ _id }); - - // notify new message - pubsub.publish('conversationChanged', { - conversationChanged: { conversationId: _id, type }, - }); - - pubsub.publish('conversationsChanged', { - conversationsChanged: { customerId: conversation.customerId, type }, - }); - } - }, -}; diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js new file mode 100644 index 000000000..235eaf3fb --- /dev/null +++ b/src/data/resolvers/mutations/conversations.js @@ -0,0 +1,280 @@ +/* + * Will implement actual db changes after removing meteor + */ + +import strip from 'strip'; + +import { + Conversations, + ConversationMessages, + Users, + Integrations, + Customers, +} from '../../../db/models'; +import { pubsub } from '../subscriptions'; +import { KIND_CHOICES, CONVERSATION_STATUSES } from '../../constants'; + +const conversationsCheckExistance = async _ids => { + const selector = { _id: { $in: _ids } }; + const conversations = await Conversations.find(selector); + + if (conversations.length !== _ids.length) { + throw new Error('Conversation not found.'); + } + + return { selector, conversations }; +}; + +const conversationsChanged = async (_ids, type) => { + for (let _id of _ids) { + const conversation = await Conversations.findOne({ _id }); + + // notify new message + pubsub.publish('conversationChanged', { + conversationChanged: { conversationId: _id, type }, + }); + + pubsub.publish('conversationsChanged', { + conversationsChanged: { customerId: conversation.customerId, type }, + }); + } +}; + +export default { + /** + * Create new message in conversation + * @param {Object} doc contains conversation message inputs + * @return {Promise} messageId + */ + async conversationMessageAdd(root, doc, { user }) { + if (!user) throw new Error('Login required'); + + const conversation = await Conversations.findOne({ _id: doc.conversationId }); + + if (!conversation) throw new Error('Conversation not found'); + + // normalize content, attachments + const content = doc.content || ''; + const attachments = doc.attachments || []; + + doc.content = content; + doc.attachments = attachments; + + // if there is no attachments and no content then throw content required + // error + if (attachments.length === 0 && !strip(content)) throw new Error('Content is required'); + + // setting conversation's content to last message + Conversations.update({ _id: doc.conversationId }, { $set: { content } }); + + // TODO: send notification + + const userId = user._id; + + // do not send internal message to third service integrations + if (doc.internal) { + return ConversationMessages.createMessage({ ...doc, userId }); + } + + const integration = await Integrations.findOne({ _id: conversation.integrationId }); + + if (!integration) throw new Error('Integration not found'); + + const kind = integration.kind; + + // send reply to twitter + if (kind === KIND_CHOICES.TWITTER) { + // TODO: return tweetReply(conversation, strip(content)); + } + + const message = await ConversationMessages.createMessage({ ...doc, userId }); + const messageId = message._id; + const customer = await Customers.findOne({ _id: conversation.customerId }); + + // subscribe + pubsub.publish('conversationMessageInserted', { + conversationMessageInserted: message, + }); + + pubsub.publish('conversationsChanged', { + conversationsChanged: { customerId: conversation.customerId, type: 'newMessage' }, + }); + + // if conversation's integration kind is form then send reply to + // customer's email + const email = customer ? customer.email : ''; + + if (kind === KIND_CHOICES.FORM && email) { + // TODO: sendEmail + } + + // send reply to facebook + if (kind === KIND_CHOICES.FACEBOOK) { + // when facebook kind is feed, assign commentId in extraData + // TODO: facebookReply(conversation, strip(content), messageId); + } + + return messageId; + }, + + /* + * assign employee to conversation + */ + async conversationsAssign(root, { conversationIds, assignedUserId }, { user }) { + if (!user) throw new Error('Login required'); + + const { selector } = await conversationsCheckExistance(conversationIds); + + if (!Users.findOne({ _id: assignedUserId })) { + throw new Error('User not found.'); + } + + await Conversations.update( + { _id: { $in: conversationIds } }, + { $set: { assignedUserId } }, + { multi: true }, + ); + + // notify graphl subscription + conversationsChanged(conversationIds, 'statusChanged'); + + const updatedConversations = await Conversations.find(selector); + + // send notification + updatedConversations.forEach(function(conversation) { + const content = 'Assigned user has changed'; + // TODO: sendNotification + }); + }, + + /* + * unassign employee from conversation + */ + async conversationsUnassign(root, { _ids }, { user }) { + if (!user) throw new Error('Login required'); + + await conversationsCheckExistance(_ids); + + await Conversations.update( + { _id: { $in: _ids } }, + { $unset: { assignedUserId: 1 } }, + { multi: true }, + ); + + // notify graphl subscription + conversationsChanged(_ids, 'statusChanged'); + }, + + async conversationsChangeStatus(root, { _ids, status }, { user }) { + if (!user) throw new Error('Login required'); + + const { conversations } = await conversationsCheckExistance(_ids); + + Conversations.update({ _id: { $in: _ids } }, { $set: { status } }, { multi: true }); + + // notify graphl subscription + conversationsChanged(_ids, 'statusChanged'); + + conversations.forEach(function(conversation) { + if (status === CONVERSATION_STATUSES.CLOSED) { + const customer = conversation.customer(); + const integration = conversation.integration(); + const messengerData = integration.messengerData || {}; + const notifyCustomer = messengerData.notifyCustomer || false; + + if (notifyCustomer && customer.email) { + // send email to customer + // TODO: send email + } + } + }); + + const content = 'Conversation status has changed.'; + + // TODO: send notification + }, + + async conversationsStar(root, { _ids }, { user }) { + if (!user) throw new Error('Login required'); + + // check conversations existance + await conversationsCheckExistance(_ids); + + await Users.update( + { _id: user._id }, + { + $addToSet: { + 'details.starredConversationIds': { $each: _ids }, + }, + }, + ); + }, + + async conversationsUnstar(root, { _ids }, { user }) { + if (!user) throw new Error('Login required'); + + // check conversations existance + await conversationsCheckExistance(_ids); + + await Users.update( + { _id: user._id }, + { + $pull: { + 'details.starredConversationIds': { $in: _ids }, + }, + }, + ); + }, + + async conversationsToggleParticipate(root, { _ids }, { user }) { + if (!user) throw new Error('Login required'); + + const { selector } = await conversationsCheckExistance(_ids); + + const extendSelector = { + ...selector, + participatedUserIds: { $in: [user._id] }, + }; + + // not previously added + if ((await Conversations.find(extendSelector).count()) === 0) { + await Conversations.update( + selector, + { $addToSet: { participatedUserIds: user._id } }, + { multi: true }, + ); + } else { + // remove + await Conversations.update( + selector, + { $pull: { participatedUserIds: { $in: [user._id] } } }, + { multi: true }, + ); + } + + // notify graphl subscription + conversationsChanged(_ids, 'participatedStateChanged'); + }, + + async conversationMarkAsRead(root, { _id }, { user }) { + if (!user) throw new Error('Login required'); + + const conversation = await Conversations.findOne({ _id }); + + if (conversation) { + const readUserIds = conversation.readUserIds; + + // if current user is first one + if (!readUserIds) { + return Conversations.update({ _id: _id }, { $set: { readUserIds: [user._id] } }); + } + + // if current user is not in read users list then add it + if (!readUserIds.includes(user._id)) { + return Conversations.update({ _id }, { $push: { readUserIds: user._id } }); + } + } + + return 'not affected'; + }, +}; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index 121e8170b..7cab413a7 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -1,10 +1,10 @@ -import conversation from './conversation'; +import conversations from './conversations'; import brands from './brands'; import emailTemplate from './emailTemplate'; import responseTemplate from './responseTemplate'; export default { - ...conversation, + ...conversations, ...brands, ...emailTemplate, ...responseTemplate, diff --git a/src/data/schema/conversation.js b/src/data/schema/conversation.js index 3a94f8bdc..f0307ab82 100644 --- a/src/data/schema/conversation.js +++ b/src/data/schema/conversation.js @@ -68,6 +68,17 @@ export const types = ` type: String! customerId: String! } + + input ConversationMessageParams { + content: String, + mentionedUserIds: [String], + conversationId: String, + internal: Boolean, + customerId: String, + userId: String, + createdAt: Date, + isCustomerRead: Boolean, + } `; export const queries = ` @@ -78,6 +89,13 @@ export const queries = ` `; export const mutations = ` - conversationsChanged(_ids: [String]!, type: String): String - conversationMessageInserted(_id: String!): String + conversationMessageAdd(params: ConversationMessageParams): String + conversationsCheckExistance(_ids: [String]!): String + conversationsAssign(conversationIds: [String]!, assignedUserId: String): String + conversationsUnassign(_ids: [String]!): String + conversationsChangeStatus(_ids: [String]!): String + conversationsStar(_ids: [String]!): String + conversationsUnstar(_ids: [String]!): String + conversationsToggleParticipate(_ids: [String]!): String + conversationMarkAsRead(_id: String): String `; diff --git a/src/db/factories.js b/src/db/factories.js index 585f1ebd6..c5f3cb9e0 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -1,7 +1,15 @@ import faker from 'faker'; import Random from 'meteor-random'; -import { Users, Brands, EmailTemplates, ResponseTemplates } from './models'; +import { + Users, + Brands, + EmailTemplates, + ResponseTemplates, + ConversationMessages, + Conversations, +} from './models'; +import { CONVERSATION_STATUSES } from '../data/constants'; export const userFactory = (params = {}) => { const user = new Users({ @@ -48,3 +56,33 @@ export const responseTemplateFactory = (params = {}) => { return responseTemplate.save(); }; + +export const conversationFactory = (params = {}) => { + const conversation = new Conversations({ + content: params.content || faker.lorem.sentence(), + customerId: params.customerId || Random.id(), + integrationId: params.integrationId || Random.id(), + status: CONVERSATION_STATUSES.NEW, + }); + + return conversation.save(); +}; + +export const conversationMessageFactory = (params = {}) => { + const conversationMessage = new ConversationMessages({ + content: params.content || faker.random.word(), + attachments: {}, + mentionedUserIds: params.mentionedUserIds || [Random.id()], + conversationId: params.conversationId || Random.id(), + internal: params.internal || true, + customerId: params.customerId || Random.id(), + userId: params.userId || Random.id(), + createdAt: new Date(), + isCustomerRead: params.isCustomerRead || true, + engageData: params.engageData || {}, + formWidgetData: params.formWidgetData || {}, + facebookData: params.facebookData || {}, + }); + + return conversationMessage.save(); +}; diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index 2ccd331e1..92178a18f 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -1,17 +1,25 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +import { CONVERSATION_STATUSES } from '../../data/constants'; const ConversationSchema = mongoose.Schema({ _id: { type: String, unique: true, default: () => Random.id() }, content: String, - integrationId: String, + integrationId: { + type: String, + required: true, + }, customerId: String, userId: String, assignedUserId: String, participatedUserIds: [String], readUserIds: [String], createdAt: Date, - status: String, + status: { + type: String, + required: true, + allowedValues: CONVERSATION_STATUSES.ALL_LIST, + }, messageCount: Number, tagIds: [String], @@ -21,6 +29,24 @@ const ConversationSchema = mongoose.Schema({ facebookData: Object, }); +class Conversation { + /** + * Create a conversation + * @param {Object} conversationObj object + * @return {Promise} Newly created conversation object + */ + static createConversation(doc) { + return this.create({ + ...doc, + status: CONVERSATION_STATUSES.NEW, + createdAt: new Date(), + number: Conversations.find().count() + 1, + messageCount: 0, + }); + } +} + +ConversationSchema.loadClass(Conversation); export const Conversations = mongoose.model('conversations', ConversationSchema); const MessageSchema = mongoose.Schema({ @@ -39,4 +65,20 @@ const MessageSchema = mongoose.Schema({ facebookData: Object, }); +class Message { + /** + * Create a message + * @param {Object} messageObj object + * @return {Promise} Newly created message object + */ + static createMessage(doc) { + return this.create({ + ...doc, + createdAt: new Date(), + }); + } +} + +MessageSchema.loadClass(Message); + export const Messages = mongoose.model('conversation_messages', MessageSchema); diff --git a/yarn.lock b/yarn.lock index 5fe574607..7e58feb87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -79,6 +79,10 @@ acorn@^5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.2.tgz#911cb53e036807cf0fa778dc5d370fbd864246d7" +addressparser@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746" + ajv-keywords@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" @@ -894,6 +898,17 @@ buffer-shims@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" +buildmail@3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/buildmail/-/buildmail-3.10.0.tgz#c6826d716e7945bb6f6b1434b53985e029a03159" + dependencies: + addressparser "1.0.1" + libbase64 "0.1.0" + libmime "2.1.0" + libqp "1.1.0" + nodemailer-fetch "1.6.0" + nodemailer-shared "1.1.0" + builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1273,6 +1288,12 @@ diff@^3.2.0: version "3.3.1" resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" +dkim-signer@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dkim-signer/-/dkim-signer-0.2.2.tgz#aa81ec071eeed3622781baa922044d7800e5f308" + dependencies: + libmime "^2.0.3" + doctrine@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" @@ -2064,6 +2085,10 @@ iconv-lite@0.4.13: version "0.4.13" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" +iconv-lite@0.4.15: + version "0.4.15" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" + iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" @@ -2759,6 +2784,30 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +libbase64@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/libbase64/-/libbase64-0.1.0.tgz#62351a839563ac5ff5bd26f12f60e9830bb751e6" + +libmime@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/libmime/-/libmime-2.1.0.tgz#51bc76de2283161eb9051c4bc80aed713e4fd1cd" + dependencies: + iconv-lite "0.4.13" + libbase64 "0.1.0" + libqp "1.1.0" + +libmime@^2.0.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/libmime/-/libmime-2.1.3.tgz#25017ca5ab5a1e98aadbe2725017cf1d48a42a0c" + dependencies: + iconv-lite "0.4.15" + libbase64 "0.1.0" + libqp "1.1.0" + +libqp@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/libqp/-/libqp-1.1.0.tgz#f5e6e06ad74b794fb5b5b66988bf728ef1dedbe8" + lint-staged@^3.6.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-3.6.1.tgz#24423c8b7bd99d96e15acd1ac8cb392a78e58582" @@ -2966,6 +3015,13 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" +mailcomposer@^3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/mailcomposer/-/mailcomposer-3.12.0.tgz#9c5e1188aa8e1c62ec8b86bd43468102b639e8f9" + dependencies: + buildmail "3.10.0" + libmime "2.1.0" + make-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" @@ -3175,6 +3231,16 @@ node-pre-gyp@^0.6.36: tar "^2.2.1" tar-pack "^3.4.0" +nodemailer-fetch@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/nodemailer-fetch/-/nodemailer-fetch-1.6.0.tgz#79c4908a1c0f5f375b73fe888da9828f6dc963a4" + +nodemailer-shared@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/nodemailer-shared/-/nodemailer-shared-1.1.0.tgz#cf5994e2fd268d00f5cf0fa767a08169edb07ec0" + dependencies: + nodemailer-fetch "1.6.0" + nodemon@^1.11.0: version "1.12.1" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.12.1.tgz#996a56dc49d9f16bbf1b78a4de08f13634b3878d" @@ -3955,6 +4021,13 @@ send@0.15.4: range-parser "~1.2.0" statuses "~1.3.1" +sendmail@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/sendmail/-/sendmail-1.2.0.tgz#a731d80ac004c58026d48b8bfc6278b6bbc89cc0" + dependencies: + dkim-signer "^0.2.2" + mailcomposer "^3.12.0" + serve-static@1.12.4: version "1.12.4" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.4.tgz#9b6aa98eeb7253c4eedc4c1f6fdbca609901a961" @@ -4166,6 +4239,10 @@ strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" +strip@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip/-/strip-3.0.0.tgz#750fc933152a7d35af0b7420e651789b914cc35e" + subscriptions-transport-ws@^0.7.3: version "0.7.3" resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.7.3.tgz#15858f03e013e1fc28f8c2d631014ec1548d38f0" From 9d099641ab941f32ffa3d8553a173a490b363e86 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 11 Oct 2017 19:16:54 +0800 Subject: [PATCH 038/318] Add defaultColumnsConfig query in fields --- src/data/resolvers/queries/customers.js | 11 ----------- src/data/resolvers/queries/fields.js | 11 +++++++++++ src/data/schema/customer.js | 7 ------- src/data/schema/field.js | 7 +++++++ 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index a0bfd857a..4caef345f 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -150,15 +150,4 @@ export default { customersTotalCount() { return Customers.find({}).count(); }, - - /** - * Default list columns config - */ - customersListConfig() { - return [ - { name: 'name', label: 'Name', order: 1 }, - { name: 'email', label: 'Email', order: 2 }, - { name: 'phone', label: 'Phone', order: 3 }, - ]; - }, }; diff --git a/src/data/resolvers/queries/fields.js b/src/data/resolvers/queries/fields.js index 1f24ade44..484149f5f 100644 --- a/src/data/resolvers/queries/fields.js +++ b/src/data/resolvers/queries/fields.js @@ -80,4 +80,15 @@ export default { return fields; }, + + /** + * Default list columns config + */ + fieldsDefaultColumnsConfig() { + return [ + { name: 'name', label: 'Name', order: 1 }, + { name: 'email', label: 'Email', order: 2 }, + { name: 'phone', label: 'Phone', order: 3 }, + ]; + }, }; diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index fef74853d..ad72112ae 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -30,12 +30,6 @@ export const types = ` getMessengerCustomData: JSON getTags: [Tag] } - - type CustomerListConfigItem { - name: String - label: String - order: Int - } `; export const queries = ` @@ -44,7 +38,6 @@ export const queries = ` customerDetail(_id: String!): Customer customerListForSegmentPreview(segment: JSON, limit: Int): [Customer] customersTotalCount: Int - customersListConfig: [CustomerListConfigItem] `; const fields = ` diff --git a/src/data/schema/field.js b/src/data/schema/field.js index 497d56b97..e3e91283f 100644 --- a/src/data/schema/field.js +++ b/src/data/schema/field.js @@ -16,11 +16,18 @@ export const types = ` _id: String! order: Int! } + + type ColumnConfigItem { + name: String + label: String + order: Int + } `; export const queries = ` fields(contentType: String!, contentTypeId: String): [Field] fieldsCombinedByContentType: JSON + fieldsDefaultColumnsConfig: [ColumnConfigItem] `; const commonFields = ` From cedc066710aa71301af8a2caa5857211ccd7067f Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Wed, 11 Oct 2017 20:34:25 +0800 Subject: [PATCH 039/318] #13 Refactor notification model, mutations and related util helpers --- package.json | 2 + src/__tests__/notificationMutations.test.js | 228 +++++++-------- src/__tests__/notificationQueries.test.js | 3 +- src/data/constants.js | 19 +- .../mutations/{channel.js => channels.js} | 21 +- .../resolvers/mutations/{form.js => forms.js} | 68 ++++- src/data/resolvers/mutations/index.js | 16 +- .../{integration.js => integrations.js} | 49 +++- .../{notification.js => notifications.js} | 24 +- src/data/resolvers/queries/notifications.js | 4 +- src/data/schema/form.js | 10 +- src/data/schema/notification.js | 14 +- src/data/utils.js | 126 +++++++-- src/db/factories.js | 27 +- src/db/models/Channels.js | 66 ++--- src/db/models/Forms.js | 111 ++++++-- src/db/models/Integrations.js | 267 ++++++++++++------ src/db/models/Notifications.js | 69 ++++- yarn.lock | 10 +- 19 files changed, 769 insertions(+), 365 deletions(-) rename src/data/resolvers/mutations/{channel.js => channels.js} (73%) rename src/data/resolvers/mutations/{form.js => forms.js} (58%) rename src/data/resolvers/mutations/{integration.js => integrations.js} (68%) rename src/data/resolvers/mutations/{notification.js => notifications.js} (54%) diff --git a/package.json b/package.json index d629e8f27..61505777f 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,11 @@ "graphql-server-module-graphiql": "^0.8.2", "graphql-subscriptions": "^0.4.3", "graphql-tools": "^1.0.0", + "handlebars": "^4.0.10", "meteor-random": "^0.0.3", "moment": "^2.18.1", "mongoose": "^4.9.2", + "mongoose-type-email": "^1.0.5", "nodemailer": "^4.1.3", "passport": "^0.4.0", "passport-anonymous": "^1.0.1", diff --git a/src/__tests__/notificationMutations.test.js b/src/__tests__/notificationMutations.test.js index 66a0bd182..807caf07b 100644 --- a/src/__tests__/notificationMutations.test.js +++ b/src/__tests__/notificationMutations.test.js @@ -1,11 +1,11 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ - import { connect, disconnect } from '../db/connection'; import { Notifications, NotificationConfigurations, Users } from '../db/models'; import mutations from '../data/resolvers/mutations'; -import { userFactory, configurationFactory } from '../db/factories'; +import { userFactory, notificationConfigurationFactory } from '../db/factories'; import { sendNotification } from '../data/utils'; +import { MODULES } from '../data/constants'; beforeAll(() => connect()); afterAll(() => disconnect()); @@ -25,10 +25,10 @@ describe('Notification tests', () => { Users.remove({}); }); - test('model exception', async () => { + test('check for error in model creation', async () => { expect.assertions(1); - await configurationFactory({ + await notificationConfigurationFactory({ user: _user2._id, notifType: 'channelMembersChange', isAllowed: false, @@ -37,7 +37,6 @@ describe('Notification tests', () => { // Create notification let doc = { notifType: 'channelMembersChange', - createdUser: _user._id, title: 'new Notification title', content: 'new Notification content', link: 'new Notification link', @@ -45,38 +44,33 @@ describe('Notification tests', () => { }; try { - await Notifications.createNotification(doc); + await Notifications.createNotification(doc, _user._id); } catch (e) { expect(e.message).toEqual('Configuration does not exist'); } }); test('model create, update, remove', async () => { - await configurationFactory({ - user: _user2._id, - notifType: 'channelMembersChange', - }); + // Create notification ================ - // Create notification let doc = { - notifType: 'channelMembersChange', - createdUser: _user._id, + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, title: 'new Notification title', content: 'new Notification content', link: 'new Notification link', receiver: _user2._id, }; - let notification = await Notifications.createNotification(doc); + let notification = await Notifications.createNotification(doc, _user._id); expect(notification.notifType).toEqual(doc.notifType); - expect(notification.createdUser).toEqual(doc.createdUser); + expect(notification.createdUser).toEqual(_user._id); expect(notification.title).toEqual(doc.title); expect(notification.content).toEqual(doc.content); expect(notification.link).toEqual(doc.link); expect(notification.receiver).toEqual(doc.receiver); - // Update notification + // Update notification =============== let user3 = await userFactory({}); doc = { @@ -97,12 +91,14 @@ describe('Notification tests', () => { expect(notification.link).toEqual(doc.link); expect(notification.receivers).toEqual(doc.receivers); - // Mark as read + // check method markAsRead ============= await Notifications.markAsRead([notification._id]); + notification = await Notifications.findOne({ _id: notification._id }); + expect(notification.isRead).toEqual(true); - // Remove notification + // remove notification ================= await Notifications.removeNotification(notification._id); expect(await Notifications.find({}).count()).toEqual(0); @@ -112,44 +108,48 @@ describe('Notification tests', () => { }); describe('NotificationConfiguration model tests', async () => { - test('model tests', async () => { - // New notification configuration + test('test if model methods are working correctly', async () => { + // creating new notification configuration ========== const user = await userFactory({}); + const doc = { - notifType: 'conversationAddMessage', + notifType: MODULES.CONVERSATION_ADD_MESSAGE, isAllowed: true, - user: user._id, }; let notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration( doc, + user, ); expect(notificationConfigurations.notifType).toEqual(doc.notifType); expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); - expect(notificationConfigurations.user).toEqual(doc.user); + expect(notificationConfigurations.user).toEqual(user._id); - // Another notification configuration - doc.notifType = 'conversationAssigneeChange'; + // creating another notification configuration ============ + doc.notifType = MODULES.CONVERSATION_ASSIGNEE_CHANGE; - notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration(doc); + notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration( + doc, + user, + ); expect(notificationConfigurations.notifType).toEqual(doc.notifType); - expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); - expect(notificationConfigurations.user).toEqual(doc.user); + expect(notificationConfigurations.user).toEqual(user._id); - // Change notification + // Changing the last added notification ========================= doc.isAllowed = false; - notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration(doc); + notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration( + doc, + user, + ); - expect(notificationConfigurations.notifType).toEqual(doc.notifType); expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); - expect(notificationConfigurations.user).toEqual(doc.user); }); }); -describe('test mutations', () => { +describe('testing mutations', () => { beforeEach(() => {}); afterEach(async () => { @@ -157,124 +157,100 @@ describe('test mutations', () => { await NotificationConfigurations.remove({}); }); - test('mutations', async () => { - // notification confuration test - const user = await userFactory({}); + test('test if `logging required` error is working as intended', () => { + expect.assertions(2); - const doc = { - notifType: 'conversationAddMessage', - isAllowed: true, - user: user._id, - }; + // Login required + expect(() => mutations.notificationsSaveConfig(null, {}, {})).toThrowError('Login required'); - let notificationConfigurations = await mutations.notificationsSaveConfig(null, doc); - expect(notificationConfigurations.notifType).toEqual(doc.notifType); + expect(() => mutations.notificationsMarkAsRead(null, {}, {})).toThrowError('Login required'); + }), + test('testing tools.sendNotification method', async () => { + const _user = await userFactory({}); + const _user2 = await userFactory({}); + const _user3 = await userFactory({}); - expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); - expect(notificationConfigurations.user).toEqual(doc.user); + // Try to send notifications when there is config not allowing it ========= + await notificationConfigurationFactory({ + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + isAllowed: false, + user: _user._id, + }); - // Another notification configuration - doc.notifType = 'conversationAssigneeChange'; + await notificationConfigurationFactory({ + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + isAllowed: false, + user: _user2._id, + }); - notificationConfigurations = await mutations.notificationsSaveConfig(null, doc); + await notificationConfigurationFactory({ + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + isAllowed: false, + user: _user3._id, + }); - expect(notificationConfigurations.notifType).toEqual(doc.notifType); - expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); - expect(notificationConfigurations.user).toEqual(doc.user); + const doc = { + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + createdUser: _user._id, + title: 'new Notification title', + content: 'new Notification content', + link: 'new Notification link', + receivers: [_user._id, _user2._id, _user3._id], + }; - // Change notification - doc.isAllowed = false; + await sendNotification(doc); + let notifications = await Notifications.find({}); - notificationConfigurations = await mutations.notificationsSaveConfig(null, doc); + expect(notifications.length).toEqual(0); - expect(notificationConfigurations.notifType).toEqual(doc.notifType); - expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); - expect(notificationConfigurations.user).toEqual(doc.user); - }); + // Send notifications when there is config allowing it ==================== + await NotificationConfigurations.update({}, { isAllowed: true }, { multi: true }); - test('send notifications', async () => { - const _user = await userFactory({}); - const _user2 = await userFactory({}); - const _user3 = await userFactory({}); + await sendNotification(doc); - // Try to send notifications when there is config not allowing it - const doc = { - notifType: 'channelMembersChange', - createdUser: _user._id, - title: 'new Notification title', - content: 'new Notification content', - link: 'new Notification link', - receivers: [_user._id, _user2._id, _user3._id], - }; - - await NotificationConfigurations.createOrUpdateConfiguration({ - notifType: 'channelMembersChange', - isAllowed: false, - user: _user._id, - }); + notifications = await Notifications.find({}); - await NotificationConfigurations.createOrUpdateConfiguration({ - notifType: 'channelMembersChange', - isAllowed: false, - user: _user2._id, - }); + expect(notifications.length).toEqual(3); - await NotificationConfigurations.createOrUpdateConfiguration({ - notifType: 'channelMembersChange', - isAllowed: false, - user: _user3._id, - }); + expect(notifications[0].notifType).toEqual(doc.notifType); + expect(notifications[0].createdUser).toEqual(doc.createdUser); + expect(notifications[0].title).toEqual(doc.title); + expect(notifications[0].content).toEqual(doc.content); + expect(notifications[0].link).toEqual(doc.link); + expect(notifications[0].receiver).toEqual(_user._id); - await sendNotification(doc); + expect(notifications[1].receiver).toEqual(_user2._id); - let notifications = await Notifications.find({}); + expect(notifications[2].receiver).toEqual(_user3._id); + }); - expect(notifications.length).toEqual(0); + test('testing if notification configuration is saved and updated successfully', async () => { + NotificationConfigurations.createOrUpdateConfiguration = jest.fn(); - // Send notifications when there is config allowing it - await NotificationConfigurations.createOrUpdateConfiguration({ - notifType: 'channelMembersChange', - isAllowed: true, - user: _user._id, - }); + const user = await userFactory({}); - await NotificationConfigurations.createOrUpdateConfiguration({ - notifType: 'channelMembersChange', + const doc = { + notifType: 'conversationAddMessage', isAllowed: true, - user: _user2._id, - }); + user: user._id, + }; - await NotificationConfigurations.createOrUpdateConfiguration({ - notifType: 'channelMembersChange', - isAllowed: true, - user: _user3._id, - }); + await mutations.notificationsSaveConfig(null, doc, { user }); - await sendNotification(doc); + expect(NotificationConfigurations.createOrUpdateConfiguration).toBeCalledWith(doc, user); + expect(NotificationConfigurations.createOrUpdateConfiguration.mock.calls.length).toBe(1); + }); - notifications = await Notifications.find({}); + test('testing if notifications are being marked as read successfully', async () => { + Notifications.markAsRead = jest.fn(); - expect(notifications.length).toEqual(3); + const user = await userFactory({}); - expect(notifications[0].notifType).toEqual(doc.notifType); - expect(notifications[0].createdUser).toEqual(doc.createdUser); - expect(notifications[0].title).toEqual(doc.title); - expect(notifications[0].content).toEqual(doc.content); - expect(notifications[0].link).toEqual(doc.link); - expect(notifications[0].receiver).toEqual(_user._id); + const args = { ids: ['11111', '22222'] }; - expect(notifications[1].notifType).toEqual(doc.notifType); - expect(notifications[1].createdUser).toEqual(doc.createdUser); - expect(notifications[1].title).toEqual(doc.title); - expect(notifications[1].content).toEqual(doc.content); - expect(notifications[1].link).toEqual(doc.link); - expect(notifications[1].receiver).toEqual(_user2._id); + await mutations.notificationsMarkAsRead(null, args, { user }); - expect(notifications[2].notifType).toEqual(doc.notifType); - expect(notifications[2].createdUser).toEqual(doc.createdUser); - expect(notifications[2].title).toEqual(doc.title); - expect(notifications[2].content).toEqual(doc.content); - expect(notifications[2].link).toEqual(doc.link); - expect(notifications[2].receiver).toEqual(_user3._id); + expect(Notifications.markAsRead).toBeCalledWith(args['ids']); + expect(Notifications.markAsRead.mock.calls.length).toBe(1); }); }); diff --git a/src/__tests__/notificationQueries.test.js b/src/__tests__/notificationQueries.test.js index feae7083a..fb54ebb2f 100644 --- a/src/__tests__/notificationQueries.test.js +++ b/src/__tests__/notificationQueries.test.js @@ -8,8 +8,9 @@ beforeAll(() => connect()); afterAll(() => disconnect()); describe('notification query test', () => { - test('notification query test', () => { + test('test of getting notification list with success', () => { const modules = queries.notificationsModules(); + expect(modules).toEqual(MODULE_LIST); }); }); diff --git a/src/data/constants.js b/src/data/constants.js index c504d8e00..4a8b99c40 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -42,9 +42,16 @@ export const KIND_CHOICES = { ALL_LIST: ['messenger', 'form', 'twitter', 'facebook'], }; -export const MODULE_LIST = [ - 'channelMembersChange', - 'conversationAddMessage', - 'conversationAssigneeChange', - 'conversationStateChange', -]; +// module constants +export const MODULES = { + CHANNEL_MEMBERS_CHANGE: 'channelMembersChange', + CONVERSATION_ADD_MESSAGE: 'conversationAddMessage', + CONVERSATION_ASSIGNEE_CHANGE: 'conversationAssigneeChange', + CONVERSATION_STATE_CHANGE: 'conversationStateChange', + ALL: [ + 'channelMembersChange', + 'conversationAddMessage', + 'conversationAssigneeChange', + 'conversationStateChange', + ], +}; diff --git a/src/data/resolvers/mutations/channel.js b/src/data/resolvers/mutations/channels.js similarity index 73% rename from src/data/resolvers/mutations/channel.js rename to src/data/resolvers/mutations/channels.js index ad692dfb5..a27d5a0d0 100644 --- a/src/data/resolvers/mutations/channel.js +++ b/src/data/resolvers/mutations/channels.js @@ -12,8 +12,13 @@ export default { * @param {String} doc.userId * @return {Promise} returns channel object * @throws {Error} throws apollo level validation errors + * @throws {Error} throws error if user is not logged in */ - async channelsCreate(root, doc) { + async channelsCreate(root, doc, { user }) { + if (!user) { + throw new Error('Login required'); + } + const channel = Channels.createChannel(doc); sendChannelNotifications({ @@ -36,8 +41,13 @@ export default { * @param {String} doc.userId * @return {Promise} returns null * @throws {Error} throws apollo level validation errors + * @throws {Error} throws error if user is not logged in */ - channelsEdit(root, { _id, ...doc }) { + channelsEdit(root, { _id, ...doc }, { user }) { + if (!user) { + throw new Error('Login required'); + } + sendChannelNotifications({ channelId: _id, memberIds: doc.memberIds, @@ -52,8 +62,13 @@ export default { * @param {Object} * @param {String} id * @return {Promise} null + * @throws {Error} throws error if user is not logged in */ - channelsRemove(root, { _id }) { + channelsRemove(root, { _id }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return Channels.removeChannel(_id); }, }; diff --git a/src/data/resolvers/mutations/form.js b/src/data/resolvers/mutations/forms.js similarity index 58% rename from src/data/resolvers/mutations/form.js rename to src/data/resolvers/mutations/forms.js index 80645faf2..200cb85c8 100644 --- a/src/data/resolvers/mutations/form.js +++ b/src/data/resolvers/mutations/forms.js @@ -1,16 +1,22 @@ import { Forms, FormFields } from '../../../db/models'; + export default { /** * Create a new form * @param {Object} - * @param {String} doc.title - * @param {String} doc.description - * @param {String} doc.createdUserId + * @param {String} args2.title + * @param {String} args2.description + * @param {String} args3.user * @return {Promise} returns the form * @throws {Error} apollo level error based on validation + * @throws {Error} throws error if user is not logged in */ - formsCreate(root, doc) { - return Forms.createForm(doc); + formsCreate(root, doc, { user }) { + if (!user) { + throw new Error('Login required'); + } + + return Forms.createForm(doc, user); }, /** @@ -19,10 +25,16 @@ export default { * @param {String} doc._id * @param {String} doc.title * @param {String} doc.description + * @param {String} args3.user * @return {Promise} returns null * @throws {Error} apollo level error based on validation + * @throws {Error} throws error if user is not logged in */ - formsEdit(root, { _id, ...doc }) { + formsEdit(root, { _id, ...doc }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return Forms.updateForm(_id, doc); }, @@ -31,9 +43,14 @@ export default { * @param {Object} * @param {String} _id * @return {Promise} null - * @throws apollo level error based on validation + * @throws {Error} apollo level error based on validation + * @throws {Error} throws error if user is not logged in */ - formsRemove(root, { _id }) { + formsRemove(root, { _id }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return Forms.removeForm(_id); }, @@ -49,8 +66,13 @@ export default { * @param {Boolean} args.isRequired * @return {Promise} return Promise(null) * @throws {Error} throws apollo error based on validation + * @throws {Error} throws error if user is not logged in */ - formsAddFormField(root, { formId, ...formFieldDoc }) { + formsAddFormField(root, { formId, ...formFieldDoc }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return FormFields.createFormField(formId, formFieldDoc); }, @@ -64,8 +86,13 @@ export default { * @param {Boolean} args.isRequired * @return {Promise} return Promise(null) * @throws {Error} throws apollo error based on validation + * @throws {Error} throws error if user is not logged in */ - formsEditFormField(root, { _id, ...formFieldDoc }) { + formsEditFormField(root, { _id, ...formFieldDoc }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return FormFields.updateFormField(_id, formFieldDoc); }, @@ -75,8 +102,13 @@ export default { * @param {String} _id * @return {Promise} null * @throws {Error} throws apollo error based on validation + * @throws {Error} throws error if user is not logged in */ - formsRemoveFormField(root, { _id }) { + formsRemoveFormField(root, { _id }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return FormFields.removeFormField(_id); }, @@ -87,8 +119,13 @@ export default { * @param {String} args.orderDics.order * @return {Promise} null * @throws {Error} throws apollo error based on validation + * @throws {Error} throws error if user is not logged in */ - formsUpdateFormFieldsOrder(root, { orderDics }) { + formsUpdateFormFieldsOrder(root, { orderDics }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return Forms.updateFormFieldsOrder(orderDics); }, @@ -98,8 +135,13 @@ export default { * @param {String} args._id * @return {Promise} returns form object * @throws {Error} throws apollo error based on validation + * @throws {Error} throws error if user is not logged in */ - formsDuplicate(root, { _id }) { + formsDuplicate(root, { _id }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return Forms.duplicate(_id); }, }; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index 3e2349548..30c894044 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -2,18 +2,18 @@ import conversation from './conversation'; import brands from './brands'; import emailTemplate from './emailTemplate'; import responseTemplate from './responseTemplate'; -import channel from './channel'; -import form from './form'; -import integration from './integration'; -import notification from './notification'; +import channels from './channels'; +import forms from './forms'; +import integrations from './integrations'; +import notifications from './notifications'; export default { ...conversation, ...brands, ...emailTemplate, ...responseTemplate, - ...channel, - ...form, - ...integration, - ...notification, + ...channels, + ...forms, + ...integrations, + ...notifications, }; diff --git a/src/data/resolvers/mutations/integration.js b/src/data/resolvers/mutations/integrations.js similarity index 68% rename from src/data/resolvers/mutations/integration.js rename to src/data/resolvers/mutations/integrations.js index 8693446ab..99b291679 100644 --- a/src/data/resolvers/mutations/integration.js +++ b/src/data/resolvers/mutations/integrations.js @@ -8,8 +8,13 @@ export default { * @param {String} doc.brandId * @return {Promise} returns the messenger integration * @throws {Error} apollo level error based on validation + * @throws {Error} throws error if user is not logged in */ - integrationsCreateMessengerIntegration(root, doc) { + integrationsCreateMessengerIntegration(root, doc, { user }) { + if (!user) { + throw new Error('Login required'); + } + return Integrations.createMessengerIntegration(doc); }, @@ -21,8 +26,13 @@ export default { * @param {String} args.brandId * @return {Promise} returns null * @throws {Error} apollo level error based on validation + * @throws {Error} throws error if user is not logged in */ - integrationsEditMessengerIntegration(root, { id, ...fields }) { + integrationsEditMessengerIntegration(root, { id, ...fields }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return Integrations.updateMessengerIntegration(id, fields); }, @@ -35,8 +45,13 @@ export default { * @param {String} args.logo * @return {Promise} returns null * @throws {Error} apollo level error based on validation + * @throws {Error} throws error if user is not logged in */ - integrationsSaveMessengerAppearanceData(root, { id, uiOptions }) { + integrationsSaveMessengerAppearanceData(root, { id, uiOptions }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return Integrations.saveMessengerAppearanceData(id, uiOptions); }, @@ -56,8 +71,13 @@ export default { * @param {String} args.thankYouMessage * @return {Promise} returns null * @throws {Error} apollo level error based on validation + * @throws {Error} throws error if user is not logged in */ - integrationsSaveMessengerConfigs(root, { id, messengerData }) { + integrationsSaveMessengerConfigs(root, { id, messengerData }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return Integrations.saveMessengerConfigs(id, messengerData); }, @@ -70,8 +90,13 @@ export default { * @param {Object} doc.formData * @return {Promise} returns the messenger integration * @throws {Error} apollo level error based on validation + * @throws {Error} throws error if user is not logged in */ - integrationsCreateFormIntegration(root, doc) { + integrationsCreateFormIntegration(root, doc, { user }) { + if (!user) { + throw new Error('Login required'); + } + return Integrations.createFormIntegration(doc); }, @@ -84,8 +109,13 @@ export default { * @param {Object} doc.formData * @return {Promise} returns null * @throws {Error} apollo level error based on validation + * @throws {Error} throws error if user is not logged in */ - integrationsEditFormIntegration(root, { id, ...doc }) { + integrationsEditFormIntegration(root, { id, ...doc }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return Integrations.updateFormIntegration(id, doc); }, @@ -95,8 +125,13 @@ export default { * @param {String} args.id * @return {Promise} returns the messenger integration * @throws {Error} apollo level error based on validation + * @throws {Error} throws error if user is not logged in */ - integrationsRemove(root, { id }) { + integrationsRemove(root, { id }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return Integrations.removeIntegration({ _id: id }); }, }; diff --git a/src/data/resolvers/mutations/notification.js b/src/data/resolvers/mutations/notifications.js similarity index 54% rename from src/data/resolvers/mutations/notification.js rename to src/data/resolvers/mutations/notifications.js index 37f24a030..b3e253ad6 100644 --- a/src/data/resolvers/mutations/notification.js +++ b/src/data/resolvers/mutations/notifications.js @@ -3,15 +3,20 @@ import { NotificationConfigurations, Notifications } from '../../../db/models'; export default { /** * Save notification configuration - * @param {Object} - * @param {String} args.notifType - * @param {Boolean} args.isAllowed - * @param {String} args.user + * @param {Object} args1 + * @param {String} args2.notifType + * @param {Boolean} args2.isAllowed + * @param {String} args3.user * @return {Promise} returns notification promise * @throws {Error} apollo level error based on validation + * @throws {Error} throws error if user is not logged in */ - notificationsSaveConfig(root, doc) { - return NotificationConfigurations.createOrUpdateConfiguration(doc); + notificationsSaveConfig(root, doc, { user }) { + if (!user) { + throw new Error('Login required'); + } + + return NotificationConfigurations.createOrUpdateConfiguration(doc, user); }, /** @@ -20,8 +25,13 @@ export default { * @param {String} args.ids * @return {Promise} returns the messenger integration * @throws {Error} apollo level error based on validation + * @throws {Error} throws error if user is not logged in */ - notificationsMarkAsRead(root, { ids }) { + notificationsMarkAsRead(root, { ids }, { user }) { + if (!user) { + throw new Error('Login required'); + } + return Notifications.markAsRead(ids); }, }; diff --git a/src/data/resolvers/queries/notifications.js b/src/data/resolvers/queries/notifications.js index 0088ec6d8..c29cc112d 100644 --- a/src/data/resolvers/queries/notifications.js +++ b/src/data/resolvers/queries/notifications.js @@ -1,4 +1,4 @@ -import { MODULE_LIST } from '../../constants'; +import { MODULES } from '../../constants'; export default { /** @@ -7,6 +7,6 @@ export default { * @return {Promise} module list */ notificationsModules() { - return MODULE_LIST; + return MODULES.ALL; }, }; diff --git a/src/data/schema/form.js b/src/data/schema/form.js index cec0b7422..55a2be151 100644 --- a/src/data/schema/form.js +++ b/src/data/schema/form.js @@ -29,15 +29,9 @@ export const types = ` `; export const mutations = ` - formsCreate( - title: String!, - description: String, - createdUserId: String!): Form + formsCreate(title: String!, description: String): Form - formsEdit( - _id: String!, - title: String!, - description: String): Boolean + formsEdit(_id: String!, title: String!, description: String): Boolean formsRemove(_id: String!): Boolean diff --git a/src/data/schema/notification.js b/src/data/schema/notification.js index d6e75091a..b33570ae7 100644 --- a/src/data/schema/notification.js +++ b/src/data/schema/notification.js @@ -19,16 +19,12 @@ export const types = ` } `; +export const queries = ` + notificationsModules(ids: [String]) : [String] +`; + export const mutations = ` - notificationsSaveConfig ( - notifType: String, - isAllowed: Boolean, - user: String, - ): NotificationConfiguration + notificationsSaveConfig (notifType: String, isAllowed: Boolean): NotificationConfiguration notificationsMarkAsRead ( ids: [String]! ) : Boolean `; - -export const queries = ` - notificationsModules(ids: [String]) : [String] -`; diff --git a/src/data/utils.js b/src/data/utils.js index c3ce2651d..74dea0aaa 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -1,8 +1,58 @@ import nodemailer from 'nodemailer'; -import { Channels, Notifications } from '../db/models'; +import Handlebars from 'handlebars'; +import fs from 'fs'; +import { MODULES } from './constants'; +import { Channels, Notifications, Users } from '../db/models'; -export const sendEmail = ({ toEmails, fromEmail, title, content }) => { - const { MAIL_SERVICE, MAIL_USER, MAIL_PASS } = process.env; +/** + * Read template file with via utf-8 + * @param {String} assetPath + * @return {String} file content + */ +const getTemplateContent = assetPath => { + // TODO: test this method + fs.readFile(assetPath, 'utf8', (err, data) => { + if (err) { + throw err; + } + + return data; + }); +}; + +/** + * SendEmail template helper + * @param {Object} data data + * @param {String} templateName + * @return email with template as text + */ +const applyTemplate = async (data, templateName) => { + let template = await getTemplateContent(`emailTemplates/${templateName}.html`); + + template = Handlebars.compile(template); + + return template(data); +}; + +/** + * Send email + * @param {Array} args.toEmails + * @param {String} args.fromEmail + * @param {String} args.title + * @param {String} args.templateArgs.name + * @param {Object} args.templateArgs.data + * @param {Boolean} args.isCustom + * @return {Promise} null +*/ +export const sendEmail = async ({ toEmails, fromEmail, title, templateArgs }) => { + // TODO: test this method + const { MAIL_SERVICE, MAIL_USER, MAIL_PASS, NODE_ENV } = process.env; + const isTest = NODE_ENV == 'test'; + + // do not send email it is running in test mode + if (isTest) { + return; + } const transporter = nodemailer.createTransport({ service: MAIL_SERVICE, @@ -12,35 +62,55 @@ export const sendEmail = ({ toEmails, fromEmail, title, content }) => { }, }); - toEmails.forEach(toEmail => { + const { isCustom, data, name } = templateArgs; + + // generate email content by given template + const content = await applyTemplate(data, name); + + let text = ''; + + if (isCustom) { + text = content; + } else { + text = await applyTemplate({ content }, 'base'); + } + + return toEmails.map(toEmail => { const mailOptions = { from: fromEmail, to: toEmail, subject: title, - text: content, + text, }; - transporter.sendMail(mailOptions, (error, info) => { + return transporter.sendMail(mailOptions, (error, info) => { console.log(error); // eslint-disable-line console.log(info); // eslint-disable-line }); }); }; -export const sendChannelNotifications = async ({ channelId, _memberIds, userId }) => { - const memberIds = _memberIds || []; +/** + * Send notification to all members of this channel except the sender + * @param {String} channelId + * @param {Array} memberIds + * @param {String} userId + */ +export const sendChannelNotifications = async ({ channelId, memberIds, userId }) => { + memberIds = memberIds || []; + const channel = await Channels.findOne({ _id: channelId }); const content = `You have invited to '${channel.name}' channel.`; return sendNotification({ createdUser: userId, - notifType: 'channelMembersChange', + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, title: content, content, link: `/inbox/${channel._id}`, - // Exclude current user + // exclude current user receivers: memberIds.filter(id => id !== userId), }); }; @@ -52,24 +122,42 @@ export const sendChannelNotifications = async ({ channelId, _memberIds, userId } * @param {String} doc.title * @param {String} doc.content * @param {String} doc.link - * @param {Array} doc.receivers Array of userIds - * @return null + * @param {Array} doc.receivers Array of user ids + * @return {Promise} */ -export const sendNotification = async ({ receivers, ...doc }) => { - // Inserting entry to every receiver +export const sendNotification = async ({ createdUser, receivers, ...doc }) => { + // collecting emails + const recipients = await Users.find({ _id: { $in: receivers } }); + + // collect recipient emails + const toEmails = recipients.map( + recipient => !(recipient.details && recipient.details.getNotificationByEmail === false), + ); + + // loop through receiver ids for (const receiverId of receivers) { doc.receiver = receiverId; try { - // Create notification - await Notifications.createNotification(doc); - // TODO: Implement sendEmail + // send notification + await Notifications.createNotification(doc, createdUser); } catch (e) { + // Any other error is serious if (e.message != 'Configuration does not exist') { - return e; + throw e; } } } - return; + return sendEmail({ + toEmails, + fromEmail: 'no-reply@erxes.io', + title: 'Notification', + template: { + name: 'notification', + data: { + notification: doc, + }, + }, + }); }; diff --git a/src/db/factories.js b/src/db/factories.js index e5178230b..787a2812a 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -1,5 +1,6 @@ import faker from 'faker'; import Random from 'meteor-random'; +import { MODULES } from '../data/constants'; import { Users, @@ -11,6 +12,7 @@ import { Forms, FormFields, NotificationConfigurations, + Notifications, } from './models'; export const userFactory = (params = {}) => { @@ -106,16 +108,29 @@ export const formFieldFactory = (formId, params) => { }); }; -export const configurationFactory = params => { +export const notificationConfigurationFactory = params => { let { isAllowed } = params; if (isAllowed == null) { isAllowed = true; } - return NotificationConfigurations.createOrUpdateConfiguration({ - user: params.user || userFactory({}), - notifType: params.notifType || faker.random.word(), - // which module's type it is. For example: indocuments - isAllowed: isAllowed, + return NotificationConfigurations.createOrUpdateConfiguration( + { + notifType: params.notifType || MODULES.CHANNEL_MEMBERS_CHANGE, + // which module's type it is. For example: indocuments + isAllowed, + }, + params.user || userFactory({}), + ); +}; + +export const notificationFactory = params => { + return Notifications.createNotification({ + notifType: params.notifType || MODULES.CHANNEL_MEMBERS_CHANGE, + createdUser: params.createdUser || userFactory({}), + title: params.title || 'new Notification title', + content: params.content || 'new Notification content', + link: params.link || 'new Notification link', + receiver: params.receiver || userFactory({}), }); }; diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index bc0b60c8e..ff161a4a0 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -2,44 +2,24 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; import { createdAtModifier } from '../plugins'; -function ChannelCreationException(message) { - this.message = message; - this.value = 'channel.create.exception'; - this.toString = `${this.value} - ${this.value}`; -} - +// schema for channel document const ChannelSchema = mongoose.Schema({ _id: { type: String, default: () => Random.id(), }, - name: { - type: String, - required: true, - }, + name: String, description: String, - // TODO: Check if regex id is available for use - integrationIds: { - type: [String], - }, - // TODO: Check if regex id is available for use - memberIds: { - type: [String], - }, - userId: { - type: String, - }, - conversationCount: { - type: Number, - }, - openConversationCount: { - type: Number, - }, + integrationIds: [String], + memberIds: [String], + userId: String, + conversationCount: Number, + openConversationCount: Number, }); class Channel { /** - * + * Pre save filter method that adds userId to memberIds if it does not contain it */ static preSave(doc) { doc.memberIds = doc.memberIds || []; @@ -50,30 +30,50 @@ class Channel { } /** - * Create a new channel, - * adds `userId` to the `memberIds` if it doesn't contain it - * @param {Object} args - * @return {Promise} Newly created channel obj + * Create a new channel document + * @param {String} doc.name + * @param {String} doc.description + * @param {Array} doc.integrationIds + * @param {Array} doc.memberIds + * @param {String} doc.userId + * @return {Promise} Newly created channel document */ static createChannel(doc) { const { userId } = doc; if (!userId) { - throw new ChannelCreationException('userId must be supplied'); + throw new Error('userId must be supplied'); } this.preSave(doc); + doc.conversationCount = 0; doc.openConversationCount = 0; return this.create(doc); } + /** + * Updates a channel document + * adds `userId` to the `memberIds` if it doesn't contain it + * @param {String} doc.name + * @param {String} doc.description + * @param {Array} doc.integrationIds + * @param {Array} doc.memberIds + * @param {String} doc.userId + * @return {Promise} + */ static updateChannel(_id, doc) { this.preSave(doc); + return this.update({ _id }, { $set: doc }, { runValidators: true }); } + /** + * Removes a channel document + * @param {String} _id + * @return {Promise} + */ static removeChannel(_id) { return this.remove({ _id }); } diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index 6a423edb3..1379a8944 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -2,6 +2,7 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; import Integrations from './Integrations'; +// schema for form document const FormSchema = mongoose.Schema({ _id: { type: String, @@ -11,40 +12,69 @@ const FormSchema = mongoose.Schema({ description: String, code: String, createdUserId: String, - createdDate: Date, + createdDate: { + type: Date, + default: Date.now, + }, }); class Form { + /** + * Generates a randomly generated and unique 6 letter code + * @return {String} random code + */ static async generateCode() { - // generate code automatically - let code = Random.id().substr(0, 6); - let foundForm = await Forms.findOne({ code }); + let code; + let foundForm = true; - while (foundForm) { + do { code = Random.id().substr(0, 6); + foundForm = await Forms.findOne({ code }); - } + } while (foundForm); return code; } - static async createForm(doc) { - const { createdUserId } = doc; - + /** + * Creates a form document + * @param {String} doc.title + * @param {String} doc.description + * @param {Date} doc.createdDate + * @param {String} createdUserId + * @return {Object} returns Form document + * @throws {Error} throws Error if createdUserId is not supplied + */ + static async createForm(doc, createdUserId) { if (!createdUserId) { throw new Error('createdUserId must be supplied'); } doc.code = await this.generateCode(); + doc.createdDate = new Date(); return this.create(doc); } - static updateForm(_id, doc) { - return this.update({ _id }, { $set: doc }, { runValidators: true }); + /** + * Updates a form document + * @param {String} _id + * @param {String} args.title + * @param {String} args.description + * @return {Object} returns Form document + * @throws {Error} + */ + static updateForm(_id, { title, description }) { + return this.update({ _id }, { $set: { title, description } }, { runValidators: true }); } + /** + * Remove a form + * @param {String} _id + * @return {Promise} + * @throws {Error} throws Error if this form has fields or if used in an integration + */ static async removeForm(_id) { const fieldCount = await FormFields.find({ formId: _id }).count(); @@ -61,13 +91,24 @@ class Form { return this.remove({ _id }); } + /** + * Update order fields of form fields + * @param {String} orderDics[]._id + * @param {String} orderDics[].order + * @return {Null} + */ static async updateFormFieldsOrder(orderDics) { // update each field's order for (let orderDic of orderDics) { - await FormFields.updateFormField(orderDic.id, { order: orderDic.order }); + await FormFields.updateFormField(orderDic._id, { order: orderDic.order }); } } + /** + * Duplicates form and form fields of the form + * @param {String} _id form id + * @return {Object} returns the duplicated copy of the form + */ static async duplicate(_id) { const form = await this.findOne({ _id }); @@ -100,45 +141,65 @@ class Form { FormSchema.loadClass(Form); export const Forms = mongoose.model('forms', FormSchema); +// schema for form fields const FormFieldSchema = mongoose.Schema({ _id: { type: String, default: () => Random.id(), }, - type: String, - validation: String, + type: String, // TODO: change to enum + validation: String, // TODO: check if can be enum text: String, description: String, options: [String], isRequired: Boolean, - formId: { - required: true, - type: String, - }, + formId: String, order: Number, }); class FormField { + /** + * Creates a new form field document + * @param {String} formId id of the form document + * @param {String} doc.type + * @param {String} doc.validation + * @param {String} doc.text + * @param {String} doc.description + * @param {Array} doc.options + * @param {Boolean} doc.isRequired + * @return {Promise} returns form document promise + */ static async createFormField(formId, doc) { const lastField = await FormFields.findOne({}, { order: 1 }, { sort: { order: -1 } }); + doc.formId = formId; // If there is no field then start with 0 - let order = 0; - - if (lastField) { - order = lastField.order + 1; - } - - doc.order = order; + doc.order = lastField ? lastField.order + 1 : 0; return this.create(doc); } + /** + * Update a form field document + * @param {String} _id id of the form document + * @param {String} doc.type + * @param {String} doc.validation + * @param {String} doc.text + * @param {String} doc.description + * @param {Array} doc.options + * @param {Boolean} doc.isRequired + * @return {Promise} + */ static updateFormField(_id, doc) { return this.update({ _id }, { $set: doc }, { runValidators: true }); } + /** + * Remove form field + * @param {String} _id + * @return {Promise} + */ static removeFormField(_id) { return this.remove({ _id }); } diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index e17943b00..0a5caa8cc 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -1,93 +1,82 @@ import mongoose from 'mongoose'; +import 'mongoose-type-email'; import Random from 'meteor-random'; import { Messages, Conversations } from './Conversations'; import { Customers } from './Customers'; import { KIND_CHOICES, FORM_SUCCESS_ACTIONS, FORM_LOAD_TYPES } from '../../data/constants'; -const MessengerOnlineHoursSchema = mongoose.Schema({ - _id: { - type: String, - }, - day: { - type: String, - }, - from: { - type: String, +// subdocument schema for MessengerOnlineHours +const MessengerOnlineHoursSchema = mongoose.Schema( + { + day: String, + from: String, + to: String, }, - to: { - type: String, - }, -}); + { _id: false }, +); -const MessengerDataSchema = mongoose.Schema({ - notifyCustomer: { - type: Boolean, - }, +// messenger data availability constants +export const MESSENGER_DATA_AVAILABILITY_CONSTANTS = { + MANUAL: 'manual', + AUTO: 'auto', + ALL: ['manual', 'auto'], +}; - // manual, auto - availabilityMethod: { - type: String, - enum: ['manual', 'auto'], +// subdocument schema for MessengerData +const MessengerDataSchema = mongoose.Schema( + { + notifyCustomer: Boolean, + // manual, auto + availabilityMethod: { + type: String, + enum: MESSENGER_DATA_AVAILABILITY_CONSTANTS.ALL, + }, + isOnline: { + type: Boolean, + }, + onlineHours: [MessengerOnlineHoursSchema], + timezone: String, + welcomeMessage: String, + awayMessage: String, + thankYouMessage: String, }, - isOnline: { - type: Boolean, - }, - onlineHours: [MessengerOnlineHoursSchema], - timezone: { - type: String, - }, - welcomeMessage: { - type: String, - }, - awayMessage: { - type: String, - }, - thankYouMessage: { - type: String, - }, -}); + { _id: false }, +); -const FormDataSchema = mongoose.Schema({ - loadType: { - type: String, - enum: FORM_LOAD_TYPES.ALL_LIST, +// subdocument schema for FormData +const FormDataSchema = mongoose.Schema( + { + loadType: { + type: String, + enum: FORM_LOAD_TYPES.ALL_LIST, + }, + successAction: { + type: String, + enum: FORM_SUCCESS_ACTIONS.ALL_LIST, + }, + fromEmail: mongoose.SchemaTypes.Email, + userEmailTitle: String, + userEmailContent: String, + adminEmails: [mongoose.SchemaTypes.Email], + adminEmailTitle: String, + adminEmailContent: String, + thankContent: String, + redirectUrl: String, }, - successAction: { - type: String, - enum: FORM_SUCCESS_ACTIONS.ALL_LIST, - }, - fromEmail: { - type: String, - }, - userEmailTitle: { - type: String, - }, - userEmailContent: { - type: String, - }, - adminEmails: { - type: [String], - }, - adminEmailTitle: { - type: String, - }, - adminEmailContent: { - type: String, - }, - thankContent: { - type: String, - }, - redirectUrl: { - type: String, - }, -}); + { _id: false }, +); -const UiOptionsSchema = mongoose.Schema({ - color: String, - wallpaper: String, - logo: String, -}); +// subdocument schema for messenger UiOptions +const UiOptionsSchema = mongoose.Schema( + { + color: String, + wallpaper: String, + logo: String, + }, + { _id: false }, +); +// schema for integration document const IntegrationSchema = mongoose.Schema({ _id: { type: String, @@ -105,6 +94,13 @@ const IntegrationSchema = mongoose.Schema({ }); class Integration { + /** + * Generate form integration data based on the given form data (formData) + * and integration data (mainDoc) + * @param {Object} mainDoc + * @param {Object} formData + * @return {Object} returns an integration object + */ static generateFormDoc(mainDoc, formData) { return { ...mainDoc, @@ -113,10 +109,49 @@ class Integration { }; } + /** + * Create an integration, intended as a private method + * @param {String} doc.kind + * @param {String} doc.name + * @param {String} doc.brandId + * @param {String} doc.formId + * @param {String} doc.formData.loadType + * @param {String} doc.formData.successAction + * @param {String} doc.formData.formEmail + * @param {String} doc.formData.userEmailTitle + * @param {String} doc.formData.userEmailContent + * @param {Array} doc.formData.adminEmails + * @param {String} doc.formData.adminEmailTitle + * @param {String} doc.formData.adminEmailContent + * @param {String} doc.formData.thankContent + * @param {String} doc.formData.redirectUrl + * @param {Boolean} doc.messengerData.notifyCustomer + * @param {String} doc.messengerData.availabilityMethod + * @param {Boolean} doc.messengerData.isOnline + * @param {String} doc.messengerData.onlineHours.day + * @param {String} doc.messengerData.onlineHours.from + * @param {String} doc.messengerData.onlineHours.to + * @param {String} doc.messengerData.timezone + * @param {String} doc.messengerData.welcomeMessage + * @param {String} doc.messengerData.awayMessage + * @param {String} doc.messengerData.thankYouMessage + * @param {String} doc.messengerData.uiOptions.color + * @param {String} doc.messengerData.uiOptions.wallpaper + * @param {String} doc.messengerData.uiOptions.logo + * @param {Object} doc.twitterData + * @param {Object} doc.facebookData + * @return {Promise} returns integration document promise + */ static createIntegration(doc) { return this.create(doc); } + /** + * Create a messenger kind integration + * @param {String} args.name + * @param {String} args.brandId + * @return {Promise} returns integration document promise + */ static createMessengerIntegration({ name, brandId }) { return this.createIntegration({ name, @@ -125,10 +160,24 @@ class Integration { }); } + /** + * Update a messenger integration + * @param {String} args.name + * @param {String} args.brandId + * @return {Promise} + */ static updateMessengerIntegration(_id, { name, brandId }) { return this.update({ _id }, { $set: { name, brandId } }, { runValidators: true }); } + /** + * Save messenger appearance data + * @param {String} _id + * @param {String} args.color + * @param {String} args.wallpaper + * @param {String} args.logo + * @return {Promise} + */ static saveMessengerAppearanceData(_id, { color, wallpaper, logo }) { return this.update( { _id }, @@ -137,29 +186,89 @@ class Integration { ); } + /** + * Saves messenger data to integration document + * @param {Boolean} messengerData.notifyCustomer + * @param {String} messengerData.availabilityMethod + * @param {Boolean} messengerData.isOnline + * @param {String} messengerData.onlineHours.day + * @param {String} messengerData.onlineHours.from + * @param {String} messengerData.onlineHours.to + * @param {String} messengerData.timezone + * @param {String} messengerData.welcomeMessage + * @param {String} messengerData.awayMessage + * @param {String} messengerData.thankYouMessage + * @param {String} messengerData.uiOptions.color + * @param {String} messengerData.uiOptions.wallpaper + * @param {String} messengerData.uiOptions.logo + * @return {Promise} + */ static saveMessengerConfigs(_id, messengerData) { return this.update({ _id }, { $set: { messengerData } }, { runValidators: true }); } + /** + * Create a form kind integration + * @param {String} args.formData.loadType + * @param {String} args.formData.successAction + * @param {String} args.formData.formEmail + * @param {String} args.formData.userEmailTitle + * @param {String} args.formData.userEmailContent + * @param {Array} args.formData.adminEmails + * @param {String} args.formData.adminEmailTitle + * @param {String} args.formData.adminEmailContent + * @param {String} args.formData.thankContent + * @param {String} args.formData.redirectUrl + * @param {String} args.mainDoc.name + * @param {String} args.mainDoc.brandId + * @param {String} args.mainDoc.formId + * @return {Promise} returns form integration document promise + * @throws {Exception} throws Exception if formData is notSupplied + */ static createFormIntegration({ formData, ...mainDoc }) { const doc = this.generateFormDoc(mainDoc, formData); if (Object.keys(formData || {}).length === 0) { - throw 'formData must be supplied'; + throw new Error('formData must be supplied'); } return this.create(doc); } - static updateFormIntegration(id, { formData, ...mainDoc }) { + /** + * Update a form kind integration + * @param {String} _id integration id + * @param {String} args.formData.loadType + * @param {String} args.formData.successAction + * @param {String} args.formData.formEmail + * @param {String} args.formData.userEmailTitle + * @param {String} args.formData.userEmailContent + * @param {Array} args.formData.adminEmails + * @param {String} args.formData.adminEmailTitle + * @param {String} args.formData.adminEmailContent + * @param {String} args.formData.thankContent + * @param {String} args.formData.redirectUrl + * @param {String} args.mainDoc.name + * @param {String} args.mainDoc.brandId + * @param {String} args.mainDoc.formId + * @return {Promise} + */ + static updateFormIntegration(_id, { formData, ...mainDoc }) { const doc = this.generateFormDoc(mainDoc, formData); - return this.update({ _id: id }, { $set: doc }, { runValidators: true }); + + return this.update({ _id }, { $set: doc }, { runValidators: true }); } + /** + * Removes an integration plus its messages, conversations, customers + * @param {String} id + * @return {Promise} + */ static async removeIntegration(id) { const conversations = await Conversations.find({ integrationId: id }, { _id: true }); const conversationIds = []; + conversations.forEach(c => { conversationIds.push(c._id); }); diff --git a/src/db/models/Notifications.js b/src/db/models/Notifications.js index 4d810b932..8293141df 100644 --- a/src/db/models/Notifications.js +++ b/src/db/models/Notifications.js @@ -1,7 +1,14 @@ import mongoose from 'mongoose'; +import Random from 'meteor-random'; import { MODULE_LIST } from '../../data/constants'; +// Notification schema const NotificationSchema = new mongoose.Schema({ + _id: { + type: String, + unique: true, + default: () => Random.id(), + }, notifType: { type: String, enum: MODULE_LIST, @@ -11,16 +18,28 @@ const NotificationSchema = new mongoose.Schema({ content: String, createdUser: String, receiver: String, - date: Date, - isRead: Boolean, + date: { + type: Date, + default: Date.now, + }, + isRead: { + type: Boolean, + default: false, + }, }); class Notification { + /** + * Marks notifications as read + * @param {Array} ids + * @return {Promise} + */ static markAsRead(ids) { return this.update({ _id: { $in: ids } }, { $set: { isRead: true } }, { multi: true }); } - /** Create a notification + /** + * Create a notification * @param {String} doc.notifType * @param {String} doc.createdUser * @param {String} doc.title @@ -30,7 +49,7 @@ class Notification { * @return {Notification} Notification Object * @throws {Exception} throws Exception if createdUser is not supplied */ - static async createNotification({ createdUser, ...doc }) { + static async createNotification(doc, createdUser) { if (!createdUser) { throw new Error('createdUser must be supplied'); } @@ -52,10 +71,26 @@ class Notification { return await this.create(doc); } + /** + * Update a notification + * @param {String} _id + * @param {String} doc.notifType + * @param {String} doc.createdUser + * @param {String} doc.title + * @param {String} doc.content + * @param {String} doc.link + * @param {String} doc.receiver + * @return {Promise} + */ static updateNotification(_id, doc) { return this.update({ _id }, doc); } + /** + * Remove a notification + * @param {String} _id + * @return {Promise} + */ static removeNotification(_id) { return this.remove({ _id }); } @@ -64,31 +99,49 @@ class Notification { NotificationSchema.loadClass(Notification); export const Notifications = mongoose.model('notifications', NotificationSchema); +// schema for NotificationConfigurations const ConfigSchema = new mongoose.Schema({ - // To whom this config is related + _id: { + type: String, + unique: true, + default: () => Random.id(), + }, + // to whom this config is related user: String, - notifType: String, + notifType: { + type: String, + enum: MODULE_LIST, + }, isAllowed: Boolean, }); class Configuration { - static async createOrUpdateConfiguration({ notifType, isAllowed, user }) { + /** + * creates an new notification or updates already existing notification configuration + * @param {String} args1.notifType + * @param {Boolean} args1.isAllowed + * @param {String} user + * @return {Object} returns NotificationConfigurations object + */ + static async createOrUpdateConfiguration({ notifType, isAllowed }, user) { if (!user) { throw new Error('user must be supplied'); } - const selector = { user, notifType }; + const selector = { user: user, notifType }; const oldOne = await this.findOne(selector); // If already inserted then raise error if (oldOne) { await this.update({ _id: oldOne._id }, { $set: { isAllowed } }); + return await this.findOne({ _id: oldOne._id }); } // If it is first time then insert selector.isAllowed = isAllowed; + return await this.create(selector); } } diff --git a/yarn.lock b/yarn.lock index 201dc3e8e..daa58f247 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1955,7 +1955,7 @@ growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" -handlebars@^4.0.3: +handlebars@^4.0.10, handlebars@^4.0.3: version "4.0.10" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f" dependencies: @@ -3089,6 +3089,10 @@ mongodb@2.2.31: mongodb-core "2.1.15" readable-stream "2.2.7" +mongoose-type-email@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/mongoose-type-email/-/mongoose-type-email-1.0.5.tgz#89e797f98bc59aac2263fd7c9d5b669854aff679" + mongoose@^4.9.2: version "4.11.12" resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-4.11.12.tgz#48ebd5cad051f6ddfd46648b86a19c7fd30e36db" @@ -4002,10 +4006,6 @@ shellwords@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" -shortid@^2.2.8: - version "2.2.8" - resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.8.tgz#033b117d6a2e975804f6f0969dbe7d3d0b355131" - signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" From 8aac98b55c2cc92bc4c620b3de6d4967ae8b459c Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Thu, 12 Oct 2017 13:14:38 +0800 Subject: [PATCH 040/318] Move updating data functions of mutations to model in conversation --- src/__tests__/conversationMessage.db.test.js | 157 +++++++++++++++++ .../conversationMessageMutations.test.js | 31 ++-- src/__tests__/emailTemplateMutations.test.js | 2 +- .../responseTemplateMutations.test.js | 2 +- src/data/resolvers/mutations/conversations.js | 136 +++++++++------ .../{emailTemplate.js => emailTemplates.js} | 0 src/data/resolvers/mutations/index.js | 8 +- ...sponseTemplate.js => responseTemplates.js} | 0 src/db/models/Brands.js | 2 +- src/db/models/Conversations.js | 164 +++++++++++++++++- 10 files changed, 420 insertions(+), 82 deletions(-) create mode 100644 src/__tests__/conversationMessage.db.test.js rename src/data/resolvers/mutations/{emailTemplate.js => emailTemplates.js} (100%) rename src/data/resolvers/mutations/{responseTemplate.js => responseTemplates.js} (100%) diff --git a/src/__tests__/conversationMessage.db.test.js b/src/__tests__/conversationMessage.db.test.js new file mode 100644 index 000000000..06482922f --- /dev/null +++ b/src/__tests__/conversationMessage.db.test.js @@ -0,0 +1,157 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Conversations, ConversationMessages, Users } from '../db/models'; +import { conversationFactory, conversationMessageFactory, userFactory } from '../db/factories'; +import conversationMutations from '../data/resolvers/mutations/conversations'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Conversation message mutations', () => { + let _conversation; + let _conversationMessage; + let _user; + let _doc; + + beforeEach(async () => { + // Creating test data + _conversation = await conversationFactory(); + _conversationMessage = await conversationMessageFactory(); + _user = await userFactory(); + _doc = { + content: _conversationMessage.content, + attachments: _conversationMessage.attachments, + status: _conversationMessage.status, + mentionedUserIds: _conversationMessage.mentionedUserIds, + conversationId: _conversation._id, + internal: _conversationMessage.internal, + customerId: _conversationMessage.customerId, + isCustomerRead: _conversationMessage.isCustomerRead, + engageData: _conversationMessage.engageData, + formWidgetData: _conversationMessage.formWidgetData, + facebookData: _conversationMessage.facebookData, + }; + }); + + afterEach(async () => { + // Clearing test data + await Conversations.remove({}); + await ConversationMessages.remove({}); + await Users.remove({}); + }); + + test('Create conversation message', async () => { + const messageObj = await ConversationMessages.createMessage({ ..._doc, userId: _user.id }); + + expect(messageObj.content).toBe(_conversationMessage.content); + expect(messageObj.attachments).toBe(_conversationMessage.attachments); + expect(messageObj.status).toBe(_conversationMessage.status); + expect(messageObj.mentionedUserIds[0]).toBe(_conversationMessage.mentionedUserIds[0]); + expect(messageObj.conversationId).toBe(_conversation._id); + expect(messageObj.internal).toBe(_conversationMessage.internal); + expect(messageObj.customerId).toBe(_conversationMessage.customerId); + expect(messageObj.isCustomerRead).toBe(_conversationMessage.isCustomerRead); + expect(messageObj.engageData).toBe(_conversationMessage.engageData); + expect(messageObj.formWidgetData).toBe(_conversationMessage.formWidgetData); + expect(messageObj.facebookData._id).toBe(_conversationMessage.facebookData._id); + expect(messageObj.userId).toBe(_user._id); + }); + + // if user assigned to conversation + test('Assign conversation to employee', async () => { + await Conversations.assignUserConversation(_conversation._id, _user.id); + + const conversationObj = await Conversations.findOne({ _id: _conversation._id }); + + expect(conversationObj.assignedUserId).toBe(_user._id); + }); + + test('Unassign employee from conversation', async () => { + // assign employee before unassign + await Conversations.assignUserConversation(_conversation._id, _user.id); + + // unassign + await Conversations.unassignUserConversation([_conversation._id]); + + const conversationObj = await Conversations.findOne({ _id: _conversation._id }); + + expect(conversationObj.assignedUserId).toBe(undefined); + }); + + test('Change conversation status', async () => { + await Conversations.changeStatusConversation([_conversation._id], 'new'); + + const conversationObj = await Conversations.findOne({ _id: _conversation._id }); + + expect(conversationObj.status).toBe('new'); + }); + + test('Conversation star', async () => { + await conversationMutations.conversationsStar( + {}, + { _ids: [_conversation._id] }, + { user: _user }, + ); + + const user = await Users.findOne({ _id: _user._id }); + expect(user.details.starredConversationIds[0]).toBe(_conversation._id); + }); + + test('Conversation unstar', async () => { + const ids = [_conversation._id]; + + // star first before unstar + await Users.update( + { _id: _user.id }, + { + $addToSet: { + 'details.starredConversationIds': { $each: ids }, + }, + }, + ); + + // unstar + await conversationMutations.conversationsUnstar({}, { _ids: ids }, { user: _user }); + + const user = await Users.findOne({ _id: _user._id }); + expect(user.details.starredConversationIds.length).toBe(0); + }); + + test('Toggle participated users in conversation ', async () => { + // make sure participated users is empty + expect(_conversation.participatedUserIds.length).toBe(0); + + // add user to conversation + await Conversations.addParticipatedUserToConversation([_conversation._id], _user.id); + + const conversationObj = await Conversations.findOne({ _id: _conversation.id }); + + expect(conversationObj.participatedUserIds[0]).toBe(_user._id); + + // remove user from conversation + await Conversations.removeParticipatedUserFromConversation([_conversation._id], _user.id); + + const conversationObjWithParticipatedUser = await Conversations.findOne({ + _id: _conversation.id, + }); + + expect(conversationObjWithParticipatedUser.participatedUserIds.length).toBe(0); + }); + + test('Conversation mark as read', async () => { + // first user read this conversation + await Conversations.markAsReadConversation(_conversation._id, _user._id); + + const conversationObj = await Conversations.findOne({ _id: _conversation._id }); + + expect(conversationObj.readUserIds[0]).toBe(_user._id); + + const second_user = await userFactory(); + + // multiple users read conversation + await Conversations.markAsReadConversation(_conversation._id, second_user._id, false); + }); +}); diff --git a/src/__tests__/conversationMessageMutations.test.js b/src/__tests__/conversationMessageMutations.test.js index a830ee393..38916cde7 100644 --- a/src/__tests__/conversationMessageMutations.test.js +++ b/src/__tests__/conversationMessageMutations.test.js @@ -44,23 +44,24 @@ describe('Conversation message mutations', () => { }); test('Create conversation message', async () => { - const conversationObj = await conversationMutations.conversationMessageAdd({}, _doc, { + const messageId = await conversationMutations.conversationMessageAdd({}, _doc, { user: _user, }); - expect(conversationObj).toBeDefined(); - expect(conversationObj.content).toBe(_conversationMessage.content); - expect(conversationObj.attachments).toBe(_conversationMessage.attachments); - expect(conversationObj.status).toBe(_conversationMessage.status); - expect(conversationObj.mentionedUserIds[0]).toBe(_conversationMessage.mentionedUserIds[0]); - expect(conversationObj.conversationId).toBe(_conversation._id); - expect(conversationObj.internal).toBe(_conversationMessage.internal); - expect(conversationObj.customerId).toBe(_conversationMessage.customerId); - expect(conversationObj.isCustomerRead).toBe(_conversationMessage.isCustomerRead); - expect(conversationObj.engageData).toBe(_conversationMessage.engageData); - expect(conversationObj.formWidgetData).toBe(_conversationMessage.formWidgetData); - expect(conversationObj.facebookData).toBe(_conversationMessage.facebookData); - expect(conversationObj.userId).toBe(_user._id); + const messageObj = await ConversationMessages.findOne({ _id: messageId }); + + expect(messageObj.content).toBe(_conversationMessage.content); + expect(messageObj.attachments).toBe(undefined); + expect(messageObj.status).toBe(_conversationMessage.status); + expect(messageObj.mentionedUserIds[0]).toBe(_conversationMessage.mentionedUserIds[0]); + expect(messageObj.conversationId).toBe(_conversation._id); + expect(messageObj.internal).toBe(_conversationMessage.internal); + expect(messageObj.customerId).toBe(_conversationMessage.customerId); + expect(messageObj.isCustomerRead).toBe(_conversationMessage.isCustomerRead); + expect(messageObj.engageData).toBe(undefined); + expect(messageObj.formWidgetData).toBe(undefined); + expect(messageObj.facebookData._id).toBe(_conversationMessage.facebookData._id); + expect(messageObj.userId).toBe(_user._id); }); // check conversation if integration doesn't found @@ -176,7 +177,7 @@ describe('Conversation message mutations', () => { _id: _conversation.id, }); - // check if participated user is add + // check if participated user add expect(conversationObjWithParticipatedUser.participatedUserIds.length).toBe(0); }); diff --git a/src/__tests__/emailTemplateMutations.test.js b/src/__tests__/emailTemplateMutations.test.js index 025fb345f..28a588405 100644 --- a/src/__tests__/emailTemplateMutations.test.js +++ b/src/__tests__/emailTemplateMutations.test.js @@ -4,7 +4,7 @@ import { connect, disconnect } from '../db/connection'; import { EmailTemplates, Users } from '../db/models'; import { emailTemplateFactory, userFactory } from '../db/factories'; -import emailTemplateMutations from '../data/resolvers/mutations/emailTemplate'; +import emailTemplateMutations from '../data/resolvers/mutations/emailTemplates'; beforeAll(() => connect()); diff --git a/src/__tests__/responseTemplateMutations.test.js b/src/__tests__/responseTemplateMutations.test.js index a26f36784..1eda530be 100644 --- a/src/__tests__/responseTemplateMutations.test.js +++ b/src/__tests__/responseTemplateMutations.test.js @@ -4,7 +4,7 @@ import { connect, disconnect } from '../db/connection'; import { ResponseTemplates, Users } from '../db/models'; import { responseTemplateFactory, userFactory } from '../db/factories'; -import responseTemplateMutations from '../data/resolvers/mutations/responseTemplate'; +import responseTemplateMutations from '../data/resolvers/mutations/responseTemplates'; beforeAll(() => connect()); diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 235eaf3fb..b7917be40 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -1,7 +1,3 @@ -/* - * Will implement actual db changes after removing meteor - */ - import strip from 'strip'; import { @@ -40,6 +36,21 @@ const conversationsChanged = async (_ids, type) => { } }; +const createMessage = async (conversation, doc, userId) => { + const message = await ConversationMessages.createMessage({ ...doc, userId }); + + // subscribe + pubsub.publish('conversationMessageInserted', { + conversationMessageInserted: message, + }); + + pubsub.publish('conversationsChanged', { + conversationsChanged: { customerId: conversation.customerId, type: 'newMessage' }, + }); + + return message._id; +}; + export default { /** * Create new message in conversation @@ -73,7 +84,7 @@ export default { // do not send internal message to third service integrations if (doc.internal) { - return ConversationMessages.createMessage({ ...doc, userId }); + return createMessage(conversation, doc, userId); } const integration = await Integrations.findOne({ _id: conversation.integrationId }); @@ -87,19 +98,9 @@ export default { // TODO: return tweetReply(conversation, strip(content)); } - const message = await ConversationMessages.createMessage({ ...doc, userId }); - const messageId = message._id; + const messageId = await createMessage(conversation, doc, userId); const customer = await Customers.findOne({ _id: conversation.customerId }); - // subscribe - pubsub.publish('conversationMessageInserted', { - conversationMessageInserted: message, - }); - - pubsub.publish('conversationsChanged', { - conversationsChanged: { customerId: conversation.customerId, type: 'newMessage' }, - }); - // if conversation's integration kind is form then send reply to // customer's email const email = customer ? customer.email : ''; @@ -117,8 +118,11 @@ export default { return messageId; }, - /* - * assign employee to conversation + /** + * Assign employee to conversation + * @param {list} conversationIds + * @param {String} assignedUserId + * @return {Promise} String */ async conversationsAssign(root, { conversationIds, assignedUserId }, { user }) { if (!user) throw new Error('Login required'); @@ -129,11 +133,7 @@ export default { throw new Error('User not found.'); } - await Conversations.update( - { _id: { $in: conversationIds } }, - { $set: { assignedUserId } }, - { multi: true }, - ); + await Conversations.assignUserConversation(conversationIds, assignedUserId); // notify graphl subscription conversationsChanged(conversationIds, 'statusChanged'); @@ -141,41 +141,49 @@ export default { const updatedConversations = await Conversations.find(selector); // send notification - updatedConversations.forEach(function(conversation) { + updatedConversations.forEach(conversation => { const content = 'Assigned user has changed'; // TODO: sendNotification }); + + return 'done'; }, - /* - * unassign employee from conversation + /** + * Unassign employee from conversation + * @param {list} ids of conversation + * @return {Promise} String */ async conversationsUnassign(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); await conversationsCheckExistance(_ids); - await Conversations.update( - { _id: { $in: _ids } }, - { $unset: { assignedUserId: 1 } }, - { multi: true }, - ); + await Conversations.unassignUserConversation(_ids); // notify graphl subscription conversationsChanged(_ids, 'statusChanged'); + + return 'done'; }, + /** + * Change conversation status + * @param {list} _ids of conversation + * @param {String} status + * @return {Promise} String + */ async conversationsChangeStatus(root, { _ids, status }, { user }) { if (!user) throw new Error('Login required'); const { conversations } = await conversationsCheckExistance(_ids); - Conversations.update({ _id: { $in: _ids } }, { $set: { status } }, { multi: true }); + Conversations.changeStatusConversation(_ids, status); // notify graphl subscription conversationsChanged(_ids, 'statusChanged'); - conversations.forEach(function(conversation) { + conversations.forEach(conversation => { if (status === CONVERSATION_STATUSES.CLOSED) { const customer = conversation.customer(); const integration = conversation.integration(); @@ -192,8 +200,15 @@ export default { const content = 'Conversation status has changed.'; // TODO: send notification + + return 'done'; }, + /** + * Star conversation + * @param {list} _ids of conversation + * @return {Promise} String + */ async conversationsStar(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); @@ -208,8 +223,15 @@ export default { }, }, ); + + return 'done'; }, + /** + * Unstar conversation + * @param {list} _ids of conversation + * @return {Promise} String + */ async conversationsUnstar(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); @@ -224,8 +246,15 @@ export default { }, }, ); + + return 'done'; }, + /** + * Add or remove participed users in conversation + * @param {list} _ids of conversation + * @return {Promise} String + */ async conversationsToggleParticipate(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); @@ -238,43 +267,42 @@ export default { // not previously added if ((await Conversations.find(extendSelector).count()) === 0) { - await Conversations.update( - selector, - { $addToSet: { participatedUserIds: user._id } }, - { multi: true }, - ); + await Conversations.addParticipatedUserToConversation(_ids, user._id); } else { // remove - await Conversations.update( - selector, - { $pull: { participatedUserIds: { $in: [user._id] } } }, - { multi: true }, - ); + await Conversations.removeParticipatedUserFromConversation(_ids, user._id); } // notify graphl subscription conversationsChanged(_ids, 'participatedStateChanged'); + + return 'done'; }, + /** + * Conversation mark as read + * @param {list} _ids of conversation + * @return {Promise} String + */ async conversationMarkAsRead(root, { _id }, { user }) { if (!user) throw new Error('Login required'); const conversation = await Conversations.findOne({ _id }); - if (conversation) { - const readUserIds = conversation.readUserIds; + if (!conversation) return 'not affected'; - // if current user is first one - if (!readUserIds) { - return Conversations.update({ _id: _id }, { $set: { readUserIds: [user._id] } }); - } + const readUserIds = conversation.readUserIds; - // if current user is not in read users list then add it - if (!readUserIds.includes(user._id)) { - return Conversations.update({ _id }, { $push: { readUserIds: user._id } }); - } + // if current user is first one + if (!readUserIds) { + return Conversations.markAsReadConversation(_id, user._id); + } + + // if current user is not in read users list then add it + if (!readUserIds.includes(user._id)) { + return Conversations.markAsReadConversation(_id, user._id, false); } - return 'not affected'; + return 'done'; }, }; diff --git a/src/data/resolvers/mutations/emailTemplate.js b/src/data/resolvers/mutations/emailTemplates.js similarity index 100% rename from src/data/resolvers/mutations/emailTemplate.js rename to src/data/resolvers/mutations/emailTemplates.js diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index 7cab413a7..72f51c4c0 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -1,11 +1,11 @@ import conversations from './conversations'; import brands from './brands'; -import emailTemplate from './emailTemplate'; -import responseTemplate from './responseTemplate'; +import emailTemplates from './emailTemplates'; +import responseTemplates from './responseTemplates'; export default { ...conversations, ...brands, - ...emailTemplate, - ...responseTemplate, + ...emailTemplates, + ...responseTemplates, }; diff --git a/src/data/resolvers/mutations/responseTemplate.js b/src/data/resolvers/mutations/responseTemplates.js similarity index 100% rename from src/data/resolvers/mutations/responseTemplate.js rename to src/data/resolvers/mutations/responseTemplates.js diff --git a/src/db/models/Brands.js b/src/db/models/Brands.js index ee8a1bc9f..f5dfaef3c 100644 --- a/src/db/models/Brands.js +++ b/src/db/models/Brands.js @@ -4,7 +4,7 @@ import Random from 'meteor-random'; const BrandEmailConfigSchema = mongoose.Schema({ type: { type: String, - allowedValues: ['simple', 'custom'], + enum: ['simple', 'custom'], }, template: String, }); diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index 92178a18f..6688aed03 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -1,7 +1,80 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -import { CONVERSATION_STATUSES } from '../../data/constants'; +import { CONVERSATION_STATUSES, FACEBOOK_DATA_KINDS } from '../../data/constants'; +const TwitterDirectMessageSchema = mongoose.Schema({ + _id: { + type: String, + unique: true, + default: () => Random.id(), + }, + senderId: { + type: Number, + }, + senderIdStr: { + type: String, + }, + recipientId: { + type: Number, + }, + recipientIdStr: { + type: String, + }, +}); + +// Twitter schema +const TwitterSchema = mongoose.Schema({ + _id: { + type: String, + unique: true, + default: () => Random.id(), + }, + idStr: { + type: String, + }, + screenName: { + type: String, + }, + isDirectMessage: { + type: Boolean, + }, + directMessage: { + type: TwitterDirectMessageSchema, + }, +}); + +// facebook schema +const FacebookSchema = mongoose.Schema({ + _id: { + type: String, + unique: true, + default: () => Random.id(), + }, + kind: { + type: String, + enum: FACEBOOK_DATA_KINDS.ALL_LIST, + }, + senderName: { + type: String, + }, + senderId: { + type: String, + }, + recipientId: { + type: String, + }, + + // when wall post + postId: { + type: String, + }, + + pageId: { + type: String, + }, +}); + +// Conversation schema const ConversationSchema = mongoose.Schema({ _id: { type: String, unique: true, default: () => Random.id() }, content: String, @@ -18,15 +91,15 @@ const ConversationSchema = mongoose.Schema({ status: { type: String, required: true, - allowedValues: CONVERSATION_STATUSES.ALL_LIST, + enum: CONVERSATION_STATUSES.ALL_LIST, }, messageCount: Number, tagIds: [String], // number of total conversations number: Number, - twitterData: Object, - facebookData: Object, + twitterData: TwitterSchema, + facebookData: FacebookSchema, }); class Conversation { @@ -40,10 +113,89 @@ class Conversation { ...doc, status: CONVERSATION_STATUSES.NEW, createdAt: new Date(), - number: Conversations.find().count() + 1, + number: this.find().count() + 1, messageCount: 0, }); } + + /** + * Assign user to conversation + * @param {list} conversationIds + * @param {String} assignedUserId + * @return {Promise} Updated conversation id + */ + static assignUserConversation(conversationIds, assignedUserId) { + return this.update( + { _id: { $in: conversationIds } }, + { $set: { assignedUserId } }, + { multi: true }, + ); + } + + /** + * Unassign user from conversation + * @param {list} conversationIds + * @return {Promise} Updated conversation id + */ + static unassignUserConversation(conversationIds) { + return this.update( + { _id: { $in: conversationIds } }, + { $unset: { assignedUserId: 1 } }, + { multi: true }, + ); + } + + /** + * Change conversation status + * @param {list} conversationIds + * @param {String} status + * @return {Promise} Updated conversation id + */ + static changeStatusConversation(conversationIds, status) { + return this.update({ _id: { $in: conversationIds } }, { $set: { status } }, { multi: true }); + } + + /** + * Add participated user to conversation + * @param {Object} selector + * @param {String} userId + * @return {Promise} Updated conversation id + */ + static addParticipatedUserToConversation(_ids, userId) { + return this.update( + { _id: { $in: _ids } }, + { $addToSet: { participatedUserIds: userId } }, + { multi: true }, + ); + } + + /** + * Remove participated user from conversation + * @param {list} _ids + * @param {String} userId + * @return {Promise} Updated conversation id + */ + static removeParticipatedUserFromConversation(_ids, userId) { + return this.update( + { _id: { $in: _ids } }, + { $pull: { participatedUserIds: { $in: [userId] } } }, + { multi: true }, + ); + } + + /** + * Mark as read conversation + * @param {String} _id of conversation + * @param {String} userId + * @param {Boolean} firstOne + * @return {Promise} Updated conversation id + */ + static markAsReadConversation(_id, userId, firstOne) { + // if current user is first one + if (firstOne) return this.update({ _id }, { $set: { readUserIds: [userId] } }); + + return this.update({ _id }, { $push: { readUserIds: userId } }); + } } ConversationSchema.loadClass(Conversation); @@ -62,7 +214,7 @@ const MessageSchema = mongoose.Schema({ isCustomerRead: Boolean, engageData: Object, formWidgetData: Object, - facebookData: Object, + facebookData: FacebookSchema, }); class Message { From 81405a1d871ec3c062d3e366caf3051ddc81cd03 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 12 Oct 2017 13:54:05 +0800 Subject: [PATCH 041/318] Add companies resolver --- src/data/resolvers/customer.js | 6 +++++- src/data/schema/customer.js | 1 + src/db/models/Customers.js | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/data/resolvers/customer.js b/src/data/resolvers/customer.js index 05ff12574..39f579096 100644 --- a/src/data/resolvers/customer.js +++ b/src/data/resolvers/customer.js @@ -1,4 +1,4 @@ -import { Conversations, Tags } from '../../db/models'; +import { Companies, Conversations, Tags } from '../../db/models'; export default { getIntegrationData(customer) { @@ -31,4 +31,8 @@ export default { conversations(customer) { return Conversations.find({ customerId: customer._id }); }, + + companies(customer) { + return Companies.find({ _id: { $in: customer.companyIds || [] } }); + }, }; diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index ad72112ae..cefc49886 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -25,6 +25,7 @@ export const types = ` twitterData: JSON facebookData: JSON + companies: [Company] conversations: [Conversation] getIntegrationData: JSON getMessengerCustomData: JSON diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index ff7d218af..d5c4b4b66 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -109,6 +109,7 @@ const CustomerSchema = mongoose.Schema({ integrationId: String, tagIds: [String], + companyIds: [String], customFieldsData: Object, internalNotes: [internalNoteSchema], From fe1aecd5625a92c005d4e8c29c6e2f2abbc836b3 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 12 Oct 2017 17:47:37 +0800 Subject: [PATCH 042/318] Implement internal notes --- src/__tests__/internalNoteMutations.test.js | 105 ++++++++++++++++++ src/constants.js | 6 + src/data/resolvers/index.js | 2 + src/data/resolvers/internalNote.js | 7 ++ src/data/resolvers/mutations/index.js | 2 + src/data/resolvers/mutations/internalNotes.js | 33 ++++++ src/data/resolvers/queries/index.js | 2 + src/data/resolvers/queries/internalNotes.js | 12 ++ src/data/schema/index.js | 9 ++ src/data/schema/internalNote.js | 22 ++++ src/db/factories.js | 11 ++ src/db/models/InternalNotes.js | 79 +++++++++++++ src/db/models/index.js | 2 + 13 files changed, 292 insertions(+) create mode 100644 src/__tests__/internalNoteMutations.test.js create mode 100644 src/data/resolvers/internalNote.js create mode 100644 src/data/resolvers/mutations/internalNotes.js create mode 100644 src/data/resolvers/queries/internalNotes.js create mode 100644 src/data/schema/internalNote.js create mode 100644 src/db/models/InternalNotes.js diff --git a/src/__tests__/internalNoteMutations.test.js b/src/__tests__/internalNoteMutations.test.js new file mode 100644 index 000000000..2b9e5d4ff --- /dev/null +++ b/src/__tests__/internalNoteMutations.test.js @@ -0,0 +1,105 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import faker from 'faker'; +import { connect, disconnect } from '../db/connection'; +import { InternalNotes, Users } from '../db/models'; +import { userFactory, internalNoteFactory } from '../db/factories'; +import internalNoteMutations from '../data/resolvers/mutations/internalNotes'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +/* + * Generate test data + */ +const generateData = () => ({ + contentType: 'customer', + contentTypeId: 'DFDFAFSFSDDSF', + content: faker.random.word(), +}); + +/* + * Check values + */ +const checkValues = (internalNoteObj, doc) => { + expect(internalNoteObj.contentType).toBe(doc.contentType); + expect(internalNoteObj.contentTypeId).toBe(doc.contentTypeId); + expect(internalNoteObj.content).toBe(doc.content); +}; + +describe('InternalNotes mutations', () => { + let _user; + let _internalNote; + + beforeEach(async () => { + // Creating test data + _user = await userFactory(); + _internalNote = await internalNoteFactory(); + }); + + afterEach(async () => { + // Clearing test data + await InternalNotes.remove({}); + await Users.remove({}); + }); + + test('Create internalNote', async () => { + // Login required + expect(() => internalNoteMutations.internalNotesAdd({}, {}, {})).toThrowError('Login required'); + + // valid + const doc = generateData(); + + const internalNoteObj = await internalNoteMutations.internalNotesAdd({}, doc, { user: _user }); + + checkValues(internalNoteObj, doc); + expect(internalNoteObj.createdUserId).toBe(_user._id); + }); + + test('Edit internalNote login required', async () => { + expect.assertions(1); + + try { + await internalNoteMutations.internalNotesEdit({}, { _id: _internalNote.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Edit internalNote valid', async () => { + const doc = generateData(); + + const internalNoteObj = await internalNoteMutations.internalNotesEdit( + {}, + { _id: _internalNote._id, ...doc }, + { user: _user }, + ); + + checkValues(internalNoteObj, doc); + }); + + test('Remove internalNote login required', async () => { + expect.assertions(1); + + try { + await internalNoteMutations.internalNotesRemove({}, { _id: _internalNote.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Remove internalNote valid', async () => { + const internalNoteDeletedObj = await internalNoteMutations.internalNotesRemove( + {}, + { _id: _internalNote.id }, + { user: _user }, + ); + + expect(internalNoteDeletedObj.id).toBe(_internalNote.id); + + const internalNoteObj = await InternalNotes.findOne({ _id: _internalNote.id }); + expect(internalNoteObj).toBeNull(); + }); +}); diff --git a/src/constants.js b/src/constants.js index 8afc58ad1..54f8a597a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -4,3 +4,9 @@ export const FIELD_CONTENT_TYPES = { COMPANY: 'company', ALL_LIST: ['form', 'customer', 'company'], }; + +export const INTERNAL_NOTE_CONTENT_TYPES = { + CUSTOMER: 'customer', + COMPANY: 'company', + ALL_LIST: ['customer', 'company'], +}; diff --git a/src/data/resolvers/index.js b/src/data/resolvers/index.js index 1fbd452ca..19eeceb39 100644 --- a/src/data/resolvers/index.js +++ b/src/data/resolvers/index.js @@ -6,6 +6,7 @@ import ResponseTemplate from './responseTemplate'; import Integration from './integration'; import Form from './form'; import EngageMessage from './engage'; +import InternalNote from './internalNote'; import Customer from './customer'; import Segment from './segment'; import Conversation from './conversation'; @@ -19,6 +20,7 @@ export default { ResponseTemplate, Integration, Form, + InternalNote, Customer, Segment, EngageMessage, diff --git a/src/data/resolvers/internalNote.js b/src/data/resolvers/internalNote.js new file mode 100644 index 000000000..ab4311d44 --- /dev/null +++ b/src/data/resolvers/internalNote.js @@ -0,0 +1,7 @@ +import { Users } from '../../db/models'; + +export default { + createdUser(note) { + return Users.findOne({ _id: note.createdUserId }); + }, +}; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index 69ea1b6e4..f699af60b 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -2,6 +2,7 @@ import conversation from './conversation'; import brands from './brands'; import emailTemplate from './emailTemplate'; import responseTemplate from './responseTemplate'; +import internalNotes from './internalNotes'; import customers from './customers'; import segments from './segments'; import companies from './companies'; @@ -12,6 +13,7 @@ export default { ...brands, ...emailTemplate, ...responseTemplate, + ...internalNotes, ...customers, ...segments, ...companies, diff --git a/src/data/resolvers/mutations/internalNotes.js b/src/data/resolvers/mutations/internalNotes.js new file mode 100644 index 000000000..6e9bd813e --- /dev/null +++ b/src/data/resolvers/mutations/internalNotes.js @@ -0,0 +1,33 @@ +import { InternalNotes } from '../../../db/models'; + +export default { + /** + * Adds internalNote object + * @return {Promise} + */ + internalNotesAdd(root, args, { user }) { + if (!user) throw new Error('Login required'); + + return InternalNotes.createInternalNote(args, user); + }, + + /** + * Updates internalNote object + * @return {Promise} return Promise(null) + */ + internalNotesEdit(root, { _id, ...doc }, { user }) { + if (!user) throw new Error('Login required'); + + return InternalNotes.updateInternalNote(_id, doc); + }, + + /** + * Remove a channel + * @return {Promise} + */ + internalNotesRemove(root, { _id }, { user }) { + if (!user) throw new Error('Login required'); + + return InternalNotes.removeInternalNote(_id); + }, +}; diff --git a/src/data/resolvers/queries/index.js b/src/data/resolvers/queries/index.js index ab10c3251..afe2c223e 100644 --- a/src/data/resolvers/queries/index.js +++ b/src/data/resolvers/queries/index.js @@ -8,6 +8,7 @@ import responseTemplates from './responseTemplates'; import emailTemplates from './emailTemplates'; import engages from './engages'; import tags from './tags'; +import internalNotes from './internalNotes'; import customers from './customers'; import segments from './segments'; import conversations from './conversations'; @@ -25,6 +26,7 @@ export default { ...emailTemplates, ...engages, ...tags, + ...internalNotes, ...customers, ...segments, ...conversations, diff --git a/src/data/resolvers/queries/internalNotes.js b/src/data/resolvers/queries/internalNotes.js new file mode 100644 index 000000000..bec4acd25 --- /dev/null +++ b/src/data/resolvers/queries/internalNotes.js @@ -0,0 +1,12 @@ +import { InternalNotes } from '../../../db/models'; + +export default { + /** + * InternalNotes list + * @param {Object} args + * @return {Promise} sorted internalNotes list + */ + internalNotes(root, { contentType, contentTypeId }) { + return InternalNotes.find({ contentType, contentTypeId }).sort({ createdDate: 1 }); + }, +}; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index 59f3f6222..74feb999d 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -28,6 +28,12 @@ import { types as EngageTypes, queries as EngageQueries } from './engage'; import { types as TagTypes, queries as TagQueries } from './tag'; +import { + types as InternalNoteTypes, + queries as InternalNoteQueries, + mutations as InternalNoteMutations, +} from './internalNote'; + import { types as CustomerTypes, queries as CustomerQueries, @@ -55,6 +61,7 @@ export const types = ` scalar Date ${UserTypes} + ${InternalNoteTypes} ${CompanyTypes} ${ChannelTypes} ${BrandTypes} @@ -84,6 +91,7 @@ export const queries = ` ${FormQueries} ${EngageQueries} ${TagQueries} + ${InternalNoteQueries} ${CustomerQueries} ${SegmentQueries} ${ConversationQueries} @@ -99,6 +107,7 @@ export const mutations = ` ${BrandMutations} ${ResponseTemplateMutations} ${EmailTemplateMutations} + ${InternalNoteMutations} ${CustomerMutations} ${SegmentMutations} ${FieldMutations} diff --git a/src/data/schema/internalNote.js b/src/data/schema/internalNote.js new file mode 100644 index 000000000..38185dc5b --- /dev/null +++ b/src/data/schema/internalNote.js @@ -0,0 +1,22 @@ +export const types = ` + type InternalNote { + _id: String! + contentType: String! + contentTypeId: String + content: String + createdUserId: String + createdDate: Date + + createdUser: User + } +`; + +export const queries = ` + internalNotes(contentType: String!, contentTypeId: String): [InternalNote] +`; + +export const mutations = ` + internalNotesAdd(contentType: String!, contentTypeId: String, content: String): InternalNote + internalNotesEdit(_id: String!, content: String): InternalNote + internalNotesRemove(_id: String!): InternalNote +`; diff --git a/src/db/factories.js b/src/db/factories.js index 14a13c611..7d7d45b6b 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -6,6 +6,7 @@ import { Brands, EmailTemplates, ResponseTemplates, + InternalNotes, Customers, Forms, Fields, @@ -82,6 +83,16 @@ export const segmentFactory = (params = {}) => { return segment.save(); }; +export const internalNoteFactory = (params = {}) => { + const internalNote = new InternalNotes({ + contentType: params.contentType || 'customer', + contentTypeId: params.contentTypeId || 'DFASFDFSDAFDF', + content: params.content || faker.random.word(), + }); + + return internalNote.save(); +}; + export const companyFactory = (params = {}) => { const company = new Companies({ name: faker.random.word(), diff --git a/src/db/models/InternalNotes.js b/src/db/models/InternalNotes.js new file mode 100644 index 000000000..5807a956a --- /dev/null +++ b/src/db/models/InternalNotes.js @@ -0,0 +1,79 @@ +import mongoose from 'mongoose'; +import Random from 'meteor-random'; +import { INTERNAL_NOTE_CONTENT_TYPES } from '../../constants'; + +/* + * internal note schema + */ +const InternalNoteSchema = mongoose.Schema({ + _id: { + type: String, + unique: true, + default: () => Random.id(), + }, + contentType: { + type: String, + enum: INTERNAL_NOTE_CONTENT_TYPES.ALL_LIST, + }, + contentTypeId: String, + content: { + type: String, + }, + createdUserId: { + type: String, + }, + createdDate: { + type: Date, + }, +}); + +class InternalNote { + /* Create new internalNote + * + * @param {String} contentType form, customer, company + * @param {String} contentTypeId when contentType is form, it will be + * formId + * + * @return {Promise} newly created internalNote object + */ + static async createInternalNote({ contentType, contentTypeId, ...fields }, user) { + return this.create({ + contentType, + contentTypeId, + createdUserId: user._id, + createdDate: new Date(), + ...fields, + }); + } + + /* + * Update internalNote + * @param {String} _id internalNote id to update + * @param {Object} doc internalNote values to update + * @return {Promise} updated internalNote object + */ + static async updateInternalNote(_id, doc) { + await this.update({ _id }, { $set: doc }); + + return this.findOne({ _id }); + } + + /* + * Remove internalNote + * @param {String} _id internalNote id to remove + * @return {Promise} + */ + static async removeInternalNote(_id) { + const internalNoteObj = await this.findOne({ _id }); + + if (!internalNoteObj) throw new Error(`InternalNote not found with id ${_id}`); + + return internalNoteObj.remove(); + } +} + +InternalNoteSchema.loadClass(InternalNote); + +const InternalNotes = mongoose.model('internal_notes', InternalNoteSchema); + +export default InternalNotes; diff --git a/src/db/models/index.js b/src/db/models/index.js index 5f088575d..fdc1de6c8 100644 --- a/src/db/models/index.js +++ b/src/db/models/index.js @@ -8,6 +8,7 @@ import EngageMessages from './Engages'; import Tags from './Tags'; import Fields from './Fields'; import { Forms, FormFields } from './Forms'; +import InternalNotes from './InternalNotes'; import Customers from './Customers'; import Companies from './Companies'; import Segments from './Segments'; @@ -31,6 +32,7 @@ export { Tags, Fields, Segments, + InternalNotes, Customers, Companies, Conversations, From d3e92c05bb0471a2febeea2c4f9bede8fc743560 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 12 Oct 2017 18:59:52 +0800 Subject: [PATCH 043/318] #13 Refactor notifications and forms --- package.json | 1 + src/__tests__/formMutations.test.js | 483 ++++++++---------- src/__tests__/notificationMutations.test.js | 121 ++--- src/data/constants.js | 32 ++ src/data/resolvers/mutations/forms.js | 93 ++-- src/data/resolvers/mutations/notifications.js | 21 +- src/db/factories.js | 18 +- src/db/models/Forms.js | 130 +++-- src/db/models/Notifications.js | 50 +- yarn.lock | 7 + 10 files changed, 493 insertions(+), 463 deletions(-) diff --git a/package.json b/package.json index 61505777f..123aebf3f 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "faker": "^4.1.0", "husky": "^0.13.4", "jest": "^21.2.1", + "jest-tobetype": "^1.1.0", "lint-staged": "^3.6.0", "nodemon": "^1.11.0", "prettier": "^1.4.4" diff --git a/src/__tests__/formMutations.test.js b/src/__tests__/formMutations.test.js index dceaebbc5..d20ed954b 100644 --- a/src/__tests__/formMutations.test.js +++ b/src/__tests__/formMutations.test.js @@ -3,30 +3,28 @@ import { connect, disconnect } from '../db/connection'; import { userFactory, formFactory, formFieldFactory, integrationFactory } from '../db/factories'; -import { Forms, Users, FormFields } from '../db/models'; +import { Forms, Users, FormFields, Integrations } from '../db/models'; import mutations from '../data/resolvers/mutations'; +import toBeType from 'jest-tobetype'; + +expect.extend(toBeType); beforeAll(() => connect()); afterAll(() => disconnect()); describe('form creation tests', () => { let _user; - /** - * Testing with an _user object - */ + beforeEach(async () => { _user = await userFactory({}); }); - /** - * Deleting the data that was used in test - */ afterEach(async () => { await Users.remove({}); await Forms.remove({}); }); - test('form creation test without userId supplied', async () => { + test('testing if `createdUser must be supplied` error is working as intended', async () => { expect.assertions(1); try { @@ -35,150 +33,140 @@ describe('form creation tests', () => { description: 'Test form description', }); } catch (e) { - expect(e.message).toEqual('createdUserId must be supplied'); + expect(e.message).toEqual('createdUser must be supplied'); } }); - test('form creating tests', async () => { - let form = await Forms.createForm({ - title: 'Test form', - description: 'Test form description', - createdUserId: _user._id, - }); + test('check if form creation method is working successfully', async () => { + let form = await Forms.createForm( + { + title: 'Test form', + description: 'Test form description', + }, + _user._id, + ); form = await Forms.findOne({ _id: form._id }); expect(form.title).toBe('Test form'); expect(form.description).toBe('Test form description'); - expect(typeof form.code).toBe('string'); - expect(form.code.length).toEqual(6); - expect(typeof form.createdDate).toBe('object'); + expect(form.code).toBeType('string'); + expect(form.code.length).toBe(6); + // typeof form.createdDate is 'object' even though its Date + expect(form.createdDate).toBeType('object'); expect(form.createdUserId).toBe(_user._id); }); }); describe('form update tests', () => { let _user; + let _form; - /** - * Testing with an _user object - */ beforeEach(async () => { _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user }); }); - /** - * Deleting the data that was used in test - */ afterEach(async () => { await Users.remove({}); await Forms.remove({}); }); - test('form update tests', async () => { - const form = await Forms.createForm({ - title: 'Test form', - description: 'Test form description', - createdUserId: _user._id, - }); - - await Forms.updateForm(form._id, { + test('check if form update method is working successfully', async () => { + const doc = { title: 'Test form 2', description: 'Test form description 2', - }); + }; - const formAfterUpdate = await Forms.findOne({ _id: form._id }); - expect(formAfterUpdate.title).toBe('Test form 2'); - expect(formAfterUpdate.description).toBe('Test form description 2'); - expect(form.createdUserId).toBe(formAfterUpdate.createdUserId); - expect(form.code).toBe(formAfterUpdate.code); - expect(typeof form.createdDate).toBe('object'); + await Forms.updateForm(_form._id, doc); + + const formAfterUpdate = await Forms.findOne({ _id: _form._id }); + + expect(formAfterUpdate.title).toBe(doc.title); + expect(formAfterUpdate.description).toBe(doc.description); + expect(formAfterUpdate.createdUserId).toBe(_form.createdUserId); + expect(formAfterUpdate.code).toBe(_form.code); + expect(_form.createdDate).toBeType('object'); }); }); describe('form remove tests', async () => { let _user; - /** - * Testing with an _user object - */ beforeEach(async () => { _user = await userFactory({}); }); - /** - * Deleting the data that was used in test - */ afterEach(async () => { await Users.remove({}); await Forms.remove({}); }); - test('form removal test', async () => { - const form = await Forms.createForm({ - title: 'Test form', - description: 'Test form description', - createdUserId: _user._id, - }); + test('check whether form removal is working successfully', async () => { + const form = await Forms.createForm( + { + title: 'Test form', + description: 'Test form description', + }, + _user._id, + ); await Forms.removeForm(form._id); const formCount = await Forms.find({}).count(); + expect(formCount).toBe(0); }); }); -describe('test exception in form remove', async () => { +describe('test exception in remove form method', async () => { let _user; + let _form; - /** - * Testing with an _user object - */ beforeEach(async () => { _user = await userFactory({}); + + _form = await formFactory({ + title: 'Test form', + description: 'Test form description', + createdUserId: _user._id, + }); }); - /** - * Deleting the data that was used in test - */ afterEach(async () => { await Users.remove({}); await Forms.remove({}); await FormFields.remove({}); + await Integrations.remove({}); }); - test('try to remove form with fields in it', async () => { + test('check if errors are being thrown as intended', async () => { expect.assertions(2); - const form = await Forms.createForm({ - title: 'Test form', - description: 'Test form description', - createdUserId: _user._id, - }); - await FormFields.createFormField(form._id, { - type: 'shoutbox', + await formFieldFactory(_form._id, { + type: 'input', validation: 'number', text: 'form field text', description: 'form field description', }); try { - await Forms.removeForm(form._id); + await Forms.removeForm(_form._id); } catch (e) { expect(e.message).toEqual('You cannot delete this form. This form has some fields.'); } - await FormFields.remove({}); - await integrationFactory({ - formId: form._id, + formId: _form._id, formData: { loadType: 'shoutbox', fromEmail: 'test@erxes.io', }, }); + await FormFields.remove({}); + try { - await Forms.removeForm(form._id); + await Forms.removeForm(_form._id); } catch (e) { expect(e.message).toEqual('You cannot delete this form. This form used in integration.'); } @@ -188,41 +176,38 @@ describe('test exception in form remove', async () => { describe('add form field test', async () => { let _user; let _form; - /** - * Testing with an _user object and a _form object - */ + beforeEach(async () => { _user = await userFactory({}); _form = await formFactory({ createdUserId: _user._id }); }); - /** - * Deleting the data that was used in test - */ afterEach(async () => { await Users.remove({}); await FormFields.remove({}); await Forms.remove({}); }); - test('add form field test', async () => { - const newFormField = await FormFields.createFormField(_form._id, { + test('check whether form fields are being created successfully', async () => { + const doc = { type: 'input', validation: 'number', text: 'How old are you?', description: 'Form field description', options: ['This', 'should', 'not', 'be', 'here', 'tho'], isRequired: false, - }); + }; + + const newFormField = await FormFields.createFormField(_form._id, doc); expect(newFormField.formId).toEqual(_form._id); expect(newFormField.order).toEqual(0); - expect(newFormField.type).toEqual('input'); - expect(newFormField.validation).toEqual('number'); - expect(newFormField.text).toEqual('How old are you?'); - expect(newFormField.description).toEqual('Form field description'); - expect.arrayContaining(newFormField.options); - expect(newFormField.isRequired).toEqual(false); + expect(newFormField.type).toEqual(doc.type); + expect(newFormField.validation).toEqual(doc.validation); + expect(newFormField.text).toEqual(doc.text); + expect(newFormField.description).toEqual(doc.description); + expect(newFormField.options).toEqual(expect.arrayContaining(doc.options)); + expect(newFormField.isRequired).toEqual(doc.isRequired); }); }); @@ -230,43 +215,45 @@ describe('update form field test', async () => { let _user; let _form; let _form_field; - /** - * Testing with an _user object and a _form object - */ + beforeEach(async () => { _user = await userFactory({}); _form = await formFactory({ createdUserId: _user._id }); _form_field = await formFieldFactory(_form._id, {}); }); - /** - * Deleting the data that was used in test - */ afterEach(async () => { await Users.remove({}); await FormFields.remove({}); await Forms.remove({}); }); - test('update form field test', async () => { - let updatedFormField = await FormFields.updateFormField(_form_field._id, { - type: 'input 1', - validation: 'number 1', + test('check whether form fields are being updated successfully', async () => { + const doc = { + type: 'textarea', + validation: 'date', text: 'How old are you? 1', description: 'Form field description 1', options: ['This', 'should', 'not', 'be', 'here', 'tho', '1'], isRequired: true, - }); + }; + + await FormFields.updateFormField(_form_field._id, doc); + + let updatedFormField = await FormFields.findOne({ _id: _form_field._id }); - updatedFormField = await FormFields.findOne({ _id: _form_field._id }); expect(updatedFormField.formId).toEqual(_form._id); - expect(updatedFormField.type).toEqual('input 1'); - expect(updatedFormField.validation).toEqual('number 1'); - expect(updatedFormField.text).toEqual('How old are you? 1'); - expect(updatedFormField.description).toEqual('Form field description 1'); - expect.arrayContaining(updatedFormField.options); + expect(updatedFormField.type).toEqual(doc.type); + expect(updatedFormField.validation).toEqual(doc.validation); + expect(updatedFormField.text).toEqual(doc.text); + expect(updatedFormField.description).toEqual(doc.description); + expect(updatedFormField.isRequired).toBe(doc.isRequired); + + for (let item of doc.options) { + expect(updatedFormField.options).toContain(item); + } + expect(updatedFormField.options.length).toEqual(7); - expect(updatedFormField.isRequired).toEqual(true); }); }); @@ -274,26 +261,22 @@ describe('remove form field test', async () => { let _user; let _form; let _form_field; - /** - * Testing with an _user object and a _form object - */ + beforeEach(async () => { _user = await userFactory({}); _form = await formFactory({ createdUserId: _user._id }); _form_field = await formFieldFactory(_form._id, {}); }); - /** - * Deleting the data that was used in test - */ afterEach(async () => { await Users.remove({}); await FormFields.remove({}); await Forms.remove({}); }); - test('remove form field test', async () => { + test('check whether form fields are being removed successfully', async () => { await FormFields.removeFormField(_form_field._id); + expect(await FormFields.find({}).count()).toEqual(0); }); }); @@ -301,9 +284,10 @@ describe('remove form field test', async () => { describe('test of update order of form fields', async () => { let _user; let _form; - let _form_field; - let _form_field2; - let _form_field3; + let _formField; + let _formField2; + let _formField3; + /** * Testing with an _user object and a _form object with 3 fields in it * to test the setting the new order @@ -311,9 +295,9 @@ describe('test of update order of form fields', async () => { beforeEach(async () => { _user = await userFactory({}); _form = await formFactory({ createdUserId: _user._id }); - _form_field = await formFieldFactory(_form._id, {}); - _form_field2 = await formFieldFactory(_form._id, {}); - _form_field3 = await formFieldFactory(_form._id, {}); + _formField = await formFieldFactory(_form._id, {}); + _formField2 = await formFieldFactory(_form._id, {}); + _formField3 = await formFieldFactory(_form._id, {}); }); /** @@ -325,38 +309,38 @@ describe('test of update order of form fields', async () => { await Forms.remove({}); }); - test('test of update order of form fields', async () => { - expect(_form_field.order).toBe(0); - expect(_form_field2.order).toBe(1); - expect(_form_field3.order).toBe(2); + test('check whether order values on form fields are being updated successfully', async () => { + expect(_formField.order).toBe(0); + expect(_formField2.order).toBe(1); + expect(_formField3.order).toBe(2); const orderDictArray = [ - { id: _form_field3._id, order: 10 }, - { id: _form_field2._id, order: 9 }, - { id: _form_field._id, order: 8 }, + { _id: _formField3._id, order: 10 }, + { _id: _formField2._id, order: 9 }, + { _id: _formField._id, order: 8 }, ]; await Forms.updateFormFieldsOrder(orderDictArray); - const ff1 = await FormFields.findOne({ _id: _form_field3._id }); + const ff1 = await FormFields.findOne({ _id: _formField3._id }); + expect(ff1.order).toBe(10); - expect(ff1.text).toBe(_form_field3.text); + expect(ff1.text).toBe(_formField3.text); + + const ff2 = await FormFields.findOne({ _id: _formField2._id }); - const ff2 = await FormFields.findOne({ _id: _form_field2._id }); expect(ff2.order).toBe(9); - expect(ff2.text).toBe(_form_field2.text); + expect(ff2.text).toBe(_formField2.text); - const ff3 = await FormFields.findOne({ _id: _form_field._id }); + const ff3 = await FormFields.findOne({ _id: _formField._id }); expect(ff3.order).toBe(8); - expect(ff3.text).toBe(_form_field.text); + expect(ff3.text).toBe(_formField.text); }); }); describe('test of form duplication', () => { let _user; let _form; - /** - * Testing with an _user object and a _form object with 3 fields in it - */ + beforeEach(async () => { _user = await userFactory({}); _form = await formFactory({ createdUserId: _user._id }); @@ -365,21 +349,18 @@ describe('test of form duplication', () => { await formFieldFactory(_form._id, {}); }); - /** - * Deleting the data that was used in test - */ afterEach(async () => { await Users.remove({}); await FormFields.remove({}); await Forms.remove({}); }); - test('test of form duplication', async () => { + test('test whether form duplication method is working successfully', async () => { const duplicatedForm = await Forms.duplicate(_form._id); expect(duplicatedForm.title).toBe(`${_form.title} duplicated`); expect(duplicatedForm.description).toBe(_form.description); - expect(typeof duplicatedForm.code).toBe('string'); + expect(duplicatedForm.code).toBeType('string'); expect(duplicatedForm.code.length).toEqual(6); expect(duplicatedForm.createdUserId).toBe(_form.createdUserId); @@ -391,190 +372,140 @@ describe('test of form duplication', () => { }); }); -describe('mutations', () => { +describe('checking all form and formField mutations', () => { + let _user; + + beforeEach(async () => { + _user = await userFactory({}); + }); + afterEach(async () => { await Users.remove({}); - await Forms.remove({}); - await FormFields.remove({}); }); - test('mutation tests ', async () => { - const _user = await userFactory({}); + test(`testing all methods of form mutations`, async () => { + Forms.createForm = jest.fn(); - // mutations.formsCreate - let form = await mutations.formsCreate(null, { + let doc = { title: 'Test form', description: 'Test form description', - createdUserId: _user._id, - }); + }; - form = await Forms.findOne({ _id: form._id }); + // test mutations.formsCreate ============ + await mutations.formsCreate(null, doc, { user: _user }); - expect(form.title).toBe('Test form'); - expect(form.description).toBe('Test form description'); - expect(typeof form.code).toBe('string'); - expect(form.code.length).toEqual(6); - expect(typeof form.createdDate).toBe('object'); - expect(form.createdUserId).toBe(_user._id); + expect(Forms.createForm).toBeCalledWith(doc, _user); + expect(Forms.createForm.mock.calls.length).toBe(1); - // mutations.formsUpdate - await mutations.formsEdit(null, { - _id: form._id, + // mutations.formsUpdate ================ + doc = { + _id: 'test id', title: 'Test form 2', description: 'Test form description 2', - }); + }; + + Forms.updateForm = jest.fn(); - const formAfterUpdate = await Forms.findOne({ _id: form._id }); + await mutations.formsEdit(null, doc, { user: _user }); - expect(formAfterUpdate.title).toBe('Test form 2'); - expect(formAfterUpdate.description).toBe('Test form description 2'); - expect(form.createdUserId).toBe(formAfterUpdate.createdUserId); - expect(form.code).toBe(formAfterUpdate.code); - expect(typeof form.createdDate).toBe('object'); + const formId = doc._id; + delete doc._id; - // mutations.formsAddFormField - const newFormField = await mutations.formsAddFormField(null, { - formId: form._id, + expect(Forms.updateForm).toBeCalledWith(formId, doc); + expect(Forms.updateForm.mock.calls.length).toBe(1); + + // test mutations.formsAddFormField ================ + doc = { + formId, type: 'input', validation: 'number', text: 'How old are you?', description: 'Form field description', options: ['This', 'should', 'not', 'be', 'here', 'tho'], isRequired: false, - }); + }; - expect(newFormField.formId).toEqual(form._id); - expect(newFormField.order).toEqual(0); - expect(newFormField.type).toEqual('input'); - expect(newFormField.validation).toEqual('number'); - expect(newFormField.text).toEqual('How old are you?'); - expect(newFormField.description).toEqual('Form field description'); - expect.arrayContaining(newFormField.options); - expect(newFormField.isRequired).toEqual(false); - - // mutations.formsAddFormField - await mutations.formsEditFormField(null, { - _id: newFormField._id, + FormFields.createFormField = jest.fn(); + + await mutations.formsAddFormField(null, doc, { user: _user }); + + delete doc.formId; + + expect(FormFields.createFormField).toBeCalledWith(formId, doc); + expect(FormFields.createFormField.mock.calls.length).toBe(1); + + // test mutations.formsEditFormField =============== + doc = { + _id: 'test form field id', type: 'mutation input 1', validation: 'mutation number 1', text: 'mutation - How old are you? 1', description: 'mutation - Form field description 1', options: ['This', 'should', 'not', 'be', 'here', 'tho', '1'], isRequired: true, - }); + }; - const updatedFormField = await FormFields.findOne({ _id: newFormField._id }); + FormFields.updateFormField = jest.fn(); - expect(updatedFormField.formId).toEqual(form._id); - expect(updatedFormField.type).toEqual('mutation input 1'); - expect(updatedFormField.validation).toEqual('mutation number 1'); - expect(updatedFormField.text).toEqual('mutation - How old are you? 1'); - expect(updatedFormField.description).toEqual('mutation - Form field description 1'); - expect.arrayContaining(updatedFormField.options); - expect(updatedFormField.options.length).toEqual(7); - expect(updatedFormField.isRequired).toEqual(true); + await mutations.formsEditFormField(null, doc, { user: _user }); - // formsRemoveFormField - await mutations.formsRemoveFormField(null, { _id: newFormField._id }); + const formFieldId = doc._id; + delete doc._id; - expect(await FormFields.find({}).count()).toEqual(0); + expect(FormFields.updateFormField).toBeCalledWith(formFieldId, doc); + expect(FormFields.updateFormField.mock.calls.length).toBe(1); - // mutations.formsRemove - await mutations.formsRemove(null, { _id: form._id }); + // test formsRemoveFormField ================= + FormFields.removeFormField = jest.fn(); - expect(await Forms.find({}).count()).toEqual(0); - }); -}); - -describe('mutations 2', async () => { - let _user; - let _form; - let _form_field; - let _form_field2; - let _form_field3; - /** - * Testing with an _user object and a _form object with 3 fields in it - * to test the setting the new order - */ - beforeEach(async () => { - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - _form_field = await formFieldFactory(_form._id, {}); - _form_field2 = await formFieldFactory(_form._id, {}); - _form_field3 = await formFieldFactory(_form._id, {}); - }); + await mutations.formsRemoveFormField(null, { _id: formFieldId }, { user: _user }); - /** - * Deleting the data that was used in test - */ - afterEach(async () => { - await Users.remove({}); - await FormFields.remove({}); - await Forms.remove({}); - }); + expect(FormFields.removeFormField).toBeCalledWith(formFieldId); + expect(FormFields.removeFormField.mock.calls.length).toBe(1); - test('test of update order of form fields', async () => { - expect(_form_field.order).toBe(0); - expect(_form_field2.order).toBe(1); - expect(_form_field3.order).toBe(2); + // test mutations.formsRemove =========== + Forms.removeForm = jest.fn(); - const orderDictArray = [ - { id: _form_field3._id, order: 10 }, - { id: _form_field2._id, order: 9 }, - { id: _form_field._id, order: 8 }, - ]; + await mutations.formsRemove(null, { _id: formId }, { user: _user }); - await Forms.updateFormFieldsOrder(orderDictArray); - const ff1 = await FormFields.findOne({ _id: _form_field3._id }); - expect(ff1.order).toBe(10); - expect(ff1.text).toBe(_form_field3.text); + expect(Forms.removeForm).toBeCalledWith(formId); + expect(Forms.removeForm.mock.calls.length).toBe(1); + }); - const ff2 = await FormFields.findOne({ _id: _form_field2._id }); - expect(ff2.order).toBe(9); - expect(ff2.text).toBe(_form_field2.text); + test('check whether order value updating mutation is working successfully', async () => { + const doc = { + orderDics: [ + { + _id: 'test form field id', + order: 10, + }, + { + _id: 'test form field id 2', + order: 11, + }, + { + _id: 'test form field id 3', + order: 12, + }, + ], + }; - const ff3 = await FormFields.findOne({ _id: _form_field._id }); - expect(ff3.order).toBe(8); - expect(ff3.text).toBe(_form_field.text); - }); -}); + Forms.updateFormFieldsOrder = jest.fn(); -describe('mutations 3', () => { - let _user; - let _form; - /** - * Testing with an _user object and a _form object with 3 fields in it - */ - beforeEach(async () => { - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - await formFieldFactory(_form._id, {}); - await formFieldFactory(_form._id, {}); - await formFieldFactory(_form._id, {}); - }); + await mutations.formsUpdateFormFieldsOrder(null, doc, { user: _user }); - /** - * Deleting the data that was used in test - */ - afterEach(async () => { - await Users.remove({}); - await FormFields.remove({}); - await Forms.remove({}); + expect(Forms.updateFormFieldsOrder).toBeCalledWith(doc.orderDics); + expect(Forms.updateFormFieldsOrder.mock.calls.length).toBe(1); }); - test('test of form duplication', async () => { - const duplicatedForm = await mutations.formsDuplicate(null, { _id: _form._id }); + test('check whether form duplication mutation is working successfully', async () => { + const fakeId = 'fakeFormid'; - expect(duplicatedForm.title).toBe(`${_form.title} duplicated`); - expect(duplicatedForm.description).toBe(_form.description); - expect(typeof duplicatedForm.code).toBe('string'); - expect(duplicatedForm.code.length).toEqual(6); - expect(duplicatedForm.createdUserId).toBe(_form.createdUserId); + Forms.duplicate = jest.fn(); - const formFieldsCount = await FormFields.find({}).count(); - const duplicateFormFieldsCount = await FormFields.find({ formId: duplicatedForm._id }).count(); + await mutations.formsDuplicate(null, { _id: fakeId }, { user: _user }); - expect(formFieldsCount).toEqual(6); - expect(duplicateFormFieldsCount).toEqual(3); + expect(Forms.duplicate).toBeCalledWith(fakeId); + expect(Forms.duplicate.mock.calls.length).toBe(1); }); }); diff --git a/src/__tests__/notificationMutations.test.js b/src/__tests__/notificationMutations.test.js index 807caf07b..edef67ed4 100644 --- a/src/__tests__/notificationMutations.test.js +++ b/src/__tests__/notificationMutations.test.js @@ -149,6 +149,67 @@ describe('NotificationConfiguration model tests', async () => { }); }); +describe('testings helper methods', () => { + test('testing tools.sendNotification method', async () => { + const _user = await userFactory({}); + const _user2 = await userFactory({}); + const _user3 = await userFactory({}); + + // Try to send notifications when there is config not allowing it ========= + await notificationConfigurationFactory({ + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + isAllowed: false, + user: _user._id, + }); + + await notificationConfigurationFactory({ + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + isAllowed: false, + user: _user2._id, + }); + + await notificationConfigurationFactory({ + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + isAllowed: false, + user: _user3._id, + }); + + const doc = { + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + createdUser: _user._id, + title: 'new Notification title', + content: 'new Notification content', + link: 'new Notification link', + receivers: [_user._id, _user2._id, _user3._id], + }; + + await sendNotification(doc); + let notifications = await Notifications.find({}); + + expect(notifications.length).toEqual(0); + + // Send notifications when there is config allowing it ==================== + await NotificationConfigurations.update({}, { isAllowed: true }, { multi: true }); + + await sendNotification(doc); + + notifications = await Notifications.find({}); + + expect(notifications.length).toEqual(3); + + expect(notifications[0].notifType).toEqual(doc.notifType); + expect(notifications[0].createdUser).toEqual(doc.createdUser); + expect(notifications[0].title).toEqual(doc.title); + expect(notifications[0].content).toEqual(doc.content); + expect(notifications[0].link).toEqual(doc.link); + expect(notifications[0].receiver).toEqual(_user._id); + + expect(notifications[1].receiver).toEqual(_user2._id); + + expect(notifications[2].receiver).toEqual(_user3._id); + }); +}); + describe('testing mutations', () => { beforeEach(() => {}); @@ -164,65 +225,7 @@ describe('testing mutations', () => { expect(() => mutations.notificationsSaveConfig(null, {}, {})).toThrowError('Login required'); expect(() => mutations.notificationsMarkAsRead(null, {}, {})).toThrowError('Login required'); - }), - test('testing tools.sendNotification method', async () => { - const _user = await userFactory({}); - const _user2 = await userFactory({}); - const _user3 = await userFactory({}); - - // Try to send notifications when there is config not allowing it ========= - await notificationConfigurationFactory({ - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, - isAllowed: false, - user: _user._id, - }); - - await notificationConfigurationFactory({ - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, - isAllowed: false, - user: _user2._id, - }); - - await notificationConfigurationFactory({ - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, - isAllowed: false, - user: _user3._id, - }); - - const doc = { - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, - createdUser: _user._id, - title: 'new Notification title', - content: 'new Notification content', - link: 'new Notification link', - receivers: [_user._id, _user2._id, _user3._id], - }; - - await sendNotification(doc); - let notifications = await Notifications.find({}); - - expect(notifications.length).toEqual(0); - - // Send notifications when there is config allowing it ==================== - await NotificationConfigurations.update({}, { isAllowed: true }, { multi: true }); - - await sendNotification(doc); - - notifications = await Notifications.find({}); - - expect(notifications.length).toEqual(3); - - expect(notifications[0].notifType).toEqual(doc.notifType); - expect(notifications[0].createdUser).toEqual(doc.createdUser); - expect(notifications[0].title).toEqual(doc.title); - expect(notifications[0].content).toEqual(doc.content); - expect(notifications[0].link).toEqual(doc.link); - expect(notifications[0].receiver).toEqual(_user._id); - - expect(notifications[1].receiver).toEqual(_user2._id); - - expect(notifications[2].receiver).toEqual(_user3._id); - }); + }); test('testing if notification configuration is saved and updated successfully', async () => { NotificationConfigurations.createOrUpdateConfiguration = jest.fn(); diff --git a/src/data/constants.js b/src/data/constants.js index 4a8b99c40..df6ea6c70 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -55,3 +55,35 @@ export const MODULES = { 'conversationStateChange', ], }; + +export const FORM_FIELDS = { + TYPES: { + INPUT: 'input', + TEXT_AREA: 'textarea', + RADIO: 'radio', + CHECK: 'check', + SELECT: 'select', + DIVIDER: 'divider', + EMAIL: 'email', + FIRST_NAME: 'firstName', + LAST_NAME: 'lastName', + ALL: [ + 'input', + 'textarea', + 'radio', + 'check', + 'select', + 'divider', + 'email', + 'firstName', + 'lastName', + ], + }, + VALIDATION: { + BLANK: '', + NUMBER: 'number', + DATE: 'date', + EMAIL: 'email', + ALL: ['', 'number', 'date', 'email'], + }, +}; diff --git a/src/data/resolvers/mutations/forms.js b/src/data/resolvers/mutations/forms.js index 200cb85c8..c44630df7 100644 --- a/src/data/resolvers/mutations/forms.js +++ b/src/data/resolvers/mutations/forms.js @@ -3,12 +3,13 @@ import { Forms, FormFields } from '../../../db/models'; export default { /** * Create a new form - * @param {Object} - * @param {String} args2.title - * @param {String} args2.description - * @param {String} args3.user - * @return {Promise} returns the form - * @throws {Error} apollo level error based on validation + * @param {Object} root + * @param {Object} doc - Form object + * @param {string} doc.title - Form title + * @param {string} doc.description - Form description + * @param {Object} doc.user - The user who created this form + * @return {Promise} returns the form promise + * @throws {Error} throws apollo level error based on validation * @throws {Error} throws error if user is not logged in */ formsCreate(root, doc, { user }) { @@ -21,11 +22,13 @@ export default { /** * Update form data - * @param {Object} - * @param {String} doc._id - * @param {String} doc.title - * @param {String} doc.description - * @param {String} args3.user + * @param {Object} root + * @param {Object} object2 - Form object + * @param {string} object2._id - Form id + * @param {string} object2.title - Form title + * @param {string} object2.description - Form description + * @param {Object} object3 - The middleware data + * @param {Object} object3.user - The user who is making this action * @return {Promise} returns null * @throws {Error} apollo level error based on validation * @throws {Error} throws error if user is not logged in @@ -40,8 +43,11 @@ export default { /** * Remove a form - * @param {Object} - * @param {String} _id + * @param {Object} root + * @param {string} object2 - Graphql input data + * @param {string} object2._id - Form id + * @param {Object} object3 - The middleware data + * @param {Object} object3.user - The user making this action * @return {Promise} null * @throws {Error} apollo level error based on validation * @throws {Error} throws error if user is not logged in @@ -56,14 +62,17 @@ export default { /** * Adds a form field to the form - * @param {Object} - * @param {String} args.formId - * @param {String} args.type - * @param {String} args.validation - * @param {String} args.text - * @param {String} args.description - * @param {Array} args.options - * @param {Boolean} args.isRequired + * @param {Object} root + * @param {Object} object2 - Form object + * @param {string} object2.formId - Form id + * @param {string} object2.type - Form field type + * @param {string} object2.validation - Form field data validation type + * @param {string} object2.text - Form field text + * @param {string} object2.description - Form field description + * @param {Array} object2.options - Form field options + * @param {Boolean} object2.isRequired - Shows whether the field is required or not + * @param {Object} object3 - Middleware data + * @param {Object} object3.user - * @return {Promise} return Promise(null) * @throws {Error} throws apollo error based on validation * @throws {Error} throws error if user is not logged in @@ -77,13 +86,17 @@ export default { }, /** - * @param {String} args._id form field id - * @param {String} args.type - * @param {String} args.validation - * @param {String} args.text - * @param {String} args.description - * @param {Array} args.options - * @param {Boolean} args.isRequired + * @param {Object} root + * @param {string} object2 - Form field object + * @param {string} object2._id - Form field id + * @param {string} object2.type - Form field type + * @param {string} object2.validation - Form field data validation type + * @param {string} object2.text - Form field text + * @param {string} object2.description - Form field description + * @param {Array} object2.options - Form field options for select type + * @param {Boolean} object2.isRequired + * @param {Object} object3 - Middleware data + * @param {Object} object3.user - The user making this action * @return {Promise} return Promise(null) * @throws {Error} throws apollo error based on validation * @throws {Error} throws error if user is not logged in @@ -97,9 +110,12 @@ export default { }, /** - * Remove a channel - * @param {Object} - * @param {String} _id + * Remove a form field + * @param {Object} root + * @param {Object} object2 - Graphql input data + * @param {string} object2._id - Form field id + * @param {Object} object3 - Middleware data + * @param {Object} object3.user - The user making this action * @return {Promise} null * @throws {Error} throws apollo error based on validation * @throws {Error} throws error if user is not logged in @@ -114,9 +130,11 @@ export default { /** * Rearranges order based on given value - * @param {Object} - * @param {String} args.orderDics.id - * @param {String} args.orderDics.order + * @param {Object} root + * @param {Object} object2 - Graphql input data + * @param {Object} object2.orderDics - Dictionary containing order values for form fields + * @param {Object} object3 - The middleware data + * @param {Object} object3.user - The user making this action * @return {Promise} null * @throws {Error} throws apollo error based on validation * @throws {Error} throws error if user is not logged in @@ -131,8 +149,11 @@ export default { /** * Duplicates the form and its fields - * @param {Object} - * @param {String} args._id + * @param {Object} root + * @param {Object} object2 - Graphql input data + * @param {string} object2._id - Form id + * @param {Object} object3 - Middleware data + * @param {Object} object3.user - The user making this action * @return {Promise} returns form object * @throws {Error} throws apollo error based on validation * @throws {Error} throws error if user is not logged in diff --git a/src/data/resolvers/mutations/notifications.js b/src/data/resolvers/mutations/notifications.js index b3e253ad6..29044efa4 100644 --- a/src/data/resolvers/mutations/notifications.js +++ b/src/data/resolvers/mutations/notifications.js @@ -3,12 +3,13 @@ import { NotificationConfigurations, Notifications } from '../../../db/models'; export default { /** * Save notification configuration - * @param {Object} args1 - * @param {String} args2.notifType - * @param {Boolean} args2.isAllowed - * @param {String} args3.user + * @param {Object} object + * @param {Object} object - NotificationConfiguration object + * @param {string} object2.notifType - Notification configuration notification type (module) + * @param {Boolean} object2.isAllowed - Shows whether notifications will be received or not + * @param {Object} object3 - Middleware data + * @param {Object} object3.user - The user making this action * @return {Promise} returns notification promise - * @throws {Error} apollo level error based on validation * @throws {Error} throws error if user is not logged in */ notificationsSaveConfig(root, doc, { user }) { @@ -20,11 +21,13 @@ export default { }, /** - * Create a new messenger integration + * Marks notification as read * @param {Object} - * @param {String} args.ids - * @return {Promise} returns the messenger integration - * @throws {Error} apollo level error based on validation + * @param {Object} object2 - Graphql input data + * @param {string} object2.ids - Notification ids + * @param {Object} object3 - Middleware data + * @param {Object} object3.user - The user making this action + * @return {Promise} * @throws {Error} throws error if user is not logged in */ notificationsMarkAsRead(root, { ids }, { user }) { diff --git a/src/db/factories.js b/src/db/factories.js index 787a2812a..4651a936c 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -87,20 +87,22 @@ export const tagsFactory = (params = {}) => { return tag.save(); }; -export const formFactory = ({ title, code, createdUserId }) => { - return Forms.createForm({ - title: title || faker.random.word(), - description: faker.random.word(), - code: code || Random.id(), +export const formFactory = ({ title, code, description, createdUserId }) => { + return Forms.createForm( + { + title: title || faker.random.word(), + description: description || faker.random.word(), + code: code || Random.id(), + }, createdUserId, - }); + ); }; export const formFieldFactory = (formId, params) => { return FormFields.createFormField(formId || Random.id(), { - type: params.type || faker.random.word(), + type: params.type || 'input', name: faker.random.word(), - validation: params.validation || faker.random.word(), + validation: params.validation || 'number', text: faker.random.word(), description: faker.random.word(), isRequired: params.isRequired || false, diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index 1379a8944..13a7567a4 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -1,6 +1,7 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; import Integrations from './Integrations'; +import { FORM_FIELDS } from '../../data/constants'; // schema for form document const FormSchema = mongoose.Schema({ @@ -9,7 +10,10 @@ const FormSchema = mongoose.Schema({ default: () => Random.id(), }, title: String, - description: String, + description: { + type: String, + required: false, + }, code: String, createdUserId: String, createdDate: { @@ -20,8 +24,8 @@ const FormSchema = mongoose.Schema({ class Form { /** - * Generates a randomly generated and unique 6 letter code - * @return {String} random code + * Generates a random and unique 6 letter code + * @return {string} random code */ static async generateCode() { let code; @@ -29,7 +33,6 @@ class Form { do { code = Random.id().substr(0, 6); - foundForm = await Forms.findOne({ code }); } while (foundForm); @@ -38,32 +41,34 @@ class Form { /** * Creates a form document - * @param {String} doc.title - * @param {String} doc.description - * @param {Date} doc.createdDate - * @param {String} createdUserId - * @return {Object} returns Form document - * @throws {Error} throws Error if createdUserId is not supplied + * @param {Object} doc - Form object + * @param {string} doc.title - Form title + * @param {string} doc.description - Form description + * @param {Date} doc.createdDate - Form creation date + * @param {Object|string} createdUser - the user who created this form, + * can be both user id or user object + * @return {Promise} returns Form document promise + * @throws {Error} throws Error if createdUser is not supplied */ - static async createForm(doc, createdUserId) { - if (!createdUserId) { - throw new Error('createdUserId must be supplied'); + static async createForm(doc, createdUser) { + if (!createdUser) { + throw new Error('createdUser must be supplied'); } doc.code = await this.generateCode(); - doc.createdDate = new Date(); + doc.createdUserId = createdUser; return this.create(doc); } /** * Updates a form document - * @param {String} _id - * @param {String} args.title - * @param {String} args.description - * @return {Object} returns Form document - * @throws {Error} + * @param {string} _id - Form id + * @param {Object} object - Form object + * @param {string} object.title - Form title + * @param {string} object.description - Form description + * @return {Promise} returns null */ static updateForm(_id, { title, description }) { return this.update({ _id }, { $set: { title, description } }, { runValidators: true }); @@ -71,8 +76,8 @@ class Form { /** * Remove a form - * @param {String} _id - * @return {Promise} + * @param {string} _id - Form document id + * @return {Promise} returns null * @throws {Error} throws Error if this form has fields or if used in an integration */ static async removeForm(_id) { @@ -93,9 +98,10 @@ class Form { /** * Update order fields of form fields - * @param {String} orderDics[]._id - * @param {String} orderDics[].order - * @return {Null} + * @param {Object[]} orderDics - dictionary containing order values with user ids + * @param {string} orderDics[]._id - _id of FormField + * @param {string} orderDics[].order - order of FormField + * @return {Promise} null */ static async updateFormFieldsOrder(orderDics) { // update each field's order @@ -106,18 +112,20 @@ class Form { /** * Duplicates form and form fields of the form - * @param {String} _id form id - * @return {Object} returns the duplicated copy of the form + * @param {string} _id - form id + * @return {FormField} - returns the duplicated copy of the form */ static async duplicate(_id) { const form = await this.findOne({ _id }); // duplicate form - const newForm = await this.createForm({ - title: `${form.title} duplicated`, - description: form.description, - createdUserId: form.createdUserId, - }); + const newForm = await this.createForm( + { + title: `${form.title} duplicated`, + description: form.description, + }, + form.createdUserId, + ); // duplicate fields const formFields = await FormFields.find({ formId: _id }); @@ -147,27 +155,43 @@ const FormFieldSchema = mongoose.Schema({ type: String, default: () => Random.id(), }, - type: String, // TODO: change to enum - validation: String, // TODO: check if can be enum + type: { + type: String, + enum: FORM_FIELDS.TYPES.ALL, + }, + validation: { + type: String, + enum: FORM_FIELDS.VALIDATION.ALL, + }, text: String, - description: String, - options: [String], + description: { + type: String, + required: false, + }, + options: { + type: [String], + required: false, + }, isRequired: Boolean, formId: String, - order: Number, + order: { + type: Number, + required: false, + }, }); class FormField { /** * Creates a new form field document - * @param {String} formId id of the form document - * @param {String} doc.type - * @param {String} doc.validation - * @param {String} doc.text - * @param {String} doc.description - * @param {Array} doc.options - * @param {Boolean} doc.isRequired - * @return {Promise} returns form document promise + * @param {string} formId - Form id + * @param {Object} doc - FormField document object + * @param {string} doc.type - The type of form field (input, textarea, ...) + * @param {string} doc.validation - The type of data to validate to (nummber, date, ...) + * @param {string} doc.text - FormField text + * @param {string} doc.description - FormField description + * @param {String[]} doc.options - FormField select options (checkbox, radion buttons, ...) + * @param {Boolean} doc.isRequired - checks whether value is filled or not on validation + * @return {Promise} - returns form field document promise */ static async createFormField(formId, doc) { const lastField = await FormFields.findOne({}, { order: 1 }, { sort: { order: -1 } }); @@ -182,13 +206,15 @@ class FormField { /** * Update a form field document - * @param {String} _id id of the form document - * @param {String} doc.type - * @param {String} doc.validation - * @param {String} doc.text - * @param {String} doc.description - * @param {Array} doc.options - * @param {Boolean} doc.isRequired + * @param {string} _id - id of the form document + * @param {Object} doc - FormField document or object + * @param {string} doc.type - The type of form field (input, textarea, etc...) + * @param {string} doc.validation - The type of data to validate to (nummber, date, ...) + * @param {Number} doc.order - FormField order value + * @param {string} doc.text - FormField text + * @param {string} doc.description - FormField description + * @param {String[]} doc.options - FormField select options (checkbox, radion buttons, ...) + * @param {Boolean} doc.isRequired checks whether value is filled or not on validation * @return {Promise} */ static updateFormField(_id, doc) { @@ -197,7 +223,7 @@ class FormField { /** * Remove form field - * @param {String} _id + * @param {string} _id - FormField id * @return {Promise} */ static removeFormField(_id) { diff --git a/src/db/models/Notifications.js b/src/db/models/Notifications.js index 8293141df..320b98291 100644 --- a/src/db/models/Notifications.js +++ b/src/db/models/Notifications.js @@ -31,7 +31,7 @@ const NotificationSchema = new mongoose.Schema({ class Notification { /** * Marks notifications as read - * @param {Array} ids + * @param {String[]} ids - Notification ids * @return {Promise} */ static markAsRead(ids) { @@ -40,12 +40,13 @@ class Notification { /** * Create a notification - * @param {String} doc.notifType - * @param {String} doc.createdUser - * @param {String} doc.title - * @param {String} doc.content - * @param {String} doc.link - * @param {String} doc.receiver + * @param {Object} doc - Notification object + * @param {string} doc.notifType - Category of notification (module) + * @param {string} doc.title - Notificaton title + * @param {string} doc.content - Notification content + * @param {string} doc.link - Notification link + * @param {Object|string} doc.receiver - Id of the user that will receive this notification + * @param {Object|string} createdUser - The user whose actions made this notification * @return {Notification} Notification Object * @throws {Exception} throws Exception if createdUser is not supplied */ @@ -73,14 +74,15 @@ class Notification { /** * Update a notification - * @param {String} _id - * @param {String} doc.notifType - * @param {String} doc.createdUser - * @param {String} doc.title - * @param {String} doc.content - * @param {String} doc.link - * @param {String} doc.receiver - * @return {Promise} + * @param {string} _id - Id of notification + * @param {Object} doc - Notification object + * @param {string} doc.notifType - Category of notification (module) + * @param {string} doc.createdUser - The user whose actions made this notification + * @param {string} doc.title - Notificaton title + * @param {string} doc.content - Notification content + * @param {string} doc.link - Notification link + * @param {Object|string} doc.receiver - Id of the user that will receive this notification + * @return {Promise} The promise returns null */ static updateNotification(_id, doc) { return this.update({ _id }, doc); @@ -88,7 +90,7 @@ class Notification { /** * Remove a notification - * @param {String} _id + * @param {string} _id - Notification id * @return {Promise} */ static removeNotification(_id) { @@ -117,18 +119,20 @@ const ConfigSchema = new mongoose.Schema({ class Configuration { /** - * creates an new notification or updates already existing notification configuration - * @param {String} args1.notifType - * @param {Boolean} args1.isAllowed - * @param {String} user - * @return {Object} returns NotificationConfigurations object + * Creates an new notification or updates already existing notification configuration + * @param {object} object - NotificationConfiguration object + * @param {string} object.notifType - NotificationType (module) + * @param {Boolean} object.isAllowed - Indicates whether notifications + * will be received or not on the given channel + * @param {Object|string} user - The object or id of the user making this action + * @return {Promise} returns NotificationConfigurations object promise */ static async createOrUpdateConfiguration({ notifType, isAllowed }, user) { if (!user) { throw new Error('user must be supplied'); } - const selector = { user: user, notifType }; + const selector = { user, notifType }; const oldOne = await this.findOne(selector); @@ -142,7 +146,7 @@ class Configuration { // If it is first time then insert selector.isAllowed = isAllowed; - return await this.create(selector); + return this.create(selector); } } diff --git a/yarn.lock b/yarn.lock index daa58f247..0a1aa0fa7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2603,6 +2603,13 @@ jest-snapshot@^21.2.1: natural-compare "^1.4.0" pretty-format "^21.2.1" +jest-tobetype@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jest-tobetype/-/jest-tobetype-1.1.0.tgz#85af0d8ab7252399a41588e0ec16d299deef9c9a" + dependencies: + jest-get-type "^21.2.0" + jest-matcher-utils "^21.2.1" + jest-util@^21.2.1: version "21.2.1" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-21.2.1.tgz#a274b2f726b0897494d694a6c3d6a61ab819bb78" From f6372d37e8b3c36d6030f8993a339f38416b2876 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 12 Oct 2017 20:06:01 +0800 Subject: [PATCH 044/318] #13 Fix bug --- src/__tests__/notificationMutations.test.js | 4 ++-- src/__tests__/notificationQueries.test.js | 4 ++-- src/db/models/Notifications.js | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/__tests__/notificationMutations.test.js b/src/__tests__/notificationMutations.test.js index edef67ed4..9b814d887 100644 --- a/src/__tests__/notificationMutations.test.js +++ b/src/__tests__/notificationMutations.test.js @@ -30,13 +30,13 @@ describe('Notification tests', () => { await notificationConfigurationFactory({ user: _user2._id, - notifType: 'channelMembersChange', + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, isAllowed: false, }); // Create notification let doc = { - notifType: 'channelMembersChange', + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, title: 'new Notification title', content: 'new Notification content', link: 'new Notification link', diff --git a/src/__tests__/notificationQueries.test.js b/src/__tests__/notificationQueries.test.js index fb54ebb2f..1b705f3eb 100644 --- a/src/__tests__/notificationQueries.test.js +++ b/src/__tests__/notificationQueries.test.js @@ -1,7 +1,7 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { MODULE_LIST } from '../data/constants'; +import { MODULES } from '../data/constants'; import queries from '../data/resolvers/queries'; beforeAll(() => connect()); @@ -11,6 +11,6 @@ describe('notification query test', () => { test('test of getting notification list with success', () => { const modules = queries.notificationsModules(); - expect(modules).toEqual(MODULE_LIST); + expect(modules).toEqual(MODULES.ALL); }); }); diff --git a/src/db/models/Notifications.js b/src/db/models/Notifications.js index 320b98291..727ff0030 100644 --- a/src/db/models/Notifications.js +++ b/src/db/models/Notifications.js @@ -1,6 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -import { MODULE_LIST } from '../../data/constants'; +import { MODULES } from '../../data/constants'; // Notification schema const NotificationSchema = new mongoose.Schema({ @@ -11,7 +11,7 @@ const NotificationSchema = new mongoose.Schema({ }, notifType: { type: String, - enum: MODULE_LIST, + enum: MODULES.ALL, }, title: String, link: String, @@ -112,7 +112,7 @@ const ConfigSchema = new mongoose.Schema({ user: String, notifType: { type: String, - enum: MODULE_LIST, + enum: MODULES.ALL, }, isAllowed: Boolean, }); From 95811005d15563ad9c98977353cb550a96e214f6 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Thu, 12 Oct 2017 20:48:02 +0800 Subject: [PATCH 045/318] Test login required --- package.json | 3 +- ...essages.test.js => engageMessages.test.js} | 43 ++++- src/__tests__/tags.test.js | 66 +++++++- src/data/constants.js | 30 ++++ src/data/resolvers/mutations/engageUtils.js | 149 ++++++++++++++++++ src/data/resolvers/mutations/engages.js | 40 +++-- src/data/resolvers/mutations/tags.js | 142 +++++++++-------- src/data/schema/engage.js | 2 +- src/data/schema/tag.js | 13 +- src/db/factories.js | 4 +- src/db/models/Engages.js | 40 ++++- src/db/models/Tags.js | 10 +- 12 files changed, 434 insertions(+), 108 deletions(-) rename src/__tests__/{engage_messages.test.js => engageMessages.test.js} (83%) create mode 100644 src/data/resolvers/mutations/engageUtils.js diff --git a/package.json b/package.json index aa885014c..d629e8f27 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "jest": "^21.2.1", "lint-staged": "^3.6.0", "nodemon": "^1.11.0", - "prettier": "^1.4.4", - "shortid": "^2.2.8" + "prettier": "^1.4.4" } } diff --git a/src/__tests__/engage_messages.test.js b/src/__tests__/engageMessages.test.js similarity index 83% rename from src/__tests__/engage_messages.test.js rename to src/__tests__/engageMessages.test.js index f06d58d6e..bdcadffa7 100644 --- a/src/__tests__/engage_messages.test.js +++ b/src/__tests__/engageMessages.test.js @@ -87,6 +87,7 @@ describe('mutations', () => { let _user; let _segment = segmentsFactory(); let _doc = null; + let messageId; beforeEach(async () => { _user = await userFactory({}); @@ -108,7 +109,7 @@ describe('mutations', () => { }); test('messages add', async () => { - const _message = await mutations.messagesAdd(null, _doc); + const _message = await mutations.messagesAdd(null, _doc, { user: _user }); expect(_message.kind).toEqual(_doc.kind); expect(_message.title).toEqual(_doc.title); expect(_message.fromUserId).toEqual(_user._id); @@ -117,14 +118,24 @@ describe('mutations', () => { expect(_message.isDraft).toEqual(_doc.isDraft); }); + test('Create message login required', async () => { + expect.assertions(1); + try { + await mutations.messagesAdd({}, _doc, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + test('messages edit', async () => { let message = await EngageMessages.createMessage(_doc); + messageId = message._id; _doc.title = 'Message test updated'; _doc.isLive = false; _doc.isDraft = true; - await mutations.messageEdit(null, { _id: message._id, ..._doc }); + await mutations.messageEdit(null, { _id: message._id, ..._doc }, { user: _user }); message = await EngageMessages.findOne({ _id: message._id }); expect(message.kind).toEqual(_doc.kind); @@ -135,23 +146,41 @@ describe('mutations', () => { expect(message.isDraft).toEqual(_doc.isDraft); }); + test('Update message login required', async () => { + expect.assertions(1); + try { + await mutations.messagesAdd({}, { _id: messageId, ..._doc }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + test('messages remove', async () => { const _message = await EngageMessages.createMessage(_doc); + messageId = _message._id; - const removeResult = await mutations.messagesRemove(null, _message._id); - expect(removeResult).toBe(true); + await mutations.messagesRemove(null, _message._id, { user: _user }); const messagesCounts = await EngageMessages.find({}).count(); expect(messagesCounts).toBe(0); }); + test('Remove message login required', async () => { + expect.assertions(1); + try { + await mutations.messagesAdd({}, messageId, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + test('set live', async () => { _doc.isLive = false; _doc.isDraft = true; let _message = await EngageMessages.createMessage(_doc); - _message = await mutations.messagesSetLive(null, _message._id); + _message = await mutations.messagesSetLive(null, _message._id, { user: _user }); expect(_message.isLive).toEqual(true); expect(_message.isDraft).toEqual(false); }); @@ -161,7 +190,7 @@ describe('mutations', () => { let _message = await EngageMessages.createMessage(_doc); - _message = await mutations.messagesSetPause(null, _message._id); + _message = await mutations.messagesSetPause(null, _message._id, { user: _user }); expect(_message.isLive).toEqual(false); }); @@ -171,7 +200,7 @@ describe('mutations', () => { let _message = await EngageMessages.createMessage(_doc); - _message = await mutations.messagesSetLiveManual(null, _message._id); + _message = await mutations.messagesSetLiveManual(null, _message._id, { user: _user }); expect(_message.isLive).toEqual(true); expect(_message.isDraft).toEqual(false); }); diff --git a/src/__tests__/tags.test.js b/src/__tests__/tags.test.js index d22fb2119..6b4ca15b1 100644 --- a/src/__tests__/tags.test.js +++ b/src/__tests__/tags.test.js @@ -2,8 +2,8 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { Tags, Users } from '../db/models'; -import { tagsFactory, userFactory } from '../db/factories'; +import { Tags, Users, EngageMessages } from '../db/models'; +import { tagsFactory, userFactory, segmentsFactory } from '../db/factories'; import tagsMutations from '../data/resolvers/mutations/tags'; beforeAll(() => connect()); @@ -13,27 +13,42 @@ afterAll(() => disconnect()); describe('Tags mutations', () => { let _tag; let _user; + let _segment; beforeEach(async () => { // Creating test data _tag = await tagsFactory(); _user = await userFactory(); + _segment = await segmentsFactory({}); }); afterEach(async () => { // Clearing test data await Tags.remove({}); await Users.remove({}); + await EngageMessages.remove({}); }); test('Create tag', async () => { const tagObj = await tagsMutations.tagsAdd( {}, - { name: `${_tag.name}1`, type: `${_tag.type}1`, colorCode: _tag.colorCode }, + { name: `${_tag.name}1`, type: _tag.type, colorCode: _tag.colorCode }, { user: _user }, ); expect(tagObj).toBeDefined(); + expect(tagObj.name).toEqual(`${_tag.name}1`); + expect(tagObj.type).toEqual(_tag.type); + expect(tagObj.colorCode).toEqual(_tag.colorCode); + }); + + test('Create tag login required', async () => { + expect.assertions(1); + try { + await tagsMutations.tagsAdd({}, { type: _tag.type, name: _tag.name }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } }); test('Update tag', async () => { @@ -44,10 +59,55 @@ describe('Tags mutations', () => { ); expect(tagObj).toBeDefined(); + expect(tagObj.name).toEqual(_tag.name); + expect(tagObj.type).toEqual(_tag.type); + expect(tagObj.colorCode).toEqual(_tag.colorCode); + }); + + test('Update tag login required', async () => { + expect.assertions(1); + try { + await tagsMutations.tagsEdit({}, { _id: _tag.id, name: _tag.name }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } }); test('Delete tag', async () => { const isDeleted = await tagsMutations.tagsRemove({}, { ids: [_tag.id] }, { user: _user }); expect(isDeleted).toBeTruthy(); }); + + test('Remove tag login required', async () => { + expect.assertions(1); + try { + await tagsMutations.tagsRemove({}, { ids: [_tag.id] }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Tags tag', async () => { + const doc = { + kind: 'manual', + title: 'Message test', + fromUserId: _user._id, + segmentId: _segment._id, + isLive: true, + isDraft: false, + }; + + const message = await EngageMessages.createMessage(doc); + await tagsMutations.tagsTag( + {}, + { type: 'engageMessage', targetIds: [message._id], tagIds: [_tag._id] }, + { user: _user }, + ); + + const messageObj = await EngageMessages.findOne({ _id: message._id }); + const tagObj = await Tags.findOne({ _id: _tag._id }); + + expect(tagObj.objectCount).toBe(1); + expect(messageObj.tagIds[0]).toEqual(_tag.id); + }); }); diff --git a/src/data/constants.js b/src/data/constants.js index c06f60b97..c0107d084 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -1,3 +1,6 @@ +export const EMAIL_CONTENT_CLASS = 'erxes-email-content'; +export const EMAIL_CONTENT_PLACEHOLDER = `
`; + export const CONVERSATION_STATUSES = { NEW: 'new', OPEN: 'open', @@ -19,3 +22,30 @@ export const TAG_TYPES = { ENGAGE_MESSAGE: 'engageMessage', ALL_LIST: ['conversation', 'customer', 'engageMessage'], }; + +export const MESSENGER_KINDS = { + CHAT: 'chat', + NOTE: 'note', + POST: 'post', + ALL_LIST: ['chat', 'note', 'post'], +}; + +export const SENT_AS_CHOICES = { + BADGE: 'badge', + SNIPPET: 'snippet', + FULL_MESSAGE: 'fullMessage', + ALL_LIST: ['badge', 'snippet', 'fullMessage'], +}; + +export const MESSAGE_KINDS = { + AUTO: 'auto', + VISITOR_AUTO: 'visitorAuto', + MANUAL: 'manual', + ALL_LIST: ['auto', 'visitorAuto', 'manual'], +}; + +export const METHODS = { + MESSENGER: 'messenger', + EMAIL: 'email', + ALL_LIST: ['messenger', 'email'], +}; diff --git a/src/data/resolvers/mutations/engageUtils.js b/src/data/resolvers/mutations/engageUtils.js new file mode 100644 index 000000000..3b7777a45 --- /dev/null +++ b/src/data/resolvers/mutations/engageUtils.js @@ -0,0 +1,149 @@ +import { EngageMessages, Customers, Users, EmailTemplates, Integrations } from '../../../db/models'; +import { + EMAIL_CONTENT_PLACEHOLDER, + METHODS, + MESSAGE_KINDS, + INTEGRATION_KIND_CHOICES, +} from '../../constants'; +import Random from 'meteor-random'; + +export const replaceKeys = ({ content, customer, user }) => { + let result = content; + + // replace customer fields + result = result.replace(/{{\s?customer.name\s?}}/gi, customer.name); + result = result.replace(/{{\s?customer.email\s?}}/gi, customer.email); + + // replace user fields + result = result.replace(/{{\s?user.fullName\s?}}/gi, user.fullName); + result = result.replace(/{{\s?user.position\s?}}/gi, user.position); + result = result.replace(/{{\s?user.email\s?}}/gi, user.email); + + return result; +}; + +const findCustomers = ({ customerIds, segmentId }) => { + // find matched customers + let customerQuery = { _id: { $in: customerIds || [] } }; + + // TODO + // if (segmentId) { + // customerQuery = customerQueryBuilder.segments(Segments.findOne(segmentId)); + // } + + return Customers.find(customerQuery).fetch(); +}; + +const saveMatchedCustomerIds = (messageId, customers) => + EngageMessages.update( + { _id: messageId }, + { $set: { customerIds: customers.map(customer => customer._id) } }, + ); + +const sendViaEmail = message => { + const { fromUserId, segmentId, customerIds } = message; + const { templateId, subject, content } = message.email; + + const user = Users.findOne(fromUserId); + const userEmail = user.emails.pop(); + const template = EmailTemplates.findOne(templateId); + + // find matched customers + const customers = findCustomers({ customerIds, segmentId }); + + // save matched customer ids + saveMatchedCustomerIds(message._id, customers); + + customers.forEach(customer => { + // replace keys in subject + const replacedSubject = replaceKeys({ content: subject, customer, user }); + + // replace keys such as {{ customer.name }} in content + let replacedContent = replaceKeys({ content, customer, user }); + + // if sender choosed some template then use it + if (template) { + replacedContent = template.content.replace(EMAIL_CONTENT_PLACEHOLDER, replacedContent); + } + + const mailMessageId = Random.id(); + + // add new delivery report + EngageMessages.update( + { _id: message._id }, + { + $set: { + [`deliveryReports.${mailMessageId}`]: { + customerId: customer._id, + status: 'pending', + }, + }, + }, + ); + + // TODO send email + }); +}; + +const sendViaMessenger = message => { + const { fromUserId, segmentId, customerIds } = message; + const { brandId, content } = message.messenger; + + const user = Users.findOne(fromUserId); + + // find integration + const integration = Integrations.findOne({ + brandId, + kind: INTEGRATION_KIND_CHOICES.MESSENGER, + }); + + if (!integration) { + return 'Integration not found'; + } + + // find matched customers + const customers = findCustomers({ customerIds, segmentId }); + + // save matched customer ids + saveMatchedCustomerIds(message._id, customers); + + // TODO + // customers.forEach(customer => { + // // replace keys in content + // const replacedContent = replaceKeys({ content, customer, user }); + + // // create conversation + // const conversationId = createConversation({ + // userId: fromUserId, + // customerId: customer._id, + // integrationId: integration._id, + // content: replacedContent, + // }); + + // // create message + // createMessage({ + // engageData: { + // messageId: message._id, + // fromUserId, + // ...message.messenger, + // }, + // conversationId, + // userId: fromUserId, + // customerId: customer._id, + // content: replacedContent, + // }); + // }); +}; + +export const send = message => { + const { method, kind } = message; + + if (method === METHODS.EMAIL) { + return sendViaEmail(message); + } + + // when kind is visitor auto, do not do anything + if (method === METHODS.MESSENGER && kind !== MESSAGE_KINDS.VISITOR_AUTO) { + return sendViaMessenger(message); + } +}; diff --git a/src/data/resolvers/mutations/engages.js b/src/data/resolvers/mutations/engages.js index 46b00d746..9bcb58fc9 100644 --- a/src/data/resolvers/mutations/engages.js +++ b/src/data/resolvers/mutations/engages.js @@ -16,8 +16,10 @@ export default { * @param {[String]} doc.tagIds * @return {Promise} message object */ - async messagesAdd(root, doc) { - return await EngageMessages.createMessage(doc); + messagesAdd(root, doc, { user }) { + if (!user) throw new Error('Login required'); + + return EngageMessages.createMessage(doc); }, /** @@ -35,10 +37,12 @@ export default { * @param {[String]} doc.tagIds * @return {Promise} message object */ - async messageEdit(root, { _id, ...doc }) { + async messageEdit(root, { _id, ...doc }, { user }) { + if (!user) throw new Error('Login required'); + await EngageMessages.updateMessage(_id, doc); - return await EngageMessages.findOne({ _id }); + return EngageMessages.findOne({ _id }); }, /** @@ -46,10 +50,14 @@ export default { * @param {String} id * @return {Promise} null */ - async messagesRemove(root, _id) { - await EngageMessages.removeMessage(_id); + async messagesRemove(root, _id, { user }) { + if (!user) throw new Error('Login required'); + + const engageObj = await EngageMessages.findOne({ _id }); + + if (!engageObj) throw new Error(`Message not found with id ${_id}`); - return true; + return engageObj.remove(); }, /** @@ -57,10 +65,12 @@ export default { * @param {String} id * @return {Promise} message object */ - async messagesSetLive(root, _id) { + async messagesSetLive(root, _id, { user }) { + if (!user) throw new Error('Login required'); + await EngageMessages.updateMessage(_id, { isLive: true, isDraft: false }); - return await EngageMessages.findOne({ _id }); + return EngageMessages.findOne({ _id }); }, /** @@ -68,10 +78,12 @@ export default { * @param {String} id * @return {Promise} message object */ - async messagesSetPause(root, _id) { + async messagesSetPause(root, _id, { user }) { + if (!user) throw new Error('Login required'); + await EngageMessages.updateMessage(_id, { isLive: false }); - return await EngageMessages.findOne({ _id }); + return EngageMessages.findOne({ _id }); }, /** @@ -79,9 +91,11 @@ export default { * @param {String} id * @return {Promise} message object */ - async messagesSetLiveManual(root, _id) { + async messagesSetLiveManual(root, _id, { user }) { + if (!user) throw new Error('Login required'); + await EngageMessages.updateMessage(_id, { isLive: true, isDraft: false }); - return await EngageMessages.findOne({ _id }); + return EngageMessages.findOne({ _id }); }, }; diff --git a/src/data/resolvers/mutations/tags.js b/src/data/resolvers/mutations/tags.js index 81634b3c7..7454ec860 100644 --- a/src/data/resolvers/mutations/tags.js +++ b/src/data/resolvers/mutations/tags.js @@ -11,16 +11,19 @@ const validateUniqueness = async (selector, data) => { // can't update name & type same time more than one tags. const count = await Tags.find(selector).count(); + if (selector && count > 1) { return false; } const obj = selector && (await Tags.findOne(selector)); + if (obj) { filter._id = { $ne: obj._id }; } const existing = await Tags.findOne(filter); + if (existing) { return false; } @@ -28,12 +31,12 @@ const validateUniqueness = async (selector, data) => { return true; }; -async function tagObject({ tagIds, objectIds, collection, tagType }) { +const tagObject = async ({ tagIds, objectIds, collection, tagType }) => { if ((await Tags.find({ _id: { $in: tagIds }, type: tagType }).count()) !== tagIds.length) { throw new Error('Tag not found.'); } - const objects = await collection.find({ _id: { $in: objectIds } }, { fields: { tagIds: 1 } }); + const objects = await collection.find({ _id: { $in: objectIds } }, { tagIds: 1 }); let removeIds = []; @@ -48,87 +51,96 @@ async function tagObject({ tagIds, objectIds, collection, tagType }) { await collection.update({ _id: { $in: objectIds } }, { $set: { tagIds } }, { multi: true }); await Tags.update({ _id: { $in: tagIds } }, { $inc: { objectCount: 1 } }, { multi: true }); -} +}; export default { /** - * Create new tag - * @return {Promise} tag object - */ - async tagsAdd(root, { name, type, colorCode }, { user }) { - if (user) { - const isUnique = await validateUniqueness(null, { name, type, colorCode }); - if (!isUnique) { - throw new Error('Tag duplicated'); - } - - return await Tags.createTag({ name, type, colorCode }); - } + * Create new tag + * @param {String} doc.name + * @param {String} doc.type + * @param {String} doc.colorCode + * @return {Promise} tag object + */ + async tagsAdd(root, doc, { user }) { + if (!user) throw new Error('Login required'); + + if (!doc.name) throw new Error('Name is required field'); + + if (!doc.type) throw new Error('Type is required field'); + + const isUnique = await validateUniqueness(null, doc); + + if (!isUnique) throw new Error('Tag duplicated'); + + return Tags.createTag(doc); }, /** - * Update tag - * @return {Promise} tag object - */ - async tagsEdit(root, { _id, name, type, colorCode }, { user }) { - if (user) { - const isUnique = await validateUniqueness({ _id }, { name, type, colorCode }); - if (!isUnique) { - throw new Error('Tag duplicated'); - } - - await Tags.update({ _id: _id }, { name, type, colorCode }); - return Tags.findOne({ _id }); - } + * Update tag + * @param {String} doc.name + * @param {String} doc.type + * @param {String} doc.colorCode + * @return {Promise} tag object + */ + async tagsEdit(root, { _id, ...doc }, { user }) { + if (!user) throw new Error('Login required'); + + const isUnique = await validateUniqueness({ _id }, doc); + + if (!isUnique) throw new Error('Tag duplicated'); + + await Tags.update({ _id: _id }, doc); + return Tags.findOne({ _id }); }, /** - * Delete tag - * @return {Promise} - */ + * Delete tag + * @param {[String]} ids + * @return {Promise} + */ async tagsRemove(root, { ids }, { user }) { - if (user) { - const tagCount = await Tags.find({ _id: { $in: ids } }).count(); - if (tagCount !== ids.length) { - throw new Error('Tag not found'); - } + if (!user) throw new Error('Login required'); - let count = 0; + const tagCount = await Tags.find({ _id: { $in: ids } }).count(); - count += await Customers.find({ tagIds: { $in: ids } }).count(); - count += await Conversations.find({ tagIds: { $in: ids } }).count(); - count += await EngageMessages.find({ tagIds: { $in: ids } }).count(); + if (tagCount !== ids.length) throw new Error('Tag not found'); - if (count > 0) { - throw new Error("Can't remove a tag with tagged object(s)"); - } + let count = 0; - return Tags.remove({ _id: { $in: ids } }); - } + count += await Customers.find({ tagIds: { $in: ids } }).count(); + count += await Conversations.find({ tagIds: { $in: ids } }).count(); + count += await EngageMessages.find({ tagIds: { $in: ids } }).count(); + + if (count > 0) throw new Error("Can't remove a tag with tagged object(s)"); + + return Tags.remove({ _id: { $in: ids } }); }, /** - * Attach a tag - * @return {Promise} - */ + * Attach a tag + * @param {String} type + * @param {[String]} targetIds + * @param {[String]} tagIds + * @return {Promise} + */ async tagsTag(root, { type, targetIds, tagIds }, { user }) { - if (user) { - let collection = Conversations; - - if (type === 'customer') { - collection = Customers; - } - - if (type === 'engageMessage') { - collection = EngageMessages; - } - - await tagObject({ - tagIds, - objectIds: targetIds, - collection, - tagType: type, - }); + if (!user) throw new Error('Login required'); + + let collection = Conversations; + + if (type === 'customer') { + collection = Customers; } + + if (type === 'engageMessage') { + collection = EngageMessages; + } + + await tagObject({ + tagIds, + objectIds: targetIds, + collection, + tagType: type, + }); }, }; diff --git a/src/data/schema/engage.js b/src/data/schema/engage.js index 5c0a93da2..d6643a98a 100644 --- a/src/data/schema/engage.js +++ b/src/data/schema/engage.js @@ -35,7 +35,7 @@ export const mutations = ` segmentId: String!, method: String!, fromUserId: String!): EngageMessage messageEdit(_id: String!, title: String!, kind: String!, segmentId: String!, method: String!, fromUserId: String!): EngageMessage - messagesRemove(ids: [String!]!): Boolean + messagesRemove(ids: [String!]!): EngageMessage messagesSetLive(_id: String!): EngageMessage messagesSetPause(_id: String!): EngageMessage messagesSetLiveManual(_id: String!): EngageMessage diff --git a/src/data/schema/tag.js b/src/data/schema/tag.js index c8e0cf567..23a451247 100644 --- a/src/data/schema/tag.js +++ b/src/data/schema/tag.js @@ -1,11 +1,4 @@ export const types = ` - enum TagType { - all - customer - conversation - engageMessage - } - type Tag { _id: String! name: String @@ -21,8 +14,8 @@ export const queries = ` `; export const mutations = ` - tagsAdd(name: String!, type: TagType!, colorCode: String): Tag - tagsEdit(_id: String!, name: String!, type: TagType!, colorCode: String): Tag + tagsAdd(name: String!, type: String!, colorCode: String): Tag + tagsEdit(_id: String!, name: String!, type: String!, colorCode: String): Tag tagsRemove(ids: [String!]!): Tag - tagsTag(type: TagType!, targetIds: [String!]!, tagIds: [String!]!): Tag + tagsTag(type: String!, targetIds: [String!]!, tagIds: [String!]!): Tag `; diff --git a/src/db/factories.js b/src/db/factories.js index d8dce3463..cad1fb23b 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -14,7 +14,7 @@ export const userFactory = (params = {}) => { return user.save(); }; -export const tagsFactory = (params = {}) => { +export const tagsFactory = (params = { type: 'engageMessage' }) => { const tag = new Tags({ name: faker.random.word(), type: params.type || faker.random.word(), @@ -25,7 +25,7 @@ export const tagsFactory = (params = {}) => { return tag.save(); }; -export const segmentsFactory = (params = {}) => { +export const segmentsFactory = () => { const segment = new Segments({ name: faker.random.word(), }); diff --git a/src/db/models/Engages.js b/src/db/models/Engages.js index 00987626a..f7337f1ea 100644 --- a/src/db/models/Engages.js +++ b/src/db/models/Engages.js @@ -1,5 +1,41 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +import { MESSENGER_KINDS, SENT_AS_CHOICES } from '../../data/constants'; + +const EmailSchema = mongoose.Schema({ + templateId: String, + subject: String, + content: String, +}); + +const RuleSchema = mongoose.Schema({ + _id: String, + + // browserLanguage, currentUrl, etc ... + kind: String, + + // Browser language, Current url etc ... + text: String, + + // is, isNot, startsWith + condition: String, + + value: String, +}); + +const MessengerSchema = mongoose.Schema({ + brandId: String, + kind: { + type: String, + enum: MESSENGER_KINDS.ALL_LIST, + }, + sentAs: { + type: String, + enum: SENT_AS_CHOICES.ALL_LIST, + }, + content: String, + rules: [RuleSchema], +}); const EngageMessageSchema = mongoose.Schema({ _id: { type: String, unique: true, default: () => Random.id() }, @@ -16,8 +52,8 @@ const EngageMessageSchema = mongoose.Schema({ tagIds: [String], messengerReceivedCustomerIds: [String], - email: Object, - messenger: Object, + email: EmailSchema, + messenger: MessengerSchema, deliveryReports: Object, }); diff --git a/src/db/models/Tags.js b/src/db/models/Tags.js index 77a2c2b1a..0fc4b495a 100644 --- a/src/db/models/Tags.js +++ b/src/db/models/Tags.js @@ -1,5 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +import { TAG_TYPES } from '../../data/constants'; const TagSchema = mongoose.Schema({ _id: { @@ -8,7 +9,10 @@ const TagSchema = mongoose.Schema({ default: () => Random.id(), }, name: String, - type: String, + type: { + type: String, + enum: TAG_TYPES.ALL_LIST, + }, colorCode: String, createdAt: Date, objectCount: Number, @@ -20,9 +24,9 @@ class Tag { * @param {Object} tagObj object * @return {Promise} Newly created tag object */ - static createTag(tagObj) { + static createTag(doc) { return this.create({ - ...tagObj, + ...doc, createdAt: new Date(), }); } From 2e50ebb1be90dceb88709dcb1f73549e9b2bbf16 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 12 Oct 2017 21:47:44 +0800 Subject: [PATCH 046/318] #13 Refactor channels --- src/__tests__/channelMutations.test.js | 195 ++++++++++------------- src/data/resolvers/mutations/channels.js | 52 +++--- src/data/schema/channel.js | 6 +- src/db/models/Channels.js | 74 +++++---- src/db/models/Forms.js | 2 +- 5 files changed, 159 insertions(+), 170 deletions(-) diff --git a/src/__tests__/channelMutations.test.js b/src/__tests__/channelMutations.test.js index 897911e2e..b7526fc42 100644 --- a/src/__tests__/channelMutations.test.js +++ b/src/__tests__/channelMutations.test.js @@ -1,57 +1,52 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ - import { connect, disconnect } from '../db/connection'; import { userFactory, integrationFactory } from '../db/factories'; import { Channels, Users, Integrations } from '../db/models'; import mutations from '../data/resolvers/mutations'; +import { sendChannelNotifications } from '../data/utils'; beforeAll(() => connect()); afterAll(() => disconnect()); -describe('channel creation tests', () => { +describe('test channel creation error', () => { + test('check if Error is being thrown as intended', async () => { + try { + Channels.createChannel({ + name: 'Channel test', + }); + } catch (e) { + expect(e.message).toBe('userId must be supplied'); + } + }); +}); + +describe('test successful channel creation', () => { let _user; let _user2; let _integration; - /** - * Before each test create test data - * containing 2 users and an integration - */ beforeEach(async () => { _user = await userFactory({}); _integration = await integrationFactory({}); _user2 = await userFactory({}); }); - /** - * After each test remove the test data - */ afterEach(async () => { await Channels.remove({}); await Users.remove({}); await Integrations.remove({}); }); - test('create channel tests', async () => { - try { - Channels.createChannel({ - name: 'Channel test', - }); - } catch (e) { - expect(e.value).toBe('channel.create.exception'); - expect(e.message).toBe('userId must be supplied'); - } - + test('check if channel is getting created successfully', async () => { const doc = { name: 'Channel test', description: 'test channel descripion', - userId: _user._id, memberIds: [_user2._id], integrationIds: [_integration._id], }; - const channel = await Channels.createChannel(doc); + const channel = await Channels.createChannel(doc, _user._id); expect(channel.name).toEqual(doc.name); expect(channel.description).toEqual(doc.description); @@ -68,6 +63,8 @@ describe('channel update tests', () => { let _user; let _user2; let _integration; + let _channelDoc; + let _channel; /** * Before each test create test data @@ -77,6 +74,16 @@ describe('channel update tests', () => { _user = await userFactory({}); _integration = await integrationFactory({}); _user2 = await userFactory({}); + + _channelDoc = { + name: 'Channel test', + description: 'Channel test description', + userId: _user._id, + memberIds: [_user2._id], + integrationIds: [_integration._id], + }; + + _channel = await Channels.createChannel(_channelDoc, _user); }); /** @@ -88,40 +95,31 @@ describe('channel update tests', () => { await Integrations.remove({}); }); - test('update channel tests', async () => { - const doc = { - name: 'Channel test', - description: 'Channel test description', - userId: _user._id, - memberIds: [_user2._id], - integrationIds: [_integration._id], - }; - - let channel = await Channels.createChannel(doc); + test(`check if Channel update method and + Channel.preSave filter is working successfully`, async () => { + // test Channels.createChannel and Channels.preSave ============= + let channel = await Channels.findOne({ _id: _channel._id }); - doc.memberIds = [_user2._id]; - - await Channels.updateChannel(channel._id, doc); - - channel = await Channels.findOne({ _id: channel._id }); - - expect(channel.name).toEqual(doc.name); - expect(channel.description).toEqual(doc.description); + expect(channel.name).toEqual(_channelDoc.name); + expect(channel.description).toEqual(_channelDoc.description); expect(channel.memberIds.length).toBe(2); expect(channel.memberIds[0]).toBe(_user2._id); expect(channel.memberIds[1]).toBe(_user._id); expect(channel.integrationIds.length).toEqual(1); expect(channel.integrationIds[0]).toEqual(_integration._id); - expect(channel.userId).toEqual(doc.userId); + + expect(channel.userId).toEqual(_channelDoc.userId); expect(channel.conversationCount).toEqual(0); expect(channel.openConversationCount).toEqual(0); - doc.memberIds = [_user._id]; - await Channels.updateChannel(channel._id, doc); + // test Channels.updateChannel and Channels.preSave on update ========== + _channelDoc.memberIds = [_user._id]; + await Channels.updateChannel(channel._id, _channelDoc); channel = await Channels.findOne({ _id: channel._id }); expect(channel.memberIds.length).toBe(1); expect(channel.memberIds[0]).toBe(_user._id); + // testing whether the updated field is not overwriting whole document ======== await Channels.updateChannel(channel._id, { name: 'Channel test 2', }); @@ -133,115 +131,84 @@ describe('channel update tests', () => { describe('channel remove test', () => { let _channel; - /** - * Before each test create test data - * containing 2 users and an integration - */ beforeEach(async () => { const user = await userFactory({}); - _channel = await Channels.createChannel({ - name: 'Channel test', - userId: user._id, - }); + _channel = await Channels.createChannel( + { + name: 'Channel test', + }, + user._id, + ); }); - /** - * Remove test data - */ afterEach(async () => { await Channels.remove({}); }); - test('channel remove test', async () => { + test('checking if channel remove method is working successfully', async () => { await Channels.removeChannel(_channel._id); const channelCount = await Channels.find({}).count(); expect(channelCount).toBe(0); }); }); -describe('mutations', () => { +describe('test mutations', () => { let _user; - let _user2; - let _integration; - /** - * Before each test create test data - * containing 2 users and an integration - */ beforeEach(async () => { _user = await userFactory({}); - _integration = await integrationFactory({}); - _user2 = await userFactory({}); }); - /** - * After each test remove the test data - */ - afterEach(async () => { - await Channels.remove({}); - await Users.remove({}); - await Integrations.remove({}); - }); + test('testing if mutations.channelsCreate is working successfully', async () => { + expect.assertions(6); + // test mutations.channelsCreate ================== - test('mutations', async () => { - // mutations.chanelsCreate let doc = { name: 'Channel test', description: 'test channel descripion', - userId: _user._id, - memberIds: [_user2._id], - integrationIds: [_integration._id], + memberIds: ['fakeUserId2'], + integrationIds: ['fakeIntegrationId'], }; - let channel = await Channels.createChannel(doc); + Channels.createChannel = jest.fn(); - expect(channel.name).toEqual(doc.name); - expect(channel.description).toEqual(doc.description); - expect(channel.memberIds.length).toBe(2); - expect(channel.integrationIds.length).toEqual(1); - expect(channel.integrationIds[0]).toEqual(_integration._id); - expect(channel.userId).toEqual(doc.userId); - expect(channel.conversationCount).toEqual(0); - expect(channel.openConversationCount).toEqual(0); + try { + await mutations.channelsCreate(null, doc, { user: _user }); + } catch (e) { + /* this error is caused by Channels.createChannel mock function; + sendChannelNotifications method further in the workflow was using + the object returned by Channels.createChannel, since we mocked it, + returns null */ + if (e.message === `Cannot read property 'userId' of undefined`) { + expect(Channels.createChannel).toBeCalledWith(doc, _user); + expect(Channels.createChannel.mock.calls.length).toBe(1); + } + } + + // test mutations.channelsUpdate ========= + const channelId = 'fakeChannelId'; - // mutations.channelsUpdate doc = { name: 'Channel test 1', description: 'Channel test description 1', - userId: _user._id, - memberIds: [_user2._id], - integrationIds: [_integration._id], + userId: 'fakeUserId1', + memberIds: ['fakeUserId2'], + integrationIds: ['integrationIds1'], }; - doc.memberIds = [_user2._id]; - - await mutations.channelsEdit(null, { ...doc, _id: channel._id }); - - channel = await Channels.findOne({ _id: channel._id }); - - expect(channel.name).toEqual(doc.name); - expect(channel.description).toEqual(doc.description); - expect(channel.memberIds.length).toBe(2); - expect(channel.memberIds[0]).toBe(_user2._id); - expect(channel.memberIds[1]).toBe(_user._id); - expect(channel.integrationIds.length).toEqual(1); - expect(channel.integrationIds[0]).toEqual(_integration._id); - expect(channel.userId).toEqual(doc.userId); - expect(channel.conversationCount).toEqual(0); - expect(channel.openConversationCount).toEqual(0); - - doc.memberIds = [_user._id]; + Channels.updateChannel = jest.fn(); - await mutations.channelsEdit(null, { ...doc, _id: channel._id }); + await mutations.channelsEdit(null, { ...doc, _id: channelId }, { user: _user }); - channel = await Channels.findOne({ _id: channel._id }); + expect(Channels.updateChannel).toBeCalledWith(channelId, doc); + expect(Channels.updateChannel.mock.calls.length).toBe(1); - expect(channel.memberIds.length).toBe(1); - expect(channel.memberIds[0]).toBe(_user._id); + // test mutations.channelsRemove ============= + Channels.removeChannel = jest.fn(); - await mutations.channelsRemove(null, { _id: channel._id }); - const channelCount = await Channels.find({}).count(); + await mutations.channelsRemove(null, { _id: channelId }, { user: _user }); - expect(channelCount).toBe(0); + expect(Channels.removeChannel).toBeCalledWith(channelId); + expect(Channels.removeChannel.mock.calls.length).toEqual(1); }); }); diff --git a/src/data/resolvers/mutations/channels.js b/src/data/resolvers/mutations/channels.js index a27d5a0d0..dac334447 100644 --- a/src/data/resolvers/mutations/channels.js +++ b/src/data/resolvers/mutations/channels.js @@ -4,26 +4,26 @@ import { sendChannelNotifications } from '../../utils'; export default { /** * Create a new channel and send notifications to its members bar the creator - * @param {Object} - * @param {String} doc.name - * @param {String} doc.description - * @param {Array} doc.memberIds - * @param {Array} doc.integrationIds - * @param {String} doc.userId + * @param {Object} root + * @param {Object} doc - Channel object + * @param {string} doc.name - Channel name + * @param {string} doc.description - Channel description + * @param {String[]} doc.memberIds - Members assigned to the channel being created + * @param {String[]} doc.integrationIds - Integrations related to the channel + * @param {Object|string} user - User making this action * @return {Promise} returns channel object - * @throws {Error} throws apollo level validation errors - * @throws {Error} throws error if user is not logged in + * @throws {Error} throws Error('Login required') if user is not logged in */ async channelsCreate(root, doc, { user }) { if (!user) { throw new Error('Login required'); } - const channel = Channels.createChannel(doc); + const channel = Channels.createChannel(doc, user); sendChannelNotifications({ - userId: doc.userId, - memberIds: doc.memberIds, + userId: channel.userId, + memberIds: channel.memberIds, channelId: channel._id, }); @@ -32,16 +32,17 @@ export default { /** * Update channel data - * @param {Object} - * @param {String} doc._id - * @param {String} doc.name - * @param {String} doc.description - * @param {Array} doc.memberIds - * @param {Array} doc.integrationIds - * @param {String} doc.userId + * @param {Object} root + * @param {string} doc - Channel object + * @param {string} doc._id - Channel id + * @param {string} doc.name - Channel name + * @param {string} doc.description - Channel description + * @param {string[]} doc.memberIds - Members assigned to this channel + * @param {string[]} doc.integrationIds - Integration related to this channel + * @param {Object} object3 - Graphql input data + * @param {Object|string} object3.user - user making this action * @return {Promise} returns null - * @throws {Error} throws apollo level validation errors - * @throws {Error} throws error if user is not logged in + * @throws {Error} throws Error('Login required') if user is not logged in */ channelsEdit(root, { _id, ...doc }, { user }) { if (!user) { @@ -51,7 +52,7 @@ export default { sendChannelNotifications({ channelId: _id, memberIds: doc.memberIds, - userId: doc.userId, + userId: user, }); return Channels.updateChannel(_id, doc); @@ -59,10 +60,13 @@ export default { /** * Remove a channel - * @param {Object} - * @param {String} id + * @param {Object} root + * @param {string} object2 - Graphql input data + * @param {string} object2._id - Channel id + * @param {string} object3 - Middleware data + * @param {Object|String} object3.user - User making this action * @return {Promise} null - * @throws {Error} throws error if user is not logged in + * @throws {Error} throws Error('Login required') if user is not logged in */ channelsRemove(root, { _id }, { user }) { if (!user) { diff --git a/src/data/schema/channel.js b/src/data/schema/channel.js index 3f6810309..0315bacaf 100644 --- a/src/data/schema/channel.js +++ b/src/data/schema/channel.js @@ -22,16 +22,14 @@ export const mutations = ` name: String!, description: String, memberIds: [String], - integrationIds: [String], - userId: String!): Channel + integrationIds: [String]): Channel channelsEdit( _id: String!, name: String!, description: String, memberIds: [String], - integrationIds: [String], - userId: String!): Boolean + integrationIds: [String]): Boolean channelsRemove(_id: String!): Boolean `; diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index ff161a4a0..5d7aab1c8 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -2,68 +2,88 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; import { createdAtModifier } from '../plugins'; -// schema for channel document +// schema for Channels const ChannelSchema = mongoose.Schema({ _id: { type: String, default: () => Random.id(), }, name: String, - description: String, + description: { + type: String, + required: false, + }, integrationIds: [String], memberIds: [String], userId: String, - conversationCount: Number, - openConversationCount: Number, + conversationCount: { + type: Number, + default: 0, + }, + openConversationCount: { + type: Number, + default: 0, + }, }); class Channel { /** * Pre save filter method that adds userId to memberIds if it does not contain it + * @param {Object} doc - Channel object + * @return {Null} */ static preSave(doc) { + // on update method userId is not supplied + const userId = doc.userId || this.userId; + doc.memberIds = doc.memberIds || []; - if (!doc.memberIds.includes(doc.userId)) { - doc.memberIds.push(doc.userId); + if (!doc.memberIds.includes(userId)) { + doc.memberIds.push(userId); } } /** * Create a new channel document - * @param {String} doc.name - * @param {String} doc.description - * @param {Array} doc.integrationIds - * @param {Array} doc.memberIds - * @param {String} doc.userId - * @return {Promise} Newly created channel document + * @param {Object} doc - Channel object + * @param {string} doc.name - Channel name + * @param {string} doc.description - Channel description + * @param {string[]} doc.integrationIds - Integrations that this channel is related with + * @param {string[]} doc.memberIds - Members assigned to this integration + * @param {Object|String} userId - The user who is making this action + * @return {Promise} return channel document promise + * @throws {Error} throws Error('userId must be supplied') if userId is not supplied */ - static createChannel(doc) { - const { userId } = doc; - + static createChannel(doc, userId) { if (!userId) { throw new Error('userId must be supplied'); } - this.preSave(doc); + doc.userId = userId._id ? userId._id : userId; - doc.conversationCount = 0; - doc.openConversationCount = 0; + this.preSave(doc); return this.create(doc); } /** * Updates a channel document - * adds `userId` to the `memberIds` if it doesn't contain it - * @param {String} doc.name - * @param {String} doc.description - * @param {Array} doc.integrationIds - * @param {Array} doc.memberIds - * @param {String} doc.userId - * @return {Promise} + * @param {string} _id - Channel id + * @param {Object} doc - Channel object + * @param {string} doc.name - Channel name + * @param {string} doc.description - Channel description + * @param {string[]} doc.integrationIds - Integration ids related to the channel + * @param {string} doc.userId - The user id or object that craeted this channel + * @param {string[]} doc.memberIds - Member ids of the members assigned to this channel + * @return {Promise} returns null promise */ static updateChannel(_id, doc) { + const { userId } = doc; + + if (userId && userId._id) { + doc.userId = doc.userId._id; + } + this.preSave(doc); return this.update({ _id }, { $set: doc }, { runValidators: true }); @@ -71,8 +91,8 @@ class Channel { /** * Removes a channel document - * @param {String} _id - * @return {Promise} + * @param {string} _id - Channel id + * @return {Promise} returns null promise */ static removeChannel(_id) { return this.remove({ _id }); diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index 13a7567a4..58e24a93b 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -45,7 +45,7 @@ class Form { * @param {string} doc.title - Form title * @param {string} doc.description - Form description * @param {Date} doc.createdDate - Form creation date - * @param {Object|string} createdUser - the user who created this form, + * @param {Object|string} createdUser - The user who is creating this form, * can be both user id or user object * @return {Promise} returns Form document promise * @throws {Error} throws Error if createdUser is not supplied From e3e54701727485f345998f6e213c7d949d0a5e50 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 12 Oct 2017 21:55:13 +0800 Subject: [PATCH 047/318] Complete company logic --- src/data/resolvers/queries/companies.js | 79 +++++++++++++++++++ src/data/resolvers/queries/customers.js | 2 +- src/data/resolvers/queries/fields.js | 49 +++++++++--- src/data/resolvers/queries/index.js | 2 + ...QueryBuilder.js => segmentQueryBuilder.js} | 0 src/data/schema/company.js | 17 ++++ src/data/schema/field.js | 4 +- src/data/schema/index.js | 7 +- src/db/models/Companies.js | 18 ++++- src/db/models/Fields.js | 4 - 10 files changed, 159 insertions(+), 23 deletions(-) create mode 100644 src/data/resolvers/queries/companies.js rename src/data/resolvers/queries/{customerQueryBuilder.js => segmentQueryBuilder.js} (100%) diff --git a/src/data/resolvers/queries/companies.js b/src/data/resolvers/queries/companies.js new file mode 100644 index 000000000..9ccb3d181 --- /dev/null +++ b/src/data/resolvers/queries/companies.js @@ -0,0 +1,79 @@ +import { Companies, Segments } from '../../../db/models'; +import QueryBuilder from './segmentQueryBuilder.js'; + +const listQuery = async params => { + const selector = {}; + + // Filter by segments + if (params.segment) { + const segment = await Segments.findOne({ _id: params.segment }); + const query = QueryBuilder.segments(segment); + Object.assign(selector, query); + } + + return selector; +}; + +export default { + /** + * Companies list + * @param {Object} args + * @param {CompanyListParams} args.params + * @return {Promise} filtered companies list by given parameters + */ + async companies(root, { params }) { + if (params.ids) { + return Companies.find({ _id: { $in: params.ids } }); + } + + const selector = await listQuery(params); + + return Companies.find(selector).limit(params.limit || 0); + }, + + /** + * Group company counts by segments + * @param {Object} args + * @param {CompanyListParams} args.params + * @return {Object} counts map + */ + async companyCounts(root, { params }) { + const counts = { bySegment: {}, byBrand: {}, byIntegrationType: {}, byTag: {} }; + const selector = await listQuery(params); + + const count = query => { + const findQuery = Object.assign({}, selector, query); + return Companies.find(findQuery).count(); + }; + + // Count current filtered companies + counts.all = await count(selector); + + // Count companies by segments + const segments = await Segments.find(); + + for (let s of segments) { + counts.bySegment[s._id] = await count(QueryBuilder.segments(s)); + } + + return counts; + }, + + /** + * Get one company + * @param {Object} args + * @param {String} args._id + * @return {Promise} found company + */ + companyDetail(root, { _id }) { + return Companies.findOne({ _id }); + }, + + /** + * Get all companies count. We will use it in pager + * @return {Promise} total count + */ + companiesTotalCount() { + return Companies.find({}).count(); + }, +}; diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index 4caef345f..6f872dc85 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import { Brands, Tags, Integrations, Customers, Segments } from '../../../db/models'; import { TAG_TYPES, INTEGRATION_KIND_CHOICES } from '../../constants'; -import QueryBuilder from './customerQueryBuilder.js'; +import QueryBuilder from './segmentQueryBuilder.js'; const listQuery = async params => { const selector = {}; diff --git a/src/data/resolvers/queries/fields.js b/src/data/resolvers/queries/fields.js index 484149f5f..8963d7bb4 100644 --- a/src/data/resolvers/queries/fields.js +++ b/src/data/resolvers/queries/fields.js @@ -1,4 +1,5 @@ -import { Customers, Fields } from '../../../db/models'; +import { Companies, Customers, Fields } from '../../../db/models'; +import { FIELD_CONTENT_TYPES } from '../../../constants'; export default { /** @@ -26,7 +27,7 @@ export default { * @return {[JSON]} * [{ name: 'messengerData.isActive', text: 'Messenger: is Active' }] */ - async fieldsCombinedByContentType() { + async fieldsCombinedByContentType(root, { contentType }) { /* * Generates fields using given schema * @param {Schema} schema Customers.schema etc ... @@ -55,11 +56,17 @@ export default { return fields; }; - // generate list using customer schema - let fields = generateFieldsFromSchema(Customers.schema, ''); + let schema = Companies.schema; - Customers.schema.eachPath(name => { - const path = Customers.schema.paths[name]; + if (contentType === FIELD_CONTENT_TYPES.CUSTOMER) { + schema = Customers.schema; + } + + // generate list using customer or company schema + let fields = generateFieldsFromSchema(schema, ''); + + schema.eachPath(name => { + const path = schema.paths[name]; // extend fields list using sub schema fields if (path.schema) { @@ -67,7 +74,7 @@ export default { } }); - const customFields = await Fields.getCustomerFields(); + const customFields = await Fields.find({ contentType }); // extend fields list using custom fields customFields.forEach(customField => { @@ -84,11 +91,27 @@ export default { /** * Default list columns config */ - fieldsDefaultColumnsConfig() { - return [ - { name: 'name', label: 'Name', order: 1 }, - { name: 'email', label: 'Email', order: 2 }, - { name: 'phone', label: 'Phone', order: 3 }, - ]; + fieldsDefaultColumnsConfig(root, { contentType }) { + if (contentType === FIELD_CONTENT_TYPES.CUSTOMER) { + return [ + { name: 'name', label: 'Name', order: 1 }, + { name: 'email', label: 'Email', order: 2 }, + { name: 'phone', label: 'Phone', order: 3 }, + ]; + } + + if (contentType === FIELD_CONTENT_TYPES.COMPANY) { + return [ + { name: 'name', label: 'Name', order: 1 }, + { name: 'size', label: 'Size', order: 2 }, + { name: 'website', label: 'Website', order: 3 }, + { name: 'industry', label: 'Industry', order: 4 }, + { name: 'plan', label: 'Plan', order: 5 }, + { name: 'lastSeenAt', label: 'Last seen at', order: 6 }, + { name: 'sessionCount', label: 'Session count', order: 7 }, + ]; + } + + return []; }, }; diff --git a/src/data/resolvers/queries/index.js b/src/data/resolvers/queries/index.js index afe2c223e..efb67a84a 100644 --- a/src/data/resolvers/queries/index.js +++ b/src/data/resolvers/queries/index.js @@ -10,6 +10,7 @@ import engages from './engages'; import tags from './tags'; import internalNotes from './internalNotes'; import customers from './customers'; +import companies from './companies'; import segments from './segments'; import conversations from './conversations'; import insights from './insights'; @@ -28,6 +29,7 @@ export default { ...tags, ...internalNotes, ...customers, + ...companies, ...segments, ...conversations, ...insights, diff --git a/src/data/resolvers/queries/customerQueryBuilder.js b/src/data/resolvers/queries/segmentQueryBuilder.js similarity index 100% rename from src/data/resolvers/queries/customerQueryBuilder.js rename to src/data/resolvers/queries/segmentQueryBuilder.js diff --git a/src/data/schema/company.js b/src/data/schema/company.js index 905c502f8..c8ed5e179 100644 --- a/src/data/schema/company.js +++ b/src/data/schema/company.js @@ -1,4 +1,11 @@ export const types = ` + input CompanyListParams { + limit: Int, + page: String, + segment: String, + ids: [String] + } + type Company { _id: String! name: String @@ -9,9 +16,18 @@ export const types = ` lastSeenAt: Date sessionCount: Int tagIds: [String], + + customFieldsData: JSON } `; +export const queries = ` + companies(params: CompanyListParams): [Company] + companyCounts(params: CompanyListParams): JSON + companyDetail(_id: String!): Company + companiesTotalCount: Int +`; + const commonFields = ` name: String!, size: Int, @@ -21,6 +37,7 @@ const commonFields = ` lastSeenAt: Date, sessionCount: Int, tagIds: [String] + customFieldsData: JSON `; export const mutations = ` diff --git a/src/data/schema/field.js b/src/data/schema/field.js index e3e91283f..23786637e 100644 --- a/src/data/schema/field.js +++ b/src/data/schema/field.js @@ -26,8 +26,8 @@ export const types = ` export const queries = ` fields(contentType: String!, contentTypeId: String): [Field] - fieldsCombinedByContentType: JSON - fieldsDefaultColumnsConfig: [ColumnConfigItem] + fieldsCombinedByContentType(contentType: String!): JSON + fieldsDefaultColumnsConfig(contentType: String!): [ColumnConfigItem] `; const commonFields = ` diff --git a/src/data/schema/index.js b/src/data/schema/index.js index 74feb999d..e6e89f529 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -1,6 +1,10 @@ import { types as UserTypes, queries as UserQueries } from './user'; -import { types as CompanyTypes, mutations as CompanyMutations } from './company'; +import { + types as CompanyTypes, + queries as CompanyQueries, + mutations as CompanyMutations, +} from './company'; import { types as ChannelTypes, queries as ChannelQueries } from './channel'; @@ -92,6 +96,7 @@ export const queries = ` ${EngageQueries} ${TagQueries} ${InternalNoteQueries} + ${CompanyQueries} ${CustomerQueries} ${SegmentQueries} ${ConversationQueries} diff --git a/src/db/models/Companies.js b/src/db/models/Companies.js index 3d4913073..ba3e79be2 100644 --- a/src/db/models/Companies.js +++ b/src/db/models/Companies.js @@ -10,36 +10,50 @@ const CompanySchema = mongoose.Schema({ name: { type: String, + label: 'Name', optional: true, }, size: { type: Number, + label: 'Size', optional: true, }, industry: { type: String, + label: 'Industry', optional: true, }, website: { type: String, + label: 'Website', optional: true, }, plan: { type: String, + label: 'Plan', optional: true, }, - lastSeenAt: Date, - sessionCount: Number, + lastSeenAt: { + type: Date, + label: 'Last seen at', + }, + + sessionCount: { + type: Number, + label: 'Session count', + }, tagIds: { type: [String], optional: true, }, + + customFieldsData: Object, }); class Company { diff --git a/src/db/models/Fields.js b/src/db/models/Fields.js index 09b50b8d2..dc6adaeac 100644 --- a/src/db/models/Fields.js +++ b/src/db/models/Fields.js @@ -124,10 +124,6 @@ class Field { return this.find({ _id: { $in: ids } }).sort({ order: 1 }); } - - static getCustomerFields() { - return this.find({ contentType: FIELD_CONTENT_TYPES.CUSTOMER }); - } } FieldSchema.loadClass(Field); From 3442636f51a02d11554e19fd832c7f9b6a04389f Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 13 Oct 2017 00:11:03 +0800 Subject: [PATCH 048/318] #13 fRefactor form mutations and tests --- src/__tests__/integrationMutations.test.js | 413 ++++++++++--------- src/data/constants.js | 11 +- src/data/resolvers/mutations/forms.js | 16 +- src/data/resolvers/mutations/integrations.js | 129 +++--- src/data/schema/form.js | 2 +- src/data/schema/integration.js | 8 +- src/data/schema/notification.js | 4 +- src/db/models/Integrations.js | 233 ++++++----- 8 files changed, 436 insertions(+), 380 deletions(-) diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js index 888e2edf2..abf5072be 100644 --- a/src/__tests__/integrationMutations.test.js +++ b/src/__tests__/integrationMutations.test.js @@ -2,6 +2,7 @@ /* eslint-disable no-underscore-dangle */ import faker from 'faker'; import { connect, disconnect } from '../db/connection'; +import { KIND_CHOICES, FORM_LOAD_TYPES, MESSENGER_DATA_AVAILABILITY } from '../data/constants'; import { brandFactory, integrationFactory, formFactory, userFactory } from '../db/factories'; import { Integrations, Brands, Users, Forms } from '../db/models'; import mutations from '../data/resolvers/mutations'; @@ -9,81 +10,79 @@ import mutations from '../data/resolvers/mutations'; beforeAll(() => connect()); afterAll(() => disconnect()); -describe('messenger integration add test', () => { +describe('messenger integration model add method test', () => { let _brand; - /** - */ + beforeEach(async () => { _brand = await brandFactory({}); }); - /** - */ afterEach(async () => { await Brands.remove({}); await Integrations.remove({}); }); - test('messenger integration add test', async () => { - const integration = await Integrations.createMessengerIntegration({ + test('check if messenger integration create method is running successfully', async () => { + const doc = { name: 'Integration test', brandId: _brand._id, - }); + }; - expect(integration.name).toBe('Integration test'); - expect(integration.brandId).toBe(_brand._id); - expect(integration.kind).toBe('messenger'); + const integration = await Integrations.createMessengerIntegration(doc); + + expect(integration.name).toBe(doc.name); + expect(integration.brandId).toBe(doc.brandId); + expect(integration.kind).toBe(KIND_CHOICES.MESSENGER); }); }); -describe('messenger integration edit test', () => { +describe('messenger integration model edit test', () => { let _brand; let _integration; - /** - */ + let _brand2; + beforeEach(async () => { _brand = await brandFactory({}); + _brand2 = await brandFactory({}); _integration = await integrationFactory({ - kind: 'messenger', + kind: KIND_CHOICES.MESSENGER, + brandId: _brand, }); }); - /** - */ afterEach(async () => { await Brands.remove({}); await Integrations.remove({}); }); - test('messenger integration edit test', async () => { - await Integrations.updateMessengerIntegration(_integration._id, { + test('check if messenger integration update method is running successfully', async () => { + const doc = { name: 'Integration test 2', - brandId: _brand._id, + brandId: _brand2._id, kind: 'new kind', - }); + }; + + await Integrations.updateMessengerIntegration(_integration._id, doc); const updatedIntegration = await Integrations.findOne({ _id: _integration._id }); - expect(updatedIntegration.name).toBe('Integration test 2'); - expect(updatedIntegration.brandId).toBe(_brand._id); - expect(updatedIntegration.kind).toBe('messenger'); + expect(updatedIntegration.name).toBe(doc.name); + expect(updatedIntegration.brandId).toBe(doc.brandId); + expect(updatedIntegration.kind).toBe(KIND_CHOICES.MESSENGER); }); }); -describe('create form integration test without formData', () => { +describe('form integration create model test without formData', () => { let _brand; let _form; let _user; - /** - */ + beforeEach(async () => { _brand = await brandFactory({}); _user = await userFactory({}); _form = await formFactory({ createdUserId: _user._id }); }); - /** - */ afterEach(async () => { await Brands.remove({}); await Integrations.remove({}); @@ -91,8 +90,9 @@ describe('create form integration test without formData', () => { await Forms.remove({}); }); - test('create form integration test wihtout formData', async () => { + test('check if create form integration test wihtout formData is throwing exception', async () => { expect.assertions(1); + const mainDoc = { name: 'form integration test', brandId: _brand._id, @@ -102,7 +102,7 @@ describe('create form integration test without formData', () => { try { await Integrations.createFormIntegration(mainDoc); } catch (e) { - expect(e).toEqual('formData must be supplied'); + expect(e.message).toEqual('formData must be supplied'); } }); }); @@ -111,16 +111,13 @@ describe('create form integration test', () => { let _brand; let _form; let _user; - /** - */ + beforeEach(async () => { _brand = await brandFactory({}); _user = await userFactory({}); _form = await formFactory({ createdUserId: _user._id }); }); - /** - */ afterEach(async () => { await Brands.remove({}); await Integrations.remove({}); @@ -128,7 +125,7 @@ describe('create form integration test', () => { await Forms.remove({}); }); - test('create form integration test without formData', async () => { + test('test if create form integration is working successfully', async () => { const mainDoc = { name: 'form integration test', brandId: _brand._id, @@ -136,16 +133,16 @@ describe('create form integration test', () => { }; const formData = { - loadType: 'embedded', + loadType: FORM_LOAD_TYPES.EMBEDDED, }; const integration = await Integrations.createFormIntegration({ ...mainDoc, formData }); expect(integration.formId).toEqual(_form._id); - expect(integration.name).toEqual('form integration test'); + expect(integration.name).toEqual(mainDoc.name); expect(integration.brandId).toEqual(_brand._id); - expect(integration.formData.loadType).toEqual('embedded'); - expect(integration.kind).toEqual('form'); + expect(integration.formData.loadType).toEqual(FORM_LOAD_TYPES.EMBEDDED); + expect(integration.kind).toEqual(KIND_CHOICES.FORM); }); }); @@ -156,8 +153,7 @@ describe('edit form integration test', () => { let _form2; let _user; let _form_integration; - /** - */ + beforeEach(async () => { _brand = await brandFactory({}); _brand2 = await brandFactory({}); @@ -168,15 +164,13 @@ describe('edit form integration test', () => { name: 'form integration test', brandId: _brand._id, formId: _form._id, - kind: 'form', + kind: KIND_CHOICES.FORM, formData: { - loadType: 'embedded', + loadType: FORM_LOAD_TYPES.EMBEDDED, }, }); }); - /** - */ afterEach(async () => { await Brands.remove({}); await Integrations.remove({}); @@ -184,7 +178,7 @@ describe('edit form integration test', () => { await Forms.remove({}); }); - test('edit form integration test', async () => { + test('test if integration form update method is running successfully', async () => { const mainDoc = { name: 'form integration test 2', brandId: _brand2._id, @@ -192,7 +186,7 @@ describe('edit form integration test', () => { }; const formData = { - loadType: 'shoutbox', + loadType: FORM_LOAD_TYPES.SHOUTBOX, }; await Integrations.updateFormIntegration(_form_integration._id, { @@ -202,18 +196,17 @@ describe('edit form integration test', () => { const integration = await Integrations.findOne({ _id: _form_integration._id }); - expect(integration.name).toEqual('form integration test 2'); + expect(integration.name).toEqual(mainDoc.name); expect(integration.formId).toEqual(_form2._id); expect(integration.brandId).toEqual(_brand2._id); - expect(integration.formData.loadType).toEqual('shoutbox'); + expect(integration.formData.loadType).toEqual(FORM_LOAD_TYPES.SHOUTBOX); }); }); -describe('remove integration test', () => { +describe('remove integration model method test', () => { let _brand; let _integration; - /** - */ + beforeEach(async () => { _brand = await brandFactory({}); _integration = await integrationFactory({ @@ -223,16 +216,17 @@ describe('remove integration test', () => { }); }); - /** - */ afterEach(async () => { await Brands.remove({}); await Integrations.remove({}); + await Users.remove({}); }); - test('remove form integration test', async () => { - await mutations.integrationsRemove(null, { id: _integration._id }); + test('test if remove form integration model method is working successfully', async () => { + await Integrations.removeIntegration({ _id: _integration._id }); + const integrationCount = await Integrations.find({}).count(); + expect(integrationCount).toEqual(0); }); }); @@ -258,7 +252,7 @@ describe('save integration messenger appearance test', () => { await Integrations.remove({}); }); - test('save integration messenger appearance test', async () => { + test('test if save integration messenger appearance method is working successfully', async () => { const uiOptions = { color: faker.random.word(), wallpaper: faker.random.word(), @@ -279,9 +273,6 @@ describe('save integration messenger configurations test', () => { let _brand; let _integration; - /** - * Create integration object to be used with messenger data configurations - */ beforeEach(async () => { _brand = await brandFactory({}); _integration = await integrationFactory({ @@ -291,18 +282,15 @@ describe('save integration messenger configurations test', () => { }); }); - /** - * Delete test data - */ afterEach(async () => { await Brands.remove({}); await Integrations.remove({}); }); - test('save integration messenger configurations test', async () => { + test('test if integration messenger save confiturations method is working correctly', async () => { const messengerData = { notifyCustomer: true, - availabilityMethod: 'manual', + availabilityMethod: MESSENGER_DATA_AVAILABILITY.MANUAL, isOnline: false, onlineHours: [ { @@ -326,23 +314,27 @@ describe('save integration messenger configurations test', () => { const integration = await Integrations.findOne({ _id: _integration._id }); - expect(integration.messengerData.notifyCustomer).toEqual(true); - expect(integration.messengerData.availabilityMethod).toEqual('manual'); - expect(integration.messengerData.isOnline).toEqual(false); - expect(integration.messengerData.onlineHours[0].day).toEqual('Monday'); - expect(integration.messengerData.onlineHours[0].from).toEqual('8am'); - expect(integration.messengerData.onlineHours[0].to).toEqual('12pm'); - expect(integration.messengerData.onlineHours[1].day).toEqual('Monday'); - expect(integration.messengerData.onlineHours[1].from).toEqual('2pm'); - expect(integration.messengerData.onlineHours[1].to).toEqual('6pm'); - expect(integration.messengerData.timezone).toEqual('CET'); - expect(integration.messengerData.welcomeMessage).toEqual('Welcome user'); - expect(integration.messengerData.awayMessage).toEqual('Bye bye'); - expect(integration.messengerData.thankYouMessage).toEqual('Thank you'); + expect(integration.messengerData.notifyCustomer).toEqual(messengerData.notifyCustomer); + expect(integration.messengerData.availabilityMethod).toEqual(messengerData.availabilityMethod); + expect(integration.messengerData.isOnline).toEqual(messengerData.isOnline); + expect(integration.messengerData.onlineHours[0].day).toEqual(messengerData.onlineHours[0].day); + expect(integration.messengerData.onlineHours[0].from).toEqual( + messengerData.onlineHours[0].from, + ); + expect(integration.messengerData.onlineHours[0].to).toEqual(messengerData.onlineHours[0].to); + expect(integration.messengerData.onlineHours[1].day).toEqual(messengerData.onlineHours[1].day); + expect(integration.messengerData.onlineHours[1].from).toEqual( + messengerData.onlineHours[1].from, + ); + expect(integration.messengerData.onlineHours[1].to).toEqual(messengerData.onlineHours[1].to); + expect(integration.messengerData.timezone).toEqual(messengerData.timezone); + expect(integration.messengerData.welcomeMessage).toEqual(messengerData.welcomeMessage); + expect(integration.messengerData.awayMessage).toEqual(messengerData.awayMessage); + expect(integration.messengerData.thankYouMessage).toEqual(messengerData.thankYouMessage); const newMessengerData = { notifyCustomer: false, - availabilityMethod: 'auto', + availabilityMethod: MESSENGER_DATA_AVAILABILITY.AUTO, isOnline: true, onlineHours: [ { @@ -366,169 +358,200 @@ describe('save integration messenger configurations test', () => { const updatedIntegration = await Integrations.findOne({ _id: _integration._id }); - expect(updatedIntegration.messengerData.notifyCustomer).toEqual(false); - expect(updatedIntegration.messengerData.availabilityMethod).toEqual('auto'); - expect(updatedIntegration.messengerData.isOnline).toEqual(true); - expect(updatedIntegration.messengerData.onlineHours[0].day).toEqual('Tuesday'); - expect(updatedIntegration.messengerData.onlineHours[0].from).toEqual('9am'); - expect(updatedIntegration.messengerData.onlineHours[0].to).toEqual('1pm'); - expect(updatedIntegration.messengerData.onlineHours[1].day).toEqual('Tuesday'); - expect(updatedIntegration.messengerData.onlineHours[1].from).toEqual('3pm'); - expect(updatedIntegration.messengerData.onlineHours[1].to).toEqual('7pm'); - expect(updatedIntegration.messengerData.timezone).toEqual('EET'); - expect(updatedIntegration.messengerData.welcomeMessage).toEqual('Welcome customer'); - expect(updatedIntegration.messengerData.awayMessage).toEqual('Good bye'); - expect(updatedIntegration.messengerData.thankYouMessage).toEqual('Gracias'); + expect(updatedIntegration.messengerData.notifyCustomer).toEqual( + newMessengerData.notifyCustomer, + ); + expect(updatedIntegration.messengerData.availabilityMethod).toEqual( + newMessengerData.availabilityMethod, + ); + expect(updatedIntegration.messengerData.isOnline).toEqual(newMessengerData.isOnline); + expect(updatedIntegration.messengerData.onlineHours[0].day).toEqual( + newMessengerData.onlineHours[0].day, + ); + expect(updatedIntegration.messengerData.onlineHours[0].from).toEqual( + newMessengerData.onlineHours[0].from, + ); + expect(updatedIntegration.messengerData.onlineHours[0].to).toEqual( + newMessengerData.onlineHours[0].to, + ); + expect(updatedIntegration.messengerData.onlineHours[1].day).toEqual( + newMessengerData.onlineHours[1].day, + ); + expect(updatedIntegration.messengerData.onlineHours[1].from).toEqual( + newMessengerData.onlineHours[1].from, + ); + expect(updatedIntegration.messengerData.onlineHours[1].to).toEqual( + newMessengerData.onlineHours[1].to, + ); + expect(updatedIntegration.messengerData.timezone).toEqual(newMessengerData.timezone); + expect(updatedIntegration.messengerData.welcomeMessage).toEqual( + newMessengerData.welcomeMessage, + ); + expect(updatedIntegration.messengerData.awayMessage).toEqual(newMessengerData.awayMessage); + expect(updatedIntegration.messengerData.thankYouMessage).toEqual( + newMessengerData.thankYouMessage, + ); }); }); -describe('mutation test', () => { - let _brand; - let _brand2; +describe('mutation tests', () => { let _user; - let _form; - let _form2; beforeEach(async () => { - _brand = await brandFactory({}); - _brand2 = await brandFactory({}); _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - _form2 = await formFactory({ createdUserId: _user._id }); - }), - afterEach(async () => { - await Brands.remove({}); - await Integrations.remove({}); - await Users.remove({}); - await Forms.remove({}); - }); + }); + + afterEach(async () => { + await Users.remove({}); + }); test('mutation test', async () => { - let integration = await mutations.integrationsCreateMessengerIntegration(null, { + // test Integrations.createMessengerIntegration ========== + const fakeBrandId = 'fakeBrandId'; + const fakeIntegrationId = 'fakeIntegrationid'; + const fakeFormId = 'fakeFormId'; + + let doc = { name: 'Integration test', - brandId: _brand._id, - }); + brandId: fakeBrandId, + }; + + Integrations.createMessengerIntegration = jest.fn(); - expect(integration.name).toBe('Integration test'); - expect(integration.brandId).toBe(_brand._id); - expect(integration.kind).toBe('messenger'); + await mutations.integrationsCreateMessengerIntegration(null, doc, { user: _user }); - await mutations.integrationsEditMessengerIntegration(null, { - id: integration._id, + expect(Integrations.createMessengerIntegration).toBeCalledWith(doc); + expect(Integrations.createMessengerIntegration.mock.calls.length).toBe(1); + + // test Integrations.updateMessengerIntegration ========================= + doc = { + _id: fakeIntegrationId, name: 'Integration test 2', - brandId: _brand2._id, - }); + brandId: fakeBrandId, + }; - integration = await Integrations.findOne({ _id: integration._id }); + Integrations.updateMessengerIntegration = jest.fn(); - expect(integration.name).toEqual('Integration test 2'); - expect(integration.brandId).toEqual(_brand2._id); + await mutations.integrationsEditMessengerIntegration(null, doc, { user: _user }); + + delete doc._id; + + expect(Integrations.updateMessengerIntegration).toBeCalledWith(fakeIntegrationId, doc); + expect(Integrations.updateMessengerIntegration.mock.calls.length).toBe(1); + // test Integrations.saveMessengerConfigs ======================= const uiOptions = { color: faker.random.word(), wallpaper: faker.random.word(), logo: faker.random.word(), }; - await mutations.integrationsSaveMessengerAppearanceData(null, { - id: integration._id, - uiOptions, - }); + Integrations.saveMessengerAppearanceData = jest.fn(); - integration = await Integrations.findOne({ _id: integration._id }); + await mutations.integrationsSaveMessengerAppearanceData( + null, + { + _id: fakeIntegrationId, + uiOptions, + }, + { user: _user }, + ); - expect(integration.uiOptions.color).toEqual(uiOptions.color); - expect(integration.uiOptions.wallpaper).toEqual(uiOptions.wallpaper); - expect(integration.uiOptions.logo).toEqual(uiOptions.logo); + expect(Integrations.saveMessengerAppearanceData).toBeCalledWith(fakeIntegrationId, uiOptions); + expect(Integrations.saveMessengerAppearanceData.mock.calls.length).toBe(1); + + // test Integrations.saveMessengerConfigs =================== + const messengerData = { + notifyCustomer: true, + availabilityMethod: MESSENGER_DATA_AVAILABILITY.AUTO, + isOnline: false, + onlineHours: [ + { + day: 'Monday', + from: '8am', + to: '12pm', + }, + { + day: 'Monday', + from: '2pm', + to: '6pm', + }, + ], + timezone: 'CET', + welcomeMessage: 'Welcome user', + awayMessage: 'Bye bye', + thankYouMessage: 'Thank you', + }; + + Integrations.saveMessengerConfigs = jest.fn(); + await mutations.integrationsSaveMessengerConfigs( + null, + { + _id: fakeIntegrationId, + messengerData, + }, + { user: _user }, + ); + + expect(Integrations.saveMessengerConfigs).toBeCalledWith(fakeIntegrationId, messengerData); + expect(Integrations.saveMessengerConfigs.mock.calls.length).toBe(1); + + // test Integrations.createFormIntegration ======================= let mainDoc = { name: 'form integration test', - brandId: _brand._id, - formId: _form._id, + brandId: fakeBrandId, + formId: fakeFormId, }; let formData = { - loadType: 'embedded', + loadType: FORM_LOAD_TYPES.EMBEDDED, }; - let integration2 = await mutations.integrationsCreateFormIntegration(null, { + Integrations.createFormIntegration = jest.fn(); + + doc = { ...mainDoc, formData, - }); - expect(integration2.formId).toEqual(_form._id); - expect(integration2.name).toEqual('form integration test'); - expect(integration2.brandId).toEqual(_brand._id); - expect(integration2.formData.loadType).toEqual('embedded'); + }; + + await mutations.integrationsCreateFormIntegration(null, doc, { user: _user }); + expect(Integrations.createFormIntegration).toBeCalledWith(doc); + expect(Integrations.createFormIntegration.mock.calls.length).toBe(1); + + // test Integrations.updateFormIntegration ===================== mainDoc = { name: 'form integration test 2', - brandId: _brand2._id, - formId: _form2._id, + brandId: fakeBrandId, + formId: fakeFormId, }; formData = { - loadType: 'shoutbox', + loadType: FORM_LOAD_TYPES.SHOUTBOX, }; - await mutations.integrationsEditFormIntegration(null, { - id: integration2._id, + doc = { + _id: fakeIntegrationId, ...mainDoc, formData, - }); + }; - const updatedIntegration = await Integrations.findOne({ _id: integration2._id }); + Integrations.updateFormIntegration = jest.fn(); - expect(updatedIntegration.name).toEqual('form integration test 2'); - expect(updatedIntegration.formId).toEqual(_form2._id); - expect(updatedIntegration.brandId).toEqual(_brand2._id); - expect(updatedIntegration.formData.loadType).toEqual('shoutbox'); + await mutations.integrationsEditFormIntegration(null, doc, { user: _user }); - const messengerData = { - notifyCustomer: true, - availabilityMethod: 'manual', - isOnline: false, - onlineHours: [ - { - day: 'Monday', - from: '8am', - to: '12pm', - }, - { - day: 'Monday', - from: '2pm', - to: '6pm', - }, - ], - timezone: 'CET', - welcomeMessage: 'Welcome user', - awayMessage: 'Bye bye', - thankYouMessage: 'Thank you', - }; + delete doc._id; - await mutations.integrationsSaveMessengerConfigs(null, { id: integration._id, messengerData }); - - integration = await Integrations.findOne({ _id: integration._id }); - - expect(integration.messengerData.notifyCustomer).toEqual(true); - expect(integration.messengerData.availabilityMethod).toEqual('manual'); - expect(integration.messengerData.isOnline).toEqual(false); - expect(integration.messengerData.onlineHours[0].day).toEqual('Monday'); - expect(integration.messengerData.onlineHours[0].from).toEqual('8am'); - expect(integration.messengerData.onlineHours[0].to).toEqual('12pm'); - expect(integration.messengerData.onlineHours[1].day).toEqual('Monday'); - expect(integration.messengerData.onlineHours[1].from).toEqual('2pm'); - expect(integration.messengerData.onlineHours[1].to).toEqual('6pm'); - expect(integration.messengerData.timezone).toEqual('CET'); - expect(integration.messengerData.welcomeMessage).toEqual('Welcome user'); - expect(integration.messengerData.awayMessage).toEqual('Bye bye'); - expect(integration.messengerData.thankYouMessage).toEqual('Thank you'); - - const integrations = await Integrations.find({}, { _id: 1 }); - for (let i of integrations) { - await mutations.integrationsRemove(null, { id: i._id }); - } + expect(Integrations.updateFormIntegration).toBeCalledWith(fakeIntegrationId, doc); + expect(Integrations.updateFormIntegration.mock.calls.length).toBe(1); - const integrationCount = await Integrations.find({}).count(); - expect(integrationCount).toEqual(0); + // test Integrations.removeIntegration =========================== + Integrations.removeIntegration = jest.fn(); + + await mutations.integrationsRemove(null, { _id: fakeIntegrationId }, { user: _user }); + + expect(Integrations.removeIntegration).toBeCalledWith(fakeIntegrationId); + expect(Integrations.removeIntegration.mock.calls.length).toBe(1); }); }); diff --git a/src/data/constants.js b/src/data/constants.js index df6ea6c70..2bc5d545a 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -24,7 +24,7 @@ export const FORM_LOAD_TYPES = { SHOUTBOX: 'shoutbox', POPUP: 'popup', EMBEDDED: 'embedded', - ALL_LIST: ['', 'shoutbox', 'popup', 'embedded'], + ALL: ['', 'shoutbox', 'popup', 'embedded'], }; export const FORM_SUCCESS_ACTIONS = { @@ -39,7 +39,7 @@ export const KIND_CHOICES = { FORM: 'form', TWITTER: 'twitter', FACEBOOK: 'facebook', - ALL_LIST: ['messenger', 'form', 'twitter', 'facebook'], + ALL: ['messenger', 'form', 'twitter', 'facebook'], }; // module constants @@ -87,3 +87,10 @@ export const FORM_FIELDS = { ALL: ['', 'number', 'date', 'email'], }, }; + +// messenger data availability constants +export const MESSENGER_DATA_AVAILABILITY = { + MANUAL: 'manual', + AUTO: 'auto', + ALL: ['manual', 'auto'], +}; diff --git a/src/data/resolvers/mutations/forms.js b/src/data/resolvers/mutations/forms.js index c44630df7..c5822be3a 100644 --- a/src/data/resolvers/mutations/forms.js +++ b/src/data/resolvers/mutations/forms.js @@ -6,10 +6,9 @@ export default { * @param {Object} root * @param {Object} doc - Form object * @param {string} doc.title - Form title - * @param {string} doc.description - Form description + * @param {string} doc.description - Form description * @param {Object} doc.user - The user who created this form * @return {Promise} returns the form promise - * @throws {Error} throws apollo level error based on validation * @throws {Error} throws error if user is not logged in */ formsCreate(root, doc, { user }) { @@ -30,7 +29,6 @@ export default { * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user who is making this action * @return {Promise} returns null - * @throws {Error} apollo level error based on validation * @throws {Error} throws error if user is not logged in */ formsEdit(root, { _id, ...doc }, { user }) { @@ -49,7 +47,6 @@ export default { * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action * @return {Promise} null - * @throws {Error} apollo level error based on validation * @throws {Error} throws error if user is not logged in */ formsRemove(root, { _id }, { user }) { @@ -72,9 +69,8 @@ export default { * @param {Array} object2.options - Form field options * @param {Boolean} object2.isRequired - Shows whether the field is required or not * @param {Object} object3 - Middleware data - * @param {Object} object3.user - + * @param {Object} object3.user - The user making this action * @return {Promise} return Promise(null) - * @throws {Error} throws apollo error based on validation * @throws {Error} throws error if user is not logged in */ formsAddFormField(root, { formId, ...formFieldDoc }, { user }) { @@ -98,7 +94,6 @@ export default { * @param {Object} object3 - Middleware data * @param {Object} object3.user - The user making this action * @return {Promise} return Promise(null) - * @throws {Error} throws apollo error based on validation * @throws {Error} throws error if user is not logged in */ formsEditFormField(root, { _id, ...formFieldDoc }, { user }) { @@ -117,7 +112,6 @@ export default { * @param {Object} object3 - Middleware data * @param {Object} object3.user - The user making this action * @return {Promise} null - * @throws {Error} throws apollo error based on validation * @throws {Error} throws error if user is not logged in */ formsRemoveFormField(root, { _id }, { user }) { @@ -133,10 +127,9 @@ export default { * @param {Object} root * @param {Object} object2 - Graphql input data * @param {Object} object2.orderDics - Dictionary containing order values for form fields - * @param {Object} object3 - The middleware data + * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action * @return {Promise} null - * @throws {Error} throws apollo error based on validation * @throws {Error} throws error if user is not logged in */ formsUpdateFormFieldsOrder(root, { orderDics }, { user }) { @@ -152,10 +145,9 @@ export default { * @param {Object} root * @param {Object} object2 - Graphql input data * @param {string} object2._id - Form id - * @param {Object} object3 - Middleware data + * @param {Object} object3 - Middleware data * @param {Object} object3.user - The user making this action * @return {Promise} returns form object - * @throws {Error} throws apollo error based on validation * @throws {Error} throws error if user is not logged in */ formsDuplicate(root, { _id }, { user }) { diff --git a/src/data/resolvers/mutations/integrations.js b/src/data/resolvers/mutations/integrations.js index 99b291679..eff9ec737 100644 --- a/src/data/resolvers/mutations/integrations.js +++ b/src/data/resolvers/mutations/integrations.js @@ -3,11 +3,13 @@ import { Integrations } from '../../../db/models'; export default { /** * Create a new messenger integration - * @param {Object} - * @param {String} doc.title - * @param {String} doc.brandId - * @return {Promise} returns the messenger integration - * @throws {Error} apollo level error based on validation + * @param {Object} root + * @param {Object} doc - Integration main doc object + * @param {string} doc.name - Integration name + * @param {string} doc.brandId - Integration brand id + * @param {Object} object3 - The middleware data + * @param {Object} object3.user - The user making this action + * @return {Promise} return integration promise * @throws {Error} throws error if user is not logged in */ integrationsCreateMessengerIntegration(root, doc, { user }) { @@ -19,77 +21,78 @@ export default { }, /** - * Edit a messenger integration - * @param {Object} - * @param {String} args.id - * @param {String} args.title - * @param {String} args.brandId - * @return {Promise} returns null - * @throws {Error} apollo level error based on validation + * Update messenger integration + * @param {Object} root + * @param {string} object2 - Integration main document object + * @param {string} object2._id - Integration id + * @param {string} object2.name - Integration name + * @param {string} object2.brandId - Integration brand id + * @param {Object} object3 - The middleware data + * @param {Object} object3.user - The user making this action + * @return {Promise} returns null promise * @throws {Error} throws error if user is not logged in */ - integrationsEditMessengerIntegration(root, { id, ...fields }, { user }) { + integrationsEditMessengerIntegration(root, { _id, ...fields }, { user }) { if (!user) { throw new Error('Login required'); } - return Integrations.updateMessengerIntegration(id, fields); + return Integrations.updateMessengerIntegration(_id, fields); }, /** - * Edit/save messenger appearance data - * @param {Object} - * @param {String} args.id - * @param {String} args.color - * @param {String} args.wallpaper - * @param {String} args.logo - * @return {Promise} returns null - * @throws {Error} apollo level error based on validation + * Update/save messenger appearance data + * @param {Object} root + * @param {Object} object2 Graphql input data + * @param {string} object2._id - Integration id + * @param {Object} object2 - MessengerUiOptions subdocument object + * @param {string} object2.color - MessengerUiOptions color + * @param {string} object2.wallpaper - MessengerUiOptions wallpaper + * @param {string} object2.logo - MessengerUiOptions logo + * @param {Object} object3 - The middleware data + * @param {Object} object3.user - The user making this action + * @return {Promise} returns null promise * @throws {Error} throws error if user is not logged in */ - integrationsSaveMessengerAppearanceData(root, { id, uiOptions }, { user }) { + integrationsSaveMessengerAppearanceData(root, { _id, uiOptions }, { user }) { if (!user) { throw new Error('Login required'); } - return Integrations.saveMessengerAppearanceData(id, uiOptions); + return Integrations.saveMessengerAppearanceData(_id, uiOptions); }, /** - * Edit/save messenger data - * @param {Object} - * @param {String} args.id - * @param {Boolean} args.notifyCustomer - * @param {String} args.availabilityMethod - * @param {Boolean} args.isOnline - * @param {String} args.onlineHours.day - * @param {String} args.onlineHours.from - * @param {String} args.onlineHours.to - * @param {String} args.timezone - * @param {String} args.welcomeMessage - * @param {String} args.awayMessage - * @param {String} args.thankYouMessage - * @return {Promise} returns null - * @throws {Error} apollo level error based on validation + * Update/save messenger data + * @param {Object} root + * @param {Object} object2 - Graphql input data + * @param {string} object2._id - Integration id + * @param {MessengerData} object2.messengerData - MessengerData subdocument + * object related to this integration + * @param {Object} object3 - The middleware data + * @param {Object} object3.user - The user making this action + * @return {Promise} returns null promise * @throws {Error} throws error if user is not logged in */ - integrationsSaveMessengerConfigs(root, { id, messengerData }, { user }) { + integrationsSaveMessengerConfigs(root, { _id, messengerData }, { user }) { if (!user) { throw new Error('Login required'); } - return Integrations.saveMessengerConfigs(id, messengerData); + return Integrations.saveMessengerConfigs(_id, messengerData); }, /** * Create a new messenger integration - * @param {Object} - * @param {String} doc.title - * @param {String} doc.brandId - * @param {String} doc.formId - * @param {Object} doc.formData + * @param {Object} root + * @param {Object} doc - Integration object + * @param {string} doc.name - Integration name + * @param {string} doc.brandId - Integration brand id + * @param {string} doc.formId - Integration form id + * @param {FormData} doc.formData - Integration form data sumbdocument object + * @param {Object} object3 - The middleware data + * @param {Object} object3.user - The user making this action * @return {Promise} returns the messenger integration - * @throws {Error} apollo level error based on validation * @throws {Error} throws error if user is not logged in */ integrationsCreateFormIntegration(root, doc, { user }) { @@ -103,35 +106,41 @@ export default { /** * Edit a form integration * @param {Object} - * @param {String} doc.title - * @param {String} doc.brandId - * @param {String} doc.formId - * @param {Object} doc.formData - * @return {Promise} returns null - * @throws {Error} apollo level error based on validation + * @param {Object} doc - Integration object + * @param {string} doc._id - Integration id + * @param {string} doc.name - Integration name + * @param {string} doc.brandId - Integration brand id + * @param {string} doc.formId - Integration form id + * @param {FormData} doc.formData - Integration form data subdocument object + * @param {Object} object3 - The middleware data + * @param {Object} object3.user - The user making this action + * @return {Promise} returns null promise * @throws {Error} throws error if user is not logged in */ - integrationsEditFormIntegration(root, { id, ...doc }, { user }) { + integrationsEditFormIntegration(root, { _id, ...doc }, { user }) { if (!user) { throw new Error('Login required'); } - return Integrations.updateFormIntegration(id, doc); + return Integrations.updateFormIntegration(_id, doc); }, /** * Delete an integration - * @param {Object} - * @param {String} args.id - * @return {Promise} returns the messenger integration + * @param {Object} root + * @param {Object} object2 - Graphql input data + * @param {string} object2._id - Integration id + * @param {Object} object3 - The middleware data + * @param {Object} object3.user - The user making this action + * @return {Promise} returns null * @throws {Error} apollo level error based on validation * @throws {Error} throws error if user is not logged in */ - integrationsRemove(root, { id }, { user }) { + integrationsRemove(root, { _id }, { user }) { if (!user) { throw new Error('Login required'); } - return Integrations.removeIntegration({ _id: id }); + return Integrations.removeIntegration(_id); }, }; diff --git a/src/data/schema/form.js b/src/data/schema/form.js index 55a2be151..fca8aaec5 100644 --- a/src/data/schema/form.js +++ b/src/data/schema/form.js @@ -23,7 +23,7 @@ export const types = ` } input OrderDicItem { - id: String! + _id: String! order: Int! } `; diff --git a/src/data/schema/integration.js b/src/data/schema/integration.js index 7bc4fb840..0287df973 100644 --- a/src/data/schema/integration.js +++ b/src/data/schema/integration.js @@ -68,13 +68,13 @@ export const mutations = ` brandId: String!): Integration integrationsEditMessengerIntegration( - id: String!, + _id: String!, name: String!, brandId: String!): Integration - integrationsSaveMessengerAppearanceData(id: String!, uiOptions: MessengerUiOptions): Boolean + integrationsSaveMessengerAppearanceData(_id: String!, uiOptions: MessengerUiOptions): Boolean - integrationsSaveMessengerConfigs(id: String!, messengerData: IntegrationMessengerData): Boolean + integrationsSaveMessengerConfigs(_id: String!, messengerData: IntegrationMessengerData): Boolean integrationsCreateFormIntegration( name: String!, @@ -83,7 +83,7 @@ export const mutations = ` formData: IntegrationFormData!): Integration integrationsEditFormIntegration( - id: String! + _id: String! name: String!, brandId: String!, formId: String, diff --git a/src/data/schema/notification.js b/src/data/schema/notification.js index b33570ae7..7445b4dc5 100644 --- a/src/data/schema/notification.js +++ b/src/data/schema/notification.js @@ -20,11 +20,11 @@ export const types = ` `; export const queries = ` - notificationsModules(ids: [String]) : [String] + notificationsModules(_ids: [String]) : [String] `; export const mutations = ` notificationsSaveConfig (notifType: String, isAllowed: Boolean): NotificationConfiguration - notificationsMarkAsRead ( ids: [String]! ) : Boolean + notificationsMarkAsRead ( _ids: [String]! ) : Boolean `; diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index 0a5caa8cc..891579e40 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -3,7 +3,12 @@ import 'mongoose-type-email'; import Random from 'meteor-random'; import { Messages, Conversations } from './Conversations'; import { Customers } from './Customers'; -import { KIND_CHOICES, FORM_SUCCESS_ACTIONS, FORM_LOAD_TYPES } from '../../data/constants'; +import { + KIND_CHOICES, + FORM_SUCCESS_ACTIONS, + FORM_LOAD_TYPES, + MESSENGER_DATA_AVAILABILITY, +} from '../../data/constants'; // subdocument schema for MessengerOnlineHours const MessengerOnlineHoursSchema = mongoose.Schema( @@ -15,21 +20,13 @@ const MessengerOnlineHoursSchema = mongoose.Schema( { _id: false }, ); -// messenger data availability constants -export const MESSENGER_DATA_AVAILABILITY_CONSTANTS = { - MANUAL: 'manual', - AUTO: 'auto', - ALL: ['manual', 'auto'], -}; - // subdocument schema for MessengerData const MessengerDataSchema = mongoose.Schema( { notifyCustomer: Boolean, - // manual, auto availabilityMethod: { type: String, - enum: MESSENGER_DATA_AVAILABILITY_CONSTANTS.ALL, + enum: MESSENGER_DATA_AVAILABILITY.ALL, }, isOnline: { type: Boolean, @@ -82,7 +79,10 @@ const IntegrationSchema = mongoose.Schema({ type: String, default: () => Random.id(), }, - kind: String, + kind: { + type: String, + enum: KIND_CHOICES.ALL, + }, name: String, brandId: String, formId: String, @@ -97,8 +97,8 @@ class Integration { /** * Generate form integration data based on the given form data (formData) * and integration data (mainDoc) - * @param {Object} mainDoc - * @param {Object} formData + * @param {Integration} mainDoc - Integration object without subdocuments + * @param {FormData} formData - Integration forData subdocument * @return {Object} returns an integration object */ static generateFormDoc(mainDoc, formData) { @@ -111,35 +111,46 @@ class Integration { /** * Create an integration, intended as a private method - * @param {String} doc.kind - * @param {String} doc.name - * @param {String} doc.brandId - * @param {String} doc.formId - * @param {String} doc.formData.loadType - * @param {String} doc.formData.successAction - * @param {String} doc.formData.formEmail - * @param {String} doc.formData.userEmailTitle - * @param {String} doc.formData.userEmailContent - * @param {Array} doc.formData.adminEmails - * @param {String} doc.formData.adminEmailTitle - * @param {String} doc.formData.adminEmailContent - * @param {String} doc.formData.thankContent - * @param {String} doc.formData.redirectUrl - * @param {Boolean} doc.messengerData.notifyCustomer - * @param {String} doc.messengerData.availabilityMethod - * @param {Boolean} doc.messengerData.isOnline - * @param {String} doc.messengerData.onlineHours.day - * @param {String} doc.messengerData.onlineHours.from - * @param {String} doc.messengerData.onlineHours.to - * @param {String} doc.messengerData.timezone - * @param {String} doc.messengerData.welcomeMessage - * @param {String} doc.messengerData.awayMessage - * @param {String} doc.messengerData.thankYouMessage - * @param {String} doc.messengerData.uiOptions.color - * @param {String} doc.messengerData.uiOptions.wallpaper - * @param {String} doc.messengerData.uiOptions.logo - * @param {Object} doc.twitterData - * @param {Object} doc.facebookData + * @param {Object} doc - Integration object + * @param {string} doc.kind - Integration kind + * @param {string} doc.name - Integration name + * @param {string} doc.brandId - Brand id of the related Brand + * @param {string} doc.formId - Form id (used in form integrations) + * @param {string} doc.formData.loadType - Load types for the embedded form + * @param {string} doc.formData.successAction - TODO: need more elaborate documentation + * @param {string} doc.formData.formEmail - TODO: need more elaborate documentation + * @param {string} doc.formData.userEmailTitle - TODO: need more elaborate documentation + * @param {string} doc.formData.userEmailContent - TODO: need more elaborate documentation + * @param {Array} doc.formData.adminEmails - TODO: need more elaborate documentation + * @param {string} doc.formData.adminEmailTitle - TODO: need more elaborate documentation + * @param {string} doc.formData.adminEmailContent - TODO: need more elaborate documentation + * @param {string} doc.formData.thankContent - TODO: need more elaborate documentation + * @param {string} doc.formData.redirectUrl - Form redirectUrl on submit + * TODO: need more elaborate documentation + * @param {Object} doc.messengerData - MessengerData object + * @param {Boolean} doc.messengerData.notifyCustomer - Identicates whether + * customer should be notified or not TODO: need more elaborate documentation + * @param {string} doc.messengerData.availabilityMethod - Sets messenger + * availability method as auto or manual TODO: need more elaborate documentation + * @param {Boolean} doc.messengerData.isOnline - Identicates whether messenger in online or not + * @param {Object[]} doc.messengerData.onlineHours - OnlineHours object array + * @param {string} doc.messengerData.onlineHours.day - OnlineHours day + * @param {string} doc.messengerData.onlineHours.from - OnlineHours from + * @param {string} doc.messengerData.onlineHours.to - OnlineHours to + * @param {string} doc.messengerData.timezone - Timezone + * @param {string} doc.messengerData.welcomeMessage - Message displayed on welcome + * TODO: need more elaborate documentation + * @param {string} doc.messengerData.awayMessage - Message displayed when status becomes away + * TODO: need more elaborate documentation + * @param {string} doc.messengerData.thankYouMessage - Thank you message + * TODO: need more elaborate documentation + * @param {string} doc.messengerData.uiOptions.color - Color of messenger + * @param {string} doc.messengerData.uiOptions.wallpaper - Wallpaper image for messenger + * @param {string} doc.messengerData.uiOptions.logo - Logo used in the embedded messenger + * @param {Object} doc.twitterData - Twitter data + * TODO: need more elaborate documentation + * @param {Object} doc.facebookData - Facebook data + * TODO: need more elaborate documentation * @return {Promise} returns integration document promise */ static createIntegration(doc) { @@ -148,8 +159,9 @@ class Integration { /** * Create a messenger kind integration - * @param {String} args.name - * @param {String} args.brandId + * @param {Object} object - Integration object + * @param {string} object.name - Integration name + * @param {String} object.brandId - Integration brand id * @return {Promise} returns integration document promise */ static createMessengerIntegration({ name, brandId }) { @@ -161,10 +173,11 @@ class Integration { } /** - * Update a messenger integration - * @param {String} args.name - * @param {String} args.brandId - * @return {Promise} + * Update messenger integration document + * @param {Object} object - Integration main doc object + * @param {string} object.name - Integration name + * @param {string} object.brandId - Integration brand id + * @return {Promise} return null promise */ static updateMessengerIntegration(_id, { name, brandId }) { return this.update({ _id }, { $set: { name, brandId } }, { runValidators: true }); @@ -172,11 +185,12 @@ class Integration { /** * Save messenger appearance data - * @param {String} _id - * @param {String} args.color - * @param {String} args.wallpaper - * @param {String} args.logo - * @return {Promise} + * @param {string} _id + * @param {Object} object - MessengerUiOptions object TODO: need more elaborate documentation + * @param {string} object.color - MessengerUiOptions color TODO: need more elaborate documentation + * @param {string} object.wallpaper - MessengerUiOptions wallpaper + * @param {string} object.logo - Messenger logo TODO: need more elaborate documentation + * @return {Promise} returns null promise */ static saveMessengerAppearanceData(_id, { color, wallpaper, logo }) { return this.update( @@ -188,20 +202,27 @@ class Integration { /** * Saves messenger data to integration document - * @param {Boolean} messengerData.notifyCustomer - * @param {String} messengerData.availabilityMethod - * @param {Boolean} messengerData.isOnline - * @param {String} messengerData.onlineHours.day - * @param {String} messengerData.onlineHours.from - * @param {String} messengerData.onlineHours.to - * @param {String} messengerData.timezone - * @param {String} messengerData.welcomeMessage - * @param {String} messengerData.awayMessage - * @param {String} messengerData.thankYouMessage - * @param {String} messengerData.uiOptions.color - * @param {String} messengerData.uiOptions.wallpaper - * @param {String} messengerData.uiOptions.logo - * @return {Promise} + * @param {Object} doc.messengerData - MessengerData object + * @param {Boolean} doc.messengerData.notifyCustomer - Identicates whether + * customer should be notified or not TODO: need more elaborate documentation + * @param {string} doc.messengerData.availabilityMethod - Sets messenger + * availability method as auto or manual TODO: need more elaborate documentation + * @param {Boolean} doc.messengerData.isOnline - Identicates whether messenger in online or not + * @param {Object[]} doc.messengerData.onlineHours - OnlineHours object array + * @param {string} doc.messengerData.onlineHours.day - OnlineHours day + * @param {string} doc.messengerData.onlineHours.from - OnlineHours from + * @param {string} doc.messengerData.onlineHours.to - OnlineHours to + * @param {string} doc.messengerData.timezone - Timezone + * @param {string} doc.messengerData.welcomeMessage - Message displayed on welcome + * TODO: need more elaborate documentation + * @param {string} doc.messengerData.awayMessage - Message displayed when status becomes away + * TODO: need more elaborate documentation + * @param {string} doc.messengerData.thankYouMessage - Thank you message + * TODO: need more elaborate documentation + * @param {string} doc.messengerData.uiOptions.color - Color of messenger + * @param {string} doc.messengerData.uiOptions.wallpaper - Wallpaper image for messenger + * @param {string} doc.messengerData.uiOptions.logo - Logo used in the embedded messenger + * @return {Promise} returns null promise */ static saveMessengerConfigs(_id, messengerData) { return this.update({ _id }, { $set: { messengerData } }, { runValidators: true }); @@ -209,19 +230,21 @@ class Integration { /** * Create a form kind integration - * @param {String} args.formData.loadType - * @param {String} args.formData.successAction - * @param {String} args.formData.formEmail - * @param {String} args.formData.userEmailTitle - * @param {String} args.formData.userEmailContent - * @param {Array} args.formData.adminEmails - * @param {String} args.formData.adminEmailTitle - * @param {String} args.formData.adminEmailContent - * @param {String} args.formData.thankContent - * @param {String} args.formData.redirectUrl - * @param {String} args.mainDoc.name - * @param {String} args.mainDoc.brandId - * @param {String} args.mainDoc.formId + * @param {Object} args.formData - FormData object + * @param {string} doc.formData.loadType - Load types for the embedded form + * @param {string} doc.formData.successAction - TODO: need more elaborate documentation + * @param {string} doc.formData.formEmail - TODO: need more elaborate documentation + * @param {string} doc.formData.userEmailTitle - TODO: need more elaborate documentation + * @param {string} doc.formData.userEmailContent - TODO: need more elaborate documentation + * @param {Email[]} doc.formData.adminEmails - TODO: need more elaborate documentation + * @param {string} doc.formData.adminEmailTitle - TODO: need more elaborate documentation + * @param {string} doc.formData.adminEmailContent - TODO: need more elaborate documentation + * @param {string} doc.formData.thankContent - TODO: need more elaborate documentation + * @param {string} doc.formData.redirectUrl - Form redirectUrl on submit + * @param {string} args.mainDoc - Integration main document object + * @param {string} args.mainDoc.name - Integration name + * @param {string} args.mainDoc.brandId - Integration brand id + * @param {string} args.mainDoc.formId - Form id related to this integration * @return {Promise} returns form integration document promise * @throws {Exception} throws Exception if formData is notSupplied */ @@ -236,22 +259,24 @@ class Integration { } /** - * Update a form kind integration - * @param {String} _id integration id - * @param {String} args.formData.loadType - * @param {String} args.formData.successAction - * @param {String} args.formData.formEmail - * @param {String} args.formData.userEmailTitle - * @param {String} args.formData.userEmailContent - * @param {Array} args.formData.adminEmails - * @param {String} args.formData.adminEmailTitle - * @param {String} args.formData.adminEmailContent - * @param {String} args.formData.thankContent - * @param {String} args.formData.redirectUrl - * @param {String} args.mainDoc.name - * @param {String} args.mainDoc.brandId - * @param {String} args.mainDoc.formId - * @return {Promise} + * Update form integration + * @param {string} _id integration id + * @param {Object} args.formData - FormData object + * @param {string} doc.formData.loadType - Load types for the embedded form + * @param {string} doc.formData.successAction - TODO: need more elaborate documentation + * @param {string} doc.formData.formEmail - TODO: need more elaborate documentation + * @param {string} doc.formData.userEmailTitle - TODO: need more elaborate documentation + * @param {string} doc.formData.userEmailContent - TODO: need more elaborate documentation + * @param {Email[]} doc.formData.adminEmails - TODO: need more elaborate documentation + * @param {string} doc.formData.adminEmailTitle - TODO: need more elaborate documentation + * @param {string} doc.formData.adminEmailContent - TODO: need more elaborate documentation + * @param {string} doc.formData.thankContent - TODO: need more elaborate documentation + * @param {string} doc.formData.redirectUrl - Form redirectUrl on submit + * @param {string} args.mainDoc - Integration main document object + * @param {string} args.mainDoc.name - Integration name + * @param {string} args.mainDoc.brandId - Integration brand id + * @param {string} args.mainDoc.formId - Form id related to this integration + * @return {Promise} returns null promise */ static updateFormIntegration(_id, { formData, ...mainDoc }) { const doc = this.generateFormDoc(mainDoc, formData); @@ -260,12 +285,12 @@ class Integration { } /** - * Removes an integration plus its messages, conversations, customers - * @param {String} id - * @return {Promise} + * Remove integration in addition with its messages, conversations, customers + * @param {string} id - Integration id + * @return {Promise} returns null promise */ - static async removeIntegration(id) { - const conversations = await Conversations.find({ integrationId: id }, { _id: true }); + static async removeIntegration(_id) { + const conversations = await Conversations.find({ integrationId: _id }, { _id: true }); const conversationIds = []; @@ -277,12 +302,12 @@ class Integration { await Messages.remove({ conversationId: { $in: conversationIds } }); // Remove conversations - await Conversations.remove({ integrationId: id }); + await Conversations.remove({ integrationId: _id }); // Remove customers - await Customers.remove({ integrationId: id }); + await Customers.remove({ integrationId: _id }); - return this.remove(id); + return this.remove({ _id }); } } From f0cb5b82895464d558b532f127c12c1ecb5138a8 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 13 Oct 2017 00:55:01 +0800 Subject: [PATCH 049/318] #13 Refactor form, notification, integration, channel mutations --- src/__tests__/formMutations.test.js | 20 ++++++-------------- src/__tests__/integrationMutations.test.js | 2 +- src/__tests__/notificationQueries.test.js | 7 +++---- src/data/resolvers/queries/notifications.js | 2 +- src/data/schema/notification.js | 5 ++--- src/db/factories.js | 4 ++-- src/db/models/Forms.js | 4 ++-- 7 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/__tests__/formMutations.test.js b/src/__tests__/formMutations.test.js index d20ed954b..4bc6e4c09 100644 --- a/src/__tests__/formMutations.test.js +++ b/src/__tests__/formMutations.test.js @@ -91,27 +91,19 @@ describe('form update tests', () => { }); describe('form remove tests', async () => { - let _user; + let _form; beforeEach(async () => { - _user = await userFactory({}); + _form = await formFactory({}); }); afterEach(async () => { - await Users.remove({}); await Forms.remove({}); }); - test('check whether form removal is working successfully', async () => { - const form = await Forms.createForm( - { - title: 'Test form', - description: 'Test form description', - }, - _user._id, - ); + test('check if form removal is working successfully', async () => { + await Forms.removeForm(_form._id); - await Forms.removeForm(form._id); const formCount = await Forms.find({}).count(); expect(formCount).toBe(0); @@ -155,6 +147,8 @@ describe('test exception in remove form method', async () => { expect(e.message).toEqual('You cannot delete this form. This form has some fields.'); } + await FormFields.remove({}); + await integrationFactory({ formId: _form._id, formData: { @@ -163,8 +157,6 @@ describe('test exception in remove form method', async () => { }, }); - await FormFields.remove({}); - try { await Forms.removeForm(_form._id); } catch (e) { diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js index abf5072be..de715afec 100644 --- a/src/__tests__/integrationMutations.test.js +++ b/src/__tests__/integrationMutations.test.js @@ -46,7 +46,7 @@ describe('messenger integration model edit test', () => { _brand2 = await brandFactory({}); _integration = await integrationFactory({ kind: KIND_CHOICES.MESSENGER, - brandId: _brand, + brandId: _brand._id, }); }); diff --git a/src/__tests__/notificationQueries.test.js b/src/__tests__/notificationQueries.test.js index 1b705f3eb..c5b2e7c45 100644 --- a/src/__tests__/notificationQueries.test.js +++ b/src/__tests__/notificationQueries.test.js @@ -2,15 +2,14 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; import { MODULES } from '../data/constants'; -import queries from '../data/resolvers/queries'; +import notificationsQueries from '../data/resolvers/queries/notifications'; beforeAll(() => connect()); afterAll(() => disconnect()); describe('notification query test', () => { test('test of getting notification list with success', () => { - const modules = queries.notificationsModules(); - - expect(modules).toEqual(MODULES.ALL); + const modules = notificationsQueries.notificationsModules(); + expect(modules).toBe(MODULES.ALL); }); }); diff --git a/src/data/resolvers/queries/notifications.js b/src/data/resolvers/queries/notifications.js index c29cc112d..15da83c20 100644 --- a/src/data/resolvers/queries/notifications.js +++ b/src/data/resolvers/queries/notifications.js @@ -4,7 +4,7 @@ export default { /** * Module list used in notifications * @param {Object} args - * @return {Promise} module list + * @return {String[]} returns module list */ notificationsModules() { return MODULES.ALL; diff --git a/src/data/schema/notification.js b/src/data/schema/notification.js index 7445b4dc5..af2663f7a 100644 --- a/src/data/schema/notification.js +++ b/src/data/schema/notification.js @@ -20,11 +20,10 @@ export const types = ` `; export const queries = ` - notificationsModules(_ids: [String]) : [String] + notificationsModules : [String] `; export const mutations = ` notificationsSaveConfig (notifType: String, isAllowed: Boolean): NotificationConfiguration - - notificationsMarkAsRead ( _ids: [String]! ) : Boolean + notificationsMarkAsRead (_ids: [String]!) : Boolean `; diff --git a/src/db/factories.js b/src/db/factories.js index 4651a936c..31a400315 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -87,14 +87,14 @@ export const tagsFactory = (params = {}) => { return tag.save(); }; -export const formFactory = ({ title, code, description, createdUserId }) => { +export const formFactory = async ({ title, code, description, createdUserId }) => { return Forms.createForm( { title: title || faker.random.word(), description: description || faker.random.word(), code: code || Random.id(), }, - createdUserId, + createdUserId || (await userFactory({})), ); }; diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index 58e24a93b..9ee361316 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -77,7 +77,7 @@ class Form { /** * Remove a form * @param {string} _id - Form document id - * @return {Promise} returns null + * @return {Null} returns null * @throws {Error} throws Error if this form has fields or if used in an integration */ static async removeForm(_id) { @@ -93,7 +93,7 @@ class Form { throw new Error('You cannot delete this form. This form used in integration.'); } - return this.remove({ _id }); + return await this.remove({ _id }); } /** From a91884d4ceb52f7370aa640072d917aea6ea46c9 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 13 Oct 2017 12:45:50 +0800 Subject: [PATCH 050/318] Add customers on company type --- src/data/resolvers/company.js | 7 +++++++ src/data/resolvers/index.js | 2 ++ src/data/schema/company.js | 2 ++ 3 files changed, 11 insertions(+) create mode 100644 src/data/resolvers/company.js diff --git a/src/data/resolvers/company.js b/src/data/resolvers/company.js new file mode 100644 index 000000000..dcdb76238 --- /dev/null +++ b/src/data/resolvers/company.js @@ -0,0 +1,7 @@ +import { Customers } from '../../db/models'; + +export default { + customers(company) { + return Customers.find({ companyIds: { $in: [company._id] } }); + }, +}; diff --git a/src/data/resolvers/index.js b/src/data/resolvers/index.js index 19eeceb39..e27aaae32 100644 --- a/src/data/resolvers/index.js +++ b/src/data/resolvers/index.js @@ -8,6 +8,7 @@ import Form from './form'; import EngageMessage from './engage'; import InternalNote from './internalNote'; import Customer from './customer'; +import Company from './company'; import Segment from './segment'; import Conversation from './conversation'; import ConversationMessage from './conversationMessage'; @@ -22,6 +23,7 @@ export default { Form, InternalNote, Customer, + Company, Segment, EngageMessage, Conversation, diff --git a/src/data/schema/company.js b/src/data/schema/company.js index c8ed5e179..06e666ff6 100644 --- a/src/data/schema/company.js +++ b/src/data/schema/company.js @@ -18,6 +18,8 @@ export const types = ` tagIds: [String], customFieldsData: JSON + + customers: [Customer] } `; From fffab6aec351dd946ef3d34a8741899b108f3b86 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 13 Oct 2017 13:20:10 +0800 Subject: [PATCH 051/318] #13 Refactor notifications --- src/__tests__/integrationMutations.test.js | 10 +- src/__tests__/notificationDb.test.js | 149 +++++++++++ src/__tests__/notificationMutations.test.js | 233 ++---------------- src/__tests__/notificationTools.test.js | 72 ++++++ src/data/resolvers/mutations/notifications.js | 2 +- src/db/models/Channels.js | 7 +- src/db/models/Forms.js | 13 +- src/db/models/Integrations.js | 31 ++- src/db/models/Notifications.js | 11 +- 9 files changed, 279 insertions(+), 249 deletions(-) create mode 100644 src/__tests__/notificationDb.test.js create mode 100644 src/__tests__/notificationTools.test.js diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js index de715afec..b76a429b5 100644 --- a/src/__tests__/integrationMutations.test.js +++ b/src/__tests__/integrationMutations.test.js @@ -234,8 +234,7 @@ describe('remove integration model method test', () => { describe('save integration messenger appearance test', () => { let _brand; let _integration; - /** - */ + beforeEach(async () => { _brand = await brandFactory({}); _integration = await integrationFactory({ @@ -245,8 +244,6 @@ describe('save integration messenger appearance test', () => { }); }); - /** - */ afterEach(async () => { await Brands.remove({}); await Integrations.remove({}); @@ -278,7 +275,7 @@ describe('save integration messenger configurations test', () => { _integration = await integrationFactory({ name: 'messenger integration test', brandId: _brand._id, - kind: 'messenger', + kind: KIND_CHOICES.MESSENGER, }); }); @@ -287,7 +284,8 @@ describe('save integration messenger configurations test', () => { await Integrations.remove({}); }); - test('test if integration messenger save confiturations method is working correctly', async () => { + test(`test if messenger integration save confiturations + method is working correctly`, async () => { const messengerData = { notifyCustomer: true, availabilityMethod: MESSENGER_DATA_AVAILABILITY.MANUAL, diff --git a/src/__tests__/notificationDb.test.js b/src/__tests__/notificationDb.test.js new file mode 100644 index 000000000..9d4e09734 --- /dev/null +++ b/src/__tests__/notificationDb.test.js @@ -0,0 +1,149 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Notifications, NotificationConfigurations, Users } from '../db/models'; +import { userFactory, notificationConfigurationFactory } from '../db/factories'; +import { MODULES } from '../data/constants'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('Notification model tests', () => { + let _user; + let _user2; + + beforeEach(async () => { + _user = await userFactory({}); + _user2 = await userFactory({}); + }); + + afterEach(async () => { + Notifications.remove({}); + NotificationConfigurations.remove({}); + Users.remove({}); + }); + + test('check for error in model creation', async () => { + expect.assertions(1); + + await notificationConfigurationFactory({ + user: _user2._id, + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + isAllowed: false, + }); + + // Create notification + let doc = { + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + title: 'new Notification title', + content: 'new Notification content', + link: 'new Notification link', + receiver: _user2._id, + }; + + try { + await Notifications.createNotification(doc, _user._id); + } catch (e) { + expect(e.message).toEqual('Configuration does not exist'); + } + }); + + test('model create, update, remove', async () => { + // Create notification ================ + + let doc = { + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + title: 'new Notification title', + content: 'new Notification content', + link: 'new Notification link', + receiver: _user2._id, + }; + + let notification = await Notifications.createNotification(doc, _user._id); + + expect(notification.notifType).toEqual(doc.notifType); + expect(notification.createdUser).toEqual(_user._id); + expect(notification.title).toEqual(doc.title); + expect(notification.content).toEqual(doc.content); + expect(notification.link).toEqual(doc.link); + expect(notification.receiver).toEqual(doc.receiver); + + // Update notification =============== + let user3 = await userFactory({}); + + doc = { + notifType: 'channelMembersChange 2', + title: 'new Notification title 2', + content: 'new Notification content 2', + link: 'new Notification link 2', + receiver: user3, + }; + + await Notifications.updateNotification(notification._id, doc); + + notification = await Notifications.findOne({ _id: notification._id }); + + expect(notification.notifType).toEqual(doc.notifType); + expect(notification.title).toEqual(doc.title); + expect(notification.content).toEqual(doc.content); + expect(notification.link).toEqual(doc.link); + expect(notification.receivers).toEqual(doc.receivers); + + // check method markAsRead ============= + await Notifications.markAsRead([notification._id]); + + notification = await Notifications.findOne({ _id: notification._id }); + + expect(notification.isRead).toEqual(true); + + // remove notification ================= + await Notifications.removeNotification(notification._id); + + expect(await Notifications.find({}).count()).toEqual(0); + }); + + test('sending notifications', () => {}); +}); + +describe('NotificationConfiguration model tests', async () => { + test('test if model methods are working correctly', async () => { + // creating new notification configuration ========== + const user = await userFactory({}); + + const doc = { + notifType: MODULES.CONVERSATION_ADD_MESSAGE, + isAllowed: true, + }; + + let notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration( + doc, + user, + ); + + expect(notificationConfigurations.notifType).toEqual(doc.notifType); + expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); + expect(notificationConfigurations.user).toEqual(user._id); + + // creating another notification configuration ============ + doc.notifType = MODULES.CONVERSATION_ASSIGNEE_CHANGE; + + notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration( + doc, + user, + ); + + expect(notificationConfigurations.notifType).toEqual(doc.notifType); + expect(notificationConfigurations.user).toEqual(user._id); + + // Changing the last added notification ========================= + doc.isAllowed = false; + + notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration( + doc, + user, + ); + + expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); + }); +}); diff --git a/src/__tests__/notificationMutations.test.js b/src/__tests__/notificationMutations.test.js index 9b814d887..40c8f6b02 100644 --- a/src/__tests__/notificationMutations.test.js +++ b/src/__tests__/notificationMutations.test.js @@ -1,257 +1,60 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ + import { connect, disconnect } from '../db/connection'; -import { Notifications, NotificationConfigurations, Users } from '../db/models'; -import mutations from '../data/resolvers/mutations'; -import { userFactory, notificationConfigurationFactory } from '../db/factories'; -import { sendNotification } from '../data/utils'; -import { MODULES } from '../data/constants'; +import { Notifications, NotificationConfigurations } from '../db/models'; +import NotificationMutations from '../data/resolvers/mutations/notifications'; +import { userFactory } from '../db/factories'; +import { Users } from '../db/models'; beforeAll(() => connect()); afterAll(() => disconnect()); -describe('Notification tests', () => { +describe('testing mutations', () => { let _user; - let _user2; beforeEach(async () => { _user = await userFactory({}); - _user2 = await userFactory({}); - }); - - afterEach(async () => { - Notifications.remove({}); - NotificationConfigurations.remove({}); - Users.remove({}); - }); - - test('check for error in model creation', async () => { - expect.assertions(1); - - await notificationConfigurationFactory({ - user: _user2._id, - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, - isAllowed: false, - }); - - // Create notification - let doc = { - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, - title: 'new Notification title', - content: 'new Notification content', - link: 'new Notification link', - receiver: _user2._id, - }; - - try { - await Notifications.createNotification(doc, _user._id); - } catch (e) { - expect(e.message).toEqual('Configuration does not exist'); - } - }); - - test('model create, update, remove', async () => { - // Create notification ================ - - let doc = { - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, - title: 'new Notification title', - content: 'new Notification content', - link: 'new Notification link', - receiver: _user2._id, - }; - - let notification = await Notifications.createNotification(doc, _user._id); - - expect(notification.notifType).toEqual(doc.notifType); - expect(notification.createdUser).toEqual(_user._id); - expect(notification.title).toEqual(doc.title); - expect(notification.content).toEqual(doc.content); - expect(notification.link).toEqual(doc.link); - expect(notification.receiver).toEqual(doc.receiver); - - // Update notification =============== - let user3 = await userFactory({}); - - doc = { - notifType: 'channelMembersChange 2', - title: 'new Notification title 2', - content: 'new Notification content 2', - link: 'new Notification link 2', - receiver: user3, - }; - - await Notifications.updateNotification(notification._id, doc); - - notification = await Notifications.findOne({ _id: notification._id }); - - expect(notification.notifType).toEqual(doc.notifType); - expect(notification.title).toEqual(doc.title); - expect(notification.content).toEqual(doc.content); - expect(notification.link).toEqual(doc.link); - expect(notification.receivers).toEqual(doc.receivers); - - // check method markAsRead ============= - await Notifications.markAsRead([notification._id]); - - notification = await Notifications.findOne({ _id: notification._id }); - - expect(notification.isRead).toEqual(true); - - // remove notification ================= - await Notifications.removeNotification(notification._id); - - expect(await Notifications.find({}).count()).toEqual(0); - }); - - test('sending notifications', () => {}); -}); - -describe('NotificationConfiguration model tests', async () => { - test('test if model methods are working correctly', async () => { - // creating new notification configuration ========== - const user = await userFactory({}); - - const doc = { - notifType: MODULES.CONVERSATION_ADD_MESSAGE, - isAllowed: true, - }; - - let notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration( - doc, - user, - ); - - expect(notificationConfigurations.notifType).toEqual(doc.notifType); - expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); - expect(notificationConfigurations.user).toEqual(user._id); - - // creating another notification configuration ============ - doc.notifType = MODULES.CONVERSATION_ASSIGNEE_CHANGE; - - notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration( - doc, - user, - ); - - expect(notificationConfigurations.notifType).toEqual(doc.notifType); - expect(notificationConfigurations.user).toEqual(user._id); - - // Changing the last added notification ========================= - doc.isAllowed = false; - - notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration( - doc, - user, - ); - - expect(notificationConfigurations.isAllowed).toEqual(doc.isAllowed); }); -}); - -describe('testings helper methods', () => { - test('testing tools.sendNotification method', async () => { - const _user = await userFactory({}); - const _user2 = await userFactory({}); - const _user3 = await userFactory({}); - - // Try to send notifications when there is config not allowing it ========= - await notificationConfigurationFactory({ - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, - isAllowed: false, - user: _user._id, - }); - - await notificationConfigurationFactory({ - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, - isAllowed: false, - user: _user2._id, - }); - - await notificationConfigurationFactory({ - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, - isAllowed: false, - user: _user3._id, - }); - - const doc = { - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, - createdUser: _user._id, - title: 'new Notification title', - content: 'new Notification content', - link: 'new Notification link', - receivers: [_user._id, _user2._id, _user3._id], - }; - - await sendNotification(doc); - let notifications = await Notifications.find({}); - - expect(notifications.length).toEqual(0); - - // Send notifications when there is config allowing it ==================== - await NotificationConfigurations.update({}, { isAllowed: true }, { multi: true }); - - await sendNotification(doc); - - notifications = await Notifications.find({}); - - expect(notifications.length).toEqual(3); - - expect(notifications[0].notifType).toEqual(doc.notifType); - expect(notifications[0].createdUser).toEqual(doc.createdUser); - expect(notifications[0].title).toEqual(doc.title); - expect(notifications[0].content).toEqual(doc.content); - expect(notifications[0].link).toEqual(doc.link); - expect(notifications[0].receiver).toEqual(_user._id); - - expect(notifications[1].receiver).toEqual(_user2._id); - - expect(notifications[2].receiver).toEqual(_user3._id); - }); -}); - -describe('testing mutations', () => { - beforeEach(() => {}); afterEach(async () => { - await Notifications.remove({}); - await NotificationConfigurations.remove({}); + await Users.remove({}); }); test('test if `logging required` error is working as intended', () => { expect.assertions(2); - // Login required - expect(() => mutations.notificationsSaveConfig(null, {}, {})).toThrowError('Login required'); + // Login required ================== + expect(() => NotificationMutations.notificationsSaveConfig(null, {}, {})).toThrowError( + 'Login required', + ); - expect(() => mutations.notificationsMarkAsRead(null, {}, {})).toThrowError('Login required'); + expect(() => NotificationMutations.notificationsMarkAsRead(null, {}, {})).toThrowError( + 'Login required', + ); }); test('testing if notification configuration is saved and updated successfully', async () => { NotificationConfigurations.createOrUpdateConfiguration = jest.fn(); - const user = await userFactory({}); - const doc = { notifType: 'conversationAddMessage', isAllowed: true, - user: user._id, + user: _user._id, }; - await mutations.notificationsSaveConfig(null, doc, { user }); + await NotificationMutations.notificationsSaveConfig(null, doc, { user: _user }); - expect(NotificationConfigurations.createOrUpdateConfiguration).toBeCalledWith(doc, user); + expect(NotificationConfigurations.createOrUpdateConfiguration).toBeCalledWith(doc, _user); expect(NotificationConfigurations.createOrUpdateConfiguration.mock.calls.length).toBe(1); }); test('testing if notifications are being marked as read successfully', async () => { Notifications.markAsRead = jest.fn(); - const user = await userFactory({}); - const args = { ids: ['11111', '22222'] }; - await mutations.notificationsMarkAsRead(null, args, { user }); + await NotificationMutations.notificationsMarkAsRead(null, args, { user: _user }); expect(Notifications.markAsRead).toBeCalledWith(args['ids']); expect(Notifications.markAsRead.mock.calls.length).toBe(1); diff --git a/src/__tests__/notificationTools.test.js b/src/__tests__/notificationTools.test.js new file mode 100644 index 000000000..f8765e538 --- /dev/null +++ b/src/__tests__/notificationTools.test.js @@ -0,0 +1,72 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { userFactory, notificationConfigurationFactory } from '../db/factories'; +import { MODULES } from '../data/constants'; +import { sendNotification } from '../data/utils'; +import { Notifications, NotificationConfigurations } from '../db/models'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('testings helper methods', () => { + test('testing tools.sendNotification method', async () => { + const _user = await userFactory({}); + const _user2 = await userFactory({}); + const _user3 = await userFactory({}); + + // Try to send notifications when there is config not allowing it ========= + await notificationConfigurationFactory({ + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + isAllowed: false, + user: _user._id, + }); + + await notificationConfigurationFactory({ + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + isAllowed: false, + user: _user2._id, + }); + + await notificationConfigurationFactory({ + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + isAllowed: false, + user: _user3._id, + }); + + const doc = { + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + createdUser: _user._id, + title: 'new Notification title', + content: 'new Notification content', + link: 'new Notification link', + receivers: [_user._id, _user2._id, _user3._id], + }; + + await sendNotification(doc); + let notifications = await Notifications.find({}); + + expect(notifications.length).toEqual(0); + + // Send notifications when there is config allowing it ==================== + await NotificationConfigurations.update({}, { isAllowed: true }, { multi: true }); + + await sendNotification(doc); + + notifications = await Notifications.find({}); + + expect(notifications.length).toEqual(3); + + expect(notifications[0].notifType).toEqual(doc.notifType); + expect(notifications[0].createdUser).toEqual(doc.createdUser); + expect(notifications[0].title).toEqual(doc.title); + expect(notifications[0].content).toEqual(doc.content); + expect(notifications[0].link).toEqual(doc.link); + expect(notifications[0].receiver).toEqual(_user._id); + + expect(notifications[1].receiver).toEqual(_user2._id); + + expect(notifications[2].receiver).toEqual(_user3._id); + }); +}); diff --git a/src/data/resolvers/mutations/notifications.js b/src/data/resolvers/mutations/notifications.js index 29044efa4..cf7c9be73 100644 --- a/src/data/resolvers/mutations/notifications.js +++ b/src/data/resolvers/mutations/notifications.js @@ -9,7 +9,7 @@ export default { * @param {Boolean} object2.isAllowed - Shows whether notifications will be received or not * @param {Object} object3 - Middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} returns notification promise + * @return {Promise} returns Promise resolving a Notification document * @throws {Error} throws error if user is not logged in */ notificationsSaveConfig(root, doc, { user }) { diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index 5d7aab1c8..fb67919b3 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -75,9 +75,9 @@ class Channel { * @param {string[]} doc.integrationIds - Integration ids related to the channel * @param {string} doc.userId - The user id or object that craeted this channel * @param {string[]} doc.memberIds - Member ids of the members assigned to this channel - * @return {Promise} returns null promise + * @return {Promise} returns Promise resolving updated channel document */ - static updateChannel(_id, doc) { + static async updateChannel(_id, doc) { const { userId } = doc; if (userId && userId._id) { @@ -86,7 +86,8 @@ class Channel { this.preSave(doc); - return this.update({ _id }, { $set: doc }, { runValidators: true }); + await this.update({ _id }, { $set: doc }, { runValidators: true }); + return this.findOne({}); } /** diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index 9ee361316..66b6a61d4 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -50,14 +50,14 @@ class Form { * @return {Promise} returns Form document promise * @throws {Error} throws Error if createdUser is not supplied */ - static async createForm(doc, createdUser) { - if (!createdUser) { + static async createForm(doc, createdUserId) { + if (!createdUserId) { throw new Error('createdUser must be supplied'); } doc.code = await this.generateCode(); doc.createdDate = new Date(); - doc.createdUserId = createdUser; + doc.createdUserId = createdUserId; return this.create(doc); } @@ -68,10 +68,11 @@ class Form { * @param {Object} object - Form object * @param {string} object.title - Form title * @param {string} object.description - Form description - * @return {Promise} returns null + * @return {Promise} returns Promise resolving updated Form document */ - static updateForm(_id, { title, description }) { - return this.update({ _id }, { $set: { title, description } }, { runValidators: true }); + static async updateForm(_id, { title, description }) { + await this.update({ _id }, { $set: { title, description } }, { runValidators: true }); + return this.findOne({ _id }); } /** diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index 891579e40..67a24aa62 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -177,10 +177,11 @@ class Integration { * @param {Object} object - Integration main doc object * @param {string} object.name - Integration name * @param {string} object.brandId - Integration brand id - * @return {Promise} return null promise + * @return {Promise} returns Promise resolving updated Integration documetn */ - static updateMessengerIntegration(_id, { name, brandId }) { - return this.update({ _id }, { $set: { name, brandId } }, { runValidators: true }); + static async updateMessengerIntegration(_id, { name, brandId }) { + await this.update({ _id }, { $set: { name, brandId } }, { runValidators: true }); + return this.findOne({ _id }); } /** @@ -190,14 +191,16 @@ class Integration { * @param {string} object.color - MessengerUiOptions color TODO: need more elaborate documentation * @param {string} object.wallpaper - MessengerUiOptions wallpaper * @param {string} object.logo - Messenger logo TODO: need more elaborate documentation - * @return {Promise} returns null promise + * @return {Promise} returns Promise resolving updated Integration document */ - static saveMessengerAppearanceData(_id, { color, wallpaper, logo }) { - return this.update( + static async saveMessengerAppearanceData(_id, { color, wallpaper, logo }) { + await this.update( { _id }, { $set: { uiOptions: { color, wallpaper, logo } } }, { runValdatiors: true }, ); + + return this.findOne({ _id }); } /** @@ -222,10 +225,11 @@ class Integration { * @param {string} doc.messengerData.uiOptions.color - Color of messenger * @param {string} doc.messengerData.uiOptions.wallpaper - Wallpaper image for messenger * @param {string} doc.messengerData.uiOptions.logo - Logo used in the embedded messenger - * @return {Promise} returns null promise + * @return {Promise} returns Promise resolving updated Integration document */ - static saveMessengerConfigs(_id, messengerData) { - return this.update({ _id }, { $set: { messengerData } }, { runValidators: true }); + static async saveMessengerConfigs(_id, messengerData) { + await this.update({ _id }, { $set: { messengerData } }, { runValidators: true }); + return this.findOne({ _id }); } /** @@ -276,18 +280,19 @@ class Integration { * @param {string} args.mainDoc.name - Integration name * @param {string} args.mainDoc.brandId - Integration brand id * @param {string} args.mainDoc.formId - Form id related to this integration - * @return {Promise} returns null promise + * @return {Promise} returns Promise resolving updated Integration document */ - static updateFormIntegration(_id, { formData, ...mainDoc }) { + static async updateFormIntegration(_id, { formData, ...mainDoc }) { const doc = this.generateFormDoc(mainDoc, formData); - return this.update({ _id }, { $set: doc }, { runValidators: true }); + await this.update({ _id }, { $set: doc }, { runValidators: true }); + return this.findOne({ _id }); } /** * Remove integration in addition with its messages, conversations, customers * @param {string} id - Integration id - * @return {Promise} returns null promise + * @return {Promise} */ static async removeIntegration(_id) { const conversations = await Conversations.find({ integrationId: _id }, { _id: true }); diff --git a/src/db/models/Notifications.js b/src/db/models/Notifications.js index 727ff0030..6a7d2daf8 100644 --- a/src/db/models/Notifications.js +++ b/src/db/models/Notifications.js @@ -82,10 +82,11 @@ class Notification { * @param {string} doc.content - Notification content * @param {string} doc.link - Notification link * @param {Object|string} doc.receiver - Id of the user that will receive this notification - * @return {Promise} The promise returns null + * @return {Promise} returns Promise resolving updated Notification document */ - static updateNotification(_id, doc) { - return this.update({ _id }, doc); + static async updateNotification(_id, doc) { + await this.update({ _id }, doc); + return this.findOne({ _id }); } /** @@ -125,7 +126,7 @@ class Configuration { * @param {Boolean} object.isAllowed - Indicates whether notifications * will be received or not on the given channel * @param {Object|string} user - The object or id of the user making this action - * @return {Promise} returns NotificationConfigurations object promise + * @return {Promise} returns Promise resolving NotificationConfigurations document */ static async createOrUpdateConfiguration({ notifType, isAllowed }, user) { if (!user) { @@ -140,7 +141,7 @@ class Configuration { if (oldOne) { await this.update({ _id: oldOne._id }, { $set: { isAllowed } }); - return await this.findOne({ _id: oldOne._id }); + return this.findOne({ _id: oldOne._id }); } // If it is first time then insert From e8b0ee827122cc0e4a4af105e387433c29262d4d Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 13 Oct 2017 13:44:48 +0800 Subject: [PATCH 052/318] #13 Refactor integrations --- src/__tests__/integrationDb.test.js | 393 ++++++++++++++ src/__tests__/integrationMutations.test.js | 494 +++--------------- src/data/resolvers/mutations/integrations.js | 29 +- src/data/resolvers/mutations/notifications.js | 6 +- 4 files changed, 485 insertions(+), 437 deletions(-) create mode 100644 src/__tests__/integrationDb.test.js diff --git a/src/__tests__/integrationDb.test.js b/src/__tests__/integrationDb.test.js new file mode 100644 index 000000000..5b3c8aeb2 --- /dev/null +++ b/src/__tests__/integrationDb.test.js @@ -0,0 +1,393 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import faker from 'faker'; +import { connect, disconnect } from '../db/connection'; +import { KIND_CHOICES, FORM_LOAD_TYPES, MESSENGER_DATA_AVAILABILITY } from '../data/constants'; +import { brandFactory, integrationFactory, formFactory, userFactory } from '../db/factories'; +import { Integrations, Brands, Users, Forms } from '../db/models'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('messenger integration model add method', () => { + let _brand; + + beforeEach(async () => { + _brand = await brandFactory({}); + }); + + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + }); + + test('check if messenger integration create method is running successfully', async () => { + const doc = { + name: 'Integration test', + brandId: _brand._id, + }; + + const integration = await Integrations.createMessengerIntegration(doc); + + expect(integration.name).toBe(doc.name); + expect(integration.brandId).toBe(doc.brandId); + expect(integration.kind).toBe(KIND_CHOICES.MESSENGER); + }); +}); + +describe('messenger integration model edit method', () => { + let _brand; + let _integration; + let _brand2; + + beforeEach(async () => { + _brand = await brandFactory({}); + _brand2 = await brandFactory({}); + _integration = await integrationFactory({ + kind: KIND_CHOICES.MESSENGER, + brandId: _brand._id, + }); + }); + + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + }); + + test('check if messenger integration update method is running successfully', async () => { + const doc = { + name: 'Integration test 2', + brandId: _brand2._id, + kind: 'new kind', + }; + + await Integrations.updateMessengerIntegration(_integration._id, doc); + + const updatedIntegration = await Integrations.findOne({ _id: _integration._id }); + + expect(updatedIntegration.name).toBe(doc.name); + expect(updatedIntegration.brandId).toBe(doc.brandId); + expect(updatedIntegration.kind).toBe(KIND_CHOICES.MESSENGER); + }); +}); + +describe('form integration create model test without formData', () => { + let _brand; + let _form; + let _user; + + beforeEach(async () => { + _brand = await brandFactory({}); + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + }); + + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + await Users.remove({}); + await Forms.remove({}); + }); + + test('check if create form integration test wihtout formData is throwing exception', async () => { + expect.assertions(1); + + const mainDoc = { + name: 'form integration test', + brandId: _brand._id, + formId: _form._id, + }; + + try { + await Integrations.createFormIntegration(mainDoc); + } catch (e) { + expect(e.message).toEqual('formData must be supplied'); + } + }); +}); + +describe('create form integration', () => { + let _brand; + let _form; + let _user; + + beforeEach(async () => { + _brand = await brandFactory({}); + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + }); + + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + await Users.remove({}); + await Forms.remove({}); + }); + + test('test if create form integration is working successfully', async () => { + const mainDoc = { + name: 'form integration test', + brandId: _brand._id, + formId: _form._id, + }; + + const formData = { + loadType: FORM_LOAD_TYPES.EMBEDDED, + }; + + const integration = await Integrations.createFormIntegration({ ...mainDoc, formData }); + + expect(integration.formId).toEqual(_form._id); + expect(integration.name).toEqual(mainDoc.name); + expect(integration.brandId).toEqual(_brand._id); + expect(integration.formData.loadType).toEqual(FORM_LOAD_TYPES.EMBEDDED); + expect(integration.kind).toEqual(KIND_CHOICES.FORM); + }); +}); + +describe('edit form integration', () => { + let _brand; + let _brand2; + let _form; + let _form2; + let _user; + let _form_integration; + + beforeEach(async () => { + _brand = await brandFactory({}); + _brand2 = await brandFactory({}); + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + _form2 = await formFactory({ createdUserId: _user._id }); + _form_integration = await integrationFactory({ + name: 'form integration test', + brandId: _brand._id, + formId: _form._id, + kind: KIND_CHOICES.FORM, + formData: { + loadType: FORM_LOAD_TYPES.EMBEDDED, + }, + }); + }); + + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + await Users.remove({}); + await Forms.remove({}); + }); + + test('test if integration form update method is running successfully', async () => { + const mainDoc = { + name: 'form integration test 2', + brandId: _brand2._id, + formId: _form2._id, + }; + + const formData = { + loadType: FORM_LOAD_TYPES.SHOUTBOX, + }; + + await Integrations.updateFormIntegration(_form_integration._id, { + ...mainDoc, + formData, + }); + + const integration = await Integrations.findOne({ _id: _form_integration._id }); + + expect(integration.name).toEqual(mainDoc.name); + expect(integration.formId).toEqual(_form2._id); + expect(integration.brandId).toEqual(_brand2._id); + expect(integration.formData.loadType).toEqual(FORM_LOAD_TYPES.SHOUTBOX); + }); +}); + +describe('remove integration model method test', () => { + let _brand; + let _integration; + + beforeEach(async () => { + _brand = await brandFactory({}); + _integration = await integrationFactory({ + name: 'form integration test', + brandId: _brand._id, + kind: 'form', + }); + }); + + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + await Users.remove({}); + }); + + test('test if remove form integration model method is working successfully', async () => { + await Integrations.removeIntegration({ _id: _integration._id }); + + const integrationCount = await Integrations.find({}).count(); + + expect(integrationCount).toEqual(0); + }); +}); + +describe('save integration messenger appearance test', () => { + let _brand; + let _integration; + + beforeEach(async () => { + _brand = await brandFactory({}); + _integration = await integrationFactory({ + name: 'messenger integration test', + brandId: _brand._id, + kind: 'messenger', + }); + }); + + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + }); + + test('test if save integration messenger appearance method is working successfully', async () => { + const uiOptions = { + color: faker.random.word(), + wallpaper: faker.random.word(), + logo: faker.random.word(), + }; + + await Integrations.saveMessengerAppearanceData(_integration._id, uiOptions); + + const integration = await Integrations.findOne({ _id: _integration._id }); + + expect(integration.uiOptions.color).toEqual(uiOptions.color); + expect(integration.uiOptions.wallpaper).toEqual(uiOptions.wallpaper); + expect(integration.uiOptions.logo).toEqual(uiOptions.logo); + }); +}); + +describe('save integration messenger configurations test', () => { + let _brand; + let _integration; + + beforeEach(async () => { + _brand = await brandFactory({}); + _integration = await integrationFactory({ + name: 'messenger integration test', + brandId: _brand._id, + kind: KIND_CHOICES.MESSENGER, + }); + }); + + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + }); + + test(`test if messenger integration save confiturations + method is working correctly`, async () => { + const messengerData = { + notifyCustomer: true, + availabilityMethod: MESSENGER_DATA_AVAILABILITY.MANUAL, + isOnline: false, + onlineHours: [ + { + day: 'Monday', + from: '8am', + to: '12pm', + }, + { + day: 'Monday', + from: '2pm', + to: '6pm', + }, + ], + timezone: 'CET', + welcomeMessage: 'Welcome user', + awayMessage: 'Bye bye', + thankYouMessage: 'Thank you', + }; + + await Integrations.saveMessengerConfigs(_integration._id, messengerData); + + const integration = await Integrations.findOne({ _id: _integration._id }); + + expect(integration.messengerData.notifyCustomer).toEqual(messengerData.notifyCustomer); + expect(integration.messengerData.availabilityMethod).toEqual(messengerData.availabilityMethod); + expect(integration.messengerData.isOnline).toEqual(messengerData.isOnline); + expect(integration.messengerData.onlineHours[0].day).toEqual(messengerData.onlineHours[0].day); + expect(integration.messengerData.onlineHours[0].from).toEqual( + messengerData.onlineHours[0].from, + ); + expect(integration.messengerData.onlineHours[0].to).toEqual(messengerData.onlineHours[0].to); + expect(integration.messengerData.onlineHours[1].day).toEqual(messengerData.onlineHours[1].day); + expect(integration.messengerData.onlineHours[1].from).toEqual( + messengerData.onlineHours[1].from, + ); + expect(integration.messengerData.onlineHours[1].to).toEqual(messengerData.onlineHours[1].to); + expect(integration.messengerData.timezone).toEqual(messengerData.timezone); + expect(integration.messengerData.welcomeMessage).toEqual(messengerData.welcomeMessage); + expect(integration.messengerData.awayMessage).toEqual(messengerData.awayMessage); + expect(integration.messengerData.thankYouMessage).toEqual(messengerData.thankYouMessage); + + const newMessengerData = { + notifyCustomer: false, + availabilityMethod: MESSENGER_DATA_AVAILABILITY.AUTO, + isOnline: true, + onlineHours: [ + { + day: 'Tuesday', + from: '9am', + to: '1pm', + }, + { + day: 'Tuesday', + from: '3pm', + to: '7pm', + }, + ], + timezone: 'EET', + welcomeMessage: 'Welcome customer', + awayMessage: 'Good bye', + thankYouMessage: 'Gracias', + }; + + await Integrations.saveMessengerConfigs(_integration._id, newMessengerData); + + const updatedIntegration = await Integrations.findOne({ _id: _integration._id }); + + expect(updatedIntegration.messengerData.notifyCustomer).toEqual( + newMessengerData.notifyCustomer, + ); + expect(updatedIntegration.messengerData.availabilityMethod).toEqual( + newMessengerData.availabilityMethod, + ); + expect(updatedIntegration.messengerData.isOnline).toEqual(newMessengerData.isOnline); + expect(updatedIntegration.messengerData.onlineHours[0].day).toEqual( + newMessengerData.onlineHours[0].day, + ); + expect(updatedIntegration.messengerData.onlineHours[0].from).toEqual( + newMessengerData.onlineHours[0].from, + ); + expect(updatedIntegration.messengerData.onlineHours[0].to).toEqual( + newMessengerData.onlineHours[0].to, + ); + expect(updatedIntegration.messengerData.onlineHours[1].day).toEqual( + newMessengerData.onlineHours[1].day, + ); + expect(updatedIntegration.messengerData.onlineHours[1].from).toEqual( + newMessengerData.onlineHours[1].from, + ); + expect(updatedIntegration.messengerData.onlineHours[1].to).toEqual( + newMessengerData.onlineHours[1].to, + ); + expect(updatedIntegration.messengerData.timezone).toEqual(newMessengerData.timezone); + expect(updatedIntegration.messengerData.welcomeMessage).toEqual( + newMessengerData.welcomeMessage, + ); + expect(updatedIntegration.messengerData.awayMessage).toEqual(newMessengerData.awayMessage); + expect(updatedIntegration.messengerData.thankYouMessage).toEqual( + newMessengerData.thankYouMessage, + ); + }); +}); diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js index b76a429b5..e72bab29a 100644 --- a/src/__tests__/integrationMutations.test.js +++ b/src/__tests__/integrationMutations.test.js @@ -1,443 +1,91 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ + import faker from 'faker'; import { connect, disconnect } from '../db/connection'; -import { KIND_CHOICES, FORM_LOAD_TYPES, MESSENGER_DATA_AVAILABILITY } from '../data/constants'; -import { brandFactory, integrationFactory, formFactory, userFactory } from '../db/factories'; -import { Integrations, Brands, Users, Forms } from '../db/models'; -import mutations from '../data/resolvers/mutations'; +import { FORM_LOAD_TYPES, MESSENGER_DATA_AVAILABILITY } from '../data/constants'; +import { userFactory } from '../db/factories'; +import { Integrations, Users } from '../db/models'; +import IntegrationMutations from '../data/resolvers/mutations/integrations'; beforeAll(() => connect()); afterAll(() => disconnect()); -describe('messenger integration model add method test', () => { - let _brand; - - beforeEach(async () => { - _brand = await brandFactory({}); - }); - - afterEach(async () => { - await Brands.remove({}); - await Integrations.remove({}); - }); - - test('check if messenger integration create method is running successfully', async () => { - const doc = { - name: 'Integration test', - brandId: _brand._id, - }; - - const integration = await Integrations.createMessengerIntegration(doc); - - expect(integration.name).toBe(doc.name); - expect(integration.brandId).toBe(doc.brandId); - expect(integration.kind).toBe(KIND_CHOICES.MESSENGER); - }); -}); - -describe('messenger integration model edit test', () => { - let _brand; - let _integration; - let _brand2; - - beforeEach(async () => { - _brand = await brandFactory({}); - _brand2 = await brandFactory({}); - _integration = await integrationFactory({ - kind: KIND_CHOICES.MESSENGER, - brandId: _brand._id, - }); - }); - - afterEach(async () => { - await Brands.remove({}); - await Integrations.remove({}); - }); - - test('check if messenger integration update method is running successfully', async () => { - const doc = { - name: 'Integration test 2', - brandId: _brand2._id, - kind: 'new kind', - }; - - await Integrations.updateMessengerIntegration(_integration._id, doc); - - const updatedIntegration = await Integrations.findOne({ _id: _integration._id }); - - expect(updatedIntegration.name).toBe(doc.name); - expect(updatedIntegration.brandId).toBe(doc.brandId); - expect(updatedIntegration.kind).toBe(KIND_CHOICES.MESSENGER); - }); -}); - -describe('form integration create model test without formData', () => { - let _brand; - let _form; - let _user; - - beforeEach(async () => { - _brand = await brandFactory({}); - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - }); - - afterEach(async () => { - await Brands.remove({}); - await Integrations.remove({}); - await Users.remove({}); - await Forms.remove({}); - }); - - test('check if create form integration test wihtout formData is throwing exception', async () => { - expect.assertions(1); - - const mainDoc = { - name: 'form integration test', - brandId: _brand._id, - formId: _form._id, - }; - - try { - await Integrations.createFormIntegration(mainDoc); - } catch (e) { - expect(e.message).toEqual('formData must be supplied'); - } - }); -}); - -describe('create form integration test', () => { - let _brand; - let _form; +describe('mutations', () => { + const _fakeBrandId = 'fakeBrandId'; + const _fakeFormId = 'fakeFormId'; + const _fakeIntegrationId = '_fakeIntegrationId'; let _user; beforeEach(async () => { - _brand = await brandFactory({}); _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - }); - - afterEach(async () => { - await Brands.remove({}); - await Integrations.remove({}); - await Users.remove({}); - await Forms.remove({}); - }); - - test('test if create form integration is working successfully', async () => { - const mainDoc = { - name: 'form integration test', - brandId: _brand._id, - formId: _form._id, - }; - - const formData = { - loadType: FORM_LOAD_TYPES.EMBEDDED, - }; - - const integration = await Integrations.createFormIntegration({ ...mainDoc, formData }); - - expect(integration.formId).toEqual(_form._id); - expect(integration.name).toEqual(mainDoc.name); - expect(integration.brandId).toEqual(_brand._id); - expect(integration.formData.loadType).toEqual(FORM_LOAD_TYPES.EMBEDDED); - expect(integration.kind).toEqual(KIND_CHOICES.FORM); - }); -}); - -describe('edit form integration test', () => { - let _brand; - let _brand2; - let _form; - let _form2; - let _user; - let _form_integration; - - beforeEach(async () => { - _brand = await brandFactory({}); - _brand2 = await brandFactory({}); - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - _form2 = await formFactory({ createdUserId: _user._id }); - _form_integration = await integrationFactory({ - name: 'form integration test', - brandId: _brand._id, - formId: _form._id, - kind: KIND_CHOICES.FORM, - formData: { - loadType: FORM_LOAD_TYPES.EMBEDDED, - }, - }); - }); - - afterEach(async () => { - await Brands.remove({}); - await Integrations.remove({}); - await Users.remove({}); - await Forms.remove({}); - }); - - test('test if integration form update method is running successfully', async () => { - const mainDoc = { - name: 'form integration test 2', - brandId: _brand2._id, - formId: _form2._id, - }; - - const formData = { - loadType: FORM_LOAD_TYPES.SHOUTBOX, - }; - - await Integrations.updateFormIntegration(_form_integration._id, { - ...mainDoc, - formData, - }); - - const integration = await Integrations.findOne({ _id: _form_integration._id }); - - expect(integration.name).toEqual(mainDoc.name); - expect(integration.formId).toEqual(_form2._id); - expect(integration.brandId).toEqual(_brand2._id); - expect(integration.formData.loadType).toEqual(FORM_LOAD_TYPES.SHOUTBOX); - }); -}); - -describe('remove integration model method test', () => { - let _brand; - let _integration; - - beforeEach(async () => { - _brand = await brandFactory({}); - _integration = await integrationFactory({ - name: 'form integration test', - brandId: _brand._id, - kind: 'form', - }); }); afterEach(async () => { - await Brands.remove({}); - await Integrations.remove({}); await Users.remove({}); }); - test('test if remove form integration model method is working successfully', async () => { - await Integrations.removeIntegration({ _id: _integration._id }); + test('test if `logging required` error is working as intended', () => { + expect.assertions(6); - const integrationCount = await Integrations.find({}).count(); + // Login required ================== + expect(() => + IntegrationMutations.integrationsCreateMessengerIntegration(null, {}, {}), + ).toThrowError('Login required'); - expect(integrationCount).toEqual(0); - }); -}); - -describe('save integration messenger appearance test', () => { - let _brand; - let _integration; - - beforeEach(async () => { - _brand = await brandFactory({}); - _integration = await integrationFactory({ - name: 'messenger integration test', - brandId: _brand._id, - kind: 'messenger', - }); - }); - - afterEach(async () => { - await Brands.remove({}); - await Integrations.remove({}); - }); - - test('test if save integration messenger appearance method is working successfully', async () => { - const uiOptions = { - color: faker.random.word(), - wallpaper: faker.random.word(), - logo: faker.random.word(), - }; - - await Integrations.saveMessengerAppearanceData(_integration._id, uiOptions); - - const integration = await Integrations.findOne({ _id: _integration._id }); - - expect(integration.uiOptions.color).toEqual(uiOptions.color); - expect(integration.uiOptions.wallpaper).toEqual(uiOptions.wallpaper); - expect(integration.uiOptions.logo).toEqual(uiOptions.logo); - }); -}); - -describe('save integration messenger configurations test', () => { - let _brand; - let _integration; - - beforeEach(async () => { - _brand = await brandFactory({}); - _integration = await integrationFactory({ - name: 'messenger integration test', - brandId: _brand._id, - kind: KIND_CHOICES.MESSENGER, - }); - }); - - afterEach(async () => { - await Brands.remove({}); - await Integrations.remove({}); - }); - - test(`test if messenger integration save confiturations - method is working correctly`, async () => { - const messengerData = { - notifyCustomer: true, - availabilityMethod: MESSENGER_DATA_AVAILABILITY.MANUAL, - isOnline: false, - onlineHours: [ - { - day: 'Monday', - from: '8am', - to: '12pm', - }, - { - day: 'Monday', - from: '2pm', - to: '6pm', - }, - ], - timezone: 'CET', - welcomeMessage: 'Welcome user', - awayMessage: 'Bye bye', - thankYouMessage: 'Thank you', - }; - - await Integrations.saveMessengerConfigs(_integration._id, messengerData); + expect(() => + IntegrationMutations.integrationsEditMessengerIntegration(null, {}, {}), + ).toThrowError('Login required'); - const integration = await Integrations.findOne({ _id: _integration._id }); + expect(() => + IntegrationMutations.integrationsSaveMessengerAppearanceData(null, {}, {}), + ).toThrowError('Login required'); - expect(integration.messengerData.notifyCustomer).toEqual(messengerData.notifyCustomer); - expect(integration.messengerData.availabilityMethod).toEqual(messengerData.availabilityMethod); - expect(integration.messengerData.isOnline).toEqual(messengerData.isOnline); - expect(integration.messengerData.onlineHours[0].day).toEqual(messengerData.onlineHours[0].day); - expect(integration.messengerData.onlineHours[0].from).toEqual( - messengerData.onlineHours[0].from, + expect(() => IntegrationMutations.integrationsCreateFormIntegration(null, {}, {})).toThrowError( + 'Login required', ); - expect(integration.messengerData.onlineHours[0].to).toEqual(messengerData.onlineHours[0].to); - expect(integration.messengerData.onlineHours[1].day).toEqual(messengerData.onlineHours[1].day); - expect(integration.messengerData.onlineHours[1].from).toEqual( - messengerData.onlineHours[1].from, - ); - expect(integration.messengerData.onlineHours[1].to).toEqual(messengerData.onlineHours[1].to); - expect(integration.messengerData.timezone).toEqual(messengerData.timezone); - expect(integration.messengerData.welcomeMessage).toEqual(messengerData.welcomeMessage); - expect(integration.messengerData.awayMessage).toEqual(messengerData.awayMessage); - expect(integration.messengerData.thankYouMessage).toEqual(messengerData.thankYouMessage); - - const newMessengerData = { - notifyCustomer: false, - availabilityMethod: MESSENGER_DATA_AVAILABILITY.AUTO, - isOnline: true, - onlineHours: [ - { - day: 'Tuesday', - from: '9am', - to: '1pm', - }, - { - day: 'Tuesday', - from: '3pm', - to: '7pm', - }, - ], - timezone: 'EET', - welcomeMessage: 'Welcome customer', - awayMessage: 'Good bye', - thankYouMessage: 'Gracias', - }; - - await Integrations.saveMessengerConfigs(_integration._id, newMessengerData); - const updatedIntegration = await Integrations.findOne({ _id: _integration._id }); - - expect(updatedIntegration.messengerData.notifyCustomer).toEqual( - newMessengerData.notifyCustomer, - ); - expect(updatedIntegration.messengerData.availabilityMethod).toEqual( - newMessengerData.availabilityMethod, - ); - expect(updatedIntegration.messengerData.isOnline).toEqual(newMessengerData.isOnline); - expect(updatedIntegration.messengerData.onlineHours[0].day).toEqual( - newMessengerData.onlineHours[0].day, + expect(() => IntegrationMutations.integrationsEditFormIntegration(null, {}, {})).toThrowError( + 'Login required', ); - expect(updatedIntegration.messengerData.onlineHours[0].from).toEqual( - newMessengerData.onlineHours[0].from, - ); - expect(updatedIntegration.messengerData.onlineHours[0].to).toEqual( - newMessengerData.onlineHours[0].to, - ); - expect(updatedIntegration.messengerData.onlineHours[1].day).toEqual( - newMessengerData.onlineHours[1].day, - ); - expect(updatedIntegration.messengerData.onlineHours[1].from).toEqual( - newMessengerData.onlineHours[1].from, - ); - expect(updatedIntegration.messengerData.onlineHours[1].to).toEqual( - newMessengerData.onlineHours[1].to, - ); - expect(updatedIntegration.messengerData.timezone).toEqual(newMessengerData.timezone); - expect(updatedIntegration.messengerData.welcomeMessage).toEqual( - newMessengerData.welcomeMessage, - ); - expect(updatedIntegration.messengerData.awayMessage).toEqual(newMessengerData.awayMessage); - expect(updatedIntegration.messengerData.thankYouMessage).toEqual( - newMessengerData.thankYouMessage, - ); - }); -}); -describe('mutation tests', () => { - let _user; - - beforeEach(async () => { - _user = await userFactory({}); - }); - - afterEach(async () => { - await Users.remove({}); + expect(() => IntegrationMutations.integrationsRemove(null, {}, {})).toThrowError( + 'Login required', + ); }); - test('mutation test', async () => { - // test Integrations.createMessengerIntegration ========== - const fakeBrandId = 'fakeBrandId'; - const fakeIntegrationId = 'fakeIntegrationid'; - const fakeFormId = 'fakeFormId'; - - let doc = { + test('test Integrations.createMessengerIntegration', async () => { + const doc = { name: 'Integration test', - brandId: fakeBrandId, + brandId: _fakeBrandId, }; Integrations.createMessengerIntegration = jest.fn(); - await mutations.integrationsCreateMessengerIntegration(null, doc, { user: _user }); + await IntegrationMutations.integrationsCreateMessengerIntegration(null, doc, { user: _user }); expect(Integrations.createMessengerIntegration).toBeCalledWith(doc); expect(Integrations.createMessengerIntegration.mock.calls.length).toBe(1); + }); - // test Integrations.updateMessengerIntegration ========================= - doc = { - _id: fakeIntegrationId, + test('test Integrations.updateMessengerIntegration', async () => { + const doc = { + _id: _fakeIntegrationId, name: 'Integration test 2', - brandId: fakeBrandId, + brandId: _fakeBrandId, }; Integrations.updateMessengerIntegration = jest.fn(); - await mutations.integrationsEditMessengerIntegration(null, doc, { user: _user }); + await IntegrationMutations.integrationsEditMessengerIntegration(null, doc, { user: _user }); delete doc._id; - expect(Integrations.updateMessengerIntegration).toBeCalledWith(fakeIntegrationId, doc); + expect(Integrations.updateMessengerIntegration).toBeCalledWith(_fakeIntegrationId, doc); expect(Integrations.updateMessengerIntegration.mock.calls.length).toBe(1); + }); - // test Integrations.saveMessengerConfigs ======================= + test('test Integrations.saveMessengerConfigs', async () => { const uiOptions = { color: faker.random.word(), wallpaper: faker.random.word(), @@ -446,19 +94,20 @@ describe('mutation tests', () => { Integrations.saveMessengerAppearanceData = jest.fn(); - await mutations.integrationsSaveMessengerAppearanceData( + await IntegrationMutations.integrationsSaveMessengerAppearanceData( null, { - _id: fakeIntegrationId, + _id: _fakeIntegrationId, uiOptions, }, { user: _user }, ); - expect(Integrations.saveMessengerAppearanceData).toBeCalledWith(fakeIntegrationId, uiOptions); + expect(Integrations.saveMessengerAppearanceData).toBeCalledWith(_fakeIntegrationId, uiOptions); expect(Integrations.saveMessengerAppearanceData.mock.calls.length).toBe(1); + }); - // test Integrations.saveMessengerConfigs =================== + test('test Integrations.saveMessengerConfigs', async () => { const messengerData = { notifyCustomer: true, availabilityMethod: MESSENGER_DATA_AVAILABILITY.AUTO, @@ -483,73 +132,80 @@ describe('mutation tests', () => { Integrations.saveMessengerConfigs = jest.fn(); - await mutations.integrationsSaveMessengerConfigs( + await IntegrationMutations.integrationsSaveMessengerConfigs( null, { - _id: fakeIntegrationId, + _id: _fakeIntegrationId, messengerData, }, { user: _user }, ); - expect(Integrations.saveMessengerConfigs).toBeCalledWith(fakeIntegrationId, messengerData); + expect(Integrations.saveMessengerConfigs).toBeCalledWith(_fakeIntegrationId, messengerData); expect(Integrations.saveMessengerConfigs.mock.calls.length).toBe(1); + }); - // test Integrations.createFormIntegration ======================= - let mainDoc = { + test('test Integrations.createFormIntegration', async () => { + const mainDoc = { name: 'form integration test', - brandId: fakeBrandId, - formId: fakeFormId, + brandId: _fakeBrandId, + formId: _fakeFormId, }; - let formData = { + const formData = { loadType: FORM_LOAD_TYPES.EMBEDDED, }; Integrations.createFormIntegration = jest.fn(); - doc = { + const doc = { ...mainDoc, formData, }; - await mutations.integrationsCreateFormIntegration(null, doc, { user: _user }); + await IntegrationMutations.integrationsCreateFormIntegration(null, doc, { user: _user }); expect(Integrations.createFormIntegration).toBeCalledWith(doc); expect(Integrations.createFormIntegration.mock.calls.length).toBe(1); + }); - // test Integrations.updateFormIntegration ===================== - mainDoc = { + test('test Integrations.updateFormIntegration', async () => { + const mainDoc = { name: 'form integration test 2', - brandId: fakeBrandId, - formId: fakeFormId, + brandId: _fakeBrandId, + formId: _fakeFormId, }; - formData = { + const formData = { loadType: FORM_LOAD_TYPES.SHOUTBOX, }; - doc = { - _id: fakeIntegrationId, + const doc = { + _id: _fakeIntegrationId, ...mainDoc, formData, }; Integrations.updateFormIntegration = jest.fn(); - await mutations.integrationsEditFormIntegration(null, doc, { user: _user }); + await IntegrationMutations.integrationsEditFormIntegration(null, doc, { user: _user }); delete doc._id; - expect(Integrations.updateFormIntegration).toBeCalledWith(fakeIntegrationId, doc); + expect(Integrations.updateFormIntegration).toBeCalledWith(_fakeIntegrationId, doc); expect(Integrations.updateFormIntegration.mock.calls.length).toBe(1); + }); - // test Integrations.removeIntegration =========================== + test('test Integrations.removeIntegration', async () => { Integrations.removeIntegration = jest.fn(); - await mutations.integrationsRemove(null, { _id: fakeIntegrationId }, { user: _user }); + await IntegrationMutations.integrationsRemove( + null, + { _id: _fakeIntegrationId }, + { user: _user }, + ); - expect(Integrations.removeIntegration).toBeCalledWith(fakeIntegrationId); + expect(Integrations.removeIntegration).toBeCalledWith(_fakeIntegrationId); expect(Integrations.removeIntegration.mock.calls.length).toBe(1); }); }); diff --git a/src/data/resolvers/mutations/integrations.js b/src/data/resolvers/mutations/integrations.js index eff9ec737..66d6253be 100644 --- a/src/data/resolvers/mutations/integrations.js +++ b/src/data/resolvers/mutations/integrations.js @@ -9,8 +9,8 @@ export default { * @param {string} doc.brandId - Integration brand id * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} return integration promise - * @throws {Error} throws error if user is not logged in + * @return {Promise} return Promise resolving Integration document + * @throws {Error} throws Error('Login required') if user is not logged in */ integrationsCreateMessengerIntegration(root, doc, { user }) { if (!user) { @@ -29,8 +29,8 @@ export default { * @param {string} object2.brandId - Integration brand id * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} returns null promise - * @throws {Error} throws error if user is not logged in + * @return {Promise} return Promise resolving Integration document + * @throws {Error} throws Error('Login required') if user is not logged in */ integrationsEditMessengerIntegration(root, { _id, ...fields }, { user }) { if (!user) { @@ -51,8 +51,8 @@ export default { * @param {string} object2.logo - MessengerUiOptions logo * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} returns null promise - * @throws {Error} throws error if user is not logged in + * @return {Promise} return Promise resolving Integration document + * @throws {Error} throws Error('Login required') if user is not logged in */ integrationsSaveMessengerAppearanceData(root, { _id, uiOptions }, { user }) { if (!user) { @@ -71,8 +71,8 @@ export default { * object related to this integration * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} returns null promise - * @throws {Error} throws error if user is not logged in + * @return {Promise} return Promise resolving Integration document + * @throws {Error} throws Error('Login required') if user is not logged in */ integrationsSaveMessengerConfigs(root, { _id, messengerData }, { user }) { if (!user) { @@ -92,8 +92,8 @@ export default { * @param {FormData} doc.formData - Integration form data sumbdocument object * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} returns the messenger integration - * @throws {Error} throws error if user is not logged in + * @return {Promise} return Promise resolving Integration document + * @throws {Error} throws Error('Login required') if user is not logged in */ integrationsCreateFormIntegration(root, doc, { user }) { if (!user) { @@ -114,8 +114,8 @@ export default { * @param {FormData} doc.formData - Integration form data subdocument object * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} returns null promise - * @throws {Error} throws error if user is not logged in + * @return {Promise} return Promise resolving Integration document + * @throws {Error} throws Error('Login required') if user is not logged in */ integrationsEditFormIntegration(root, { _id, ...doc }, { user }) { if (!user) { @@ -132,9 +132,8 @@ export default { * @param {string} object2._id - Integration id * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} returns null - * @throws {Error} apollo level error based on validation - * @throws {Error} throws error if user is not logged in + * @return {Promise} + * @throws {Error} throws Error('Login required') if user is not logged in */ integrationsRemove(root, { _id }, { user }) { if (!user) { diff --git a/src/data/resolvers/mutations/notifications.js b/src/data/resolvers/mutations/notifications.js index cf7c9be73..b3781cbe2 100644 --- a/src/data/resolvers/mutations/notifications.js +++ b/src/data/resolvers/mutations/notifications.js @@ -9,8 +9,8 @@ export default { * @param {Boolean} object2.isAllowed - Shows whether notifications will be received or not * @param {Object} object3 - Middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} returns Promise resolving a Notification document - * @throws {Error} throws error if user is not logged in + * @return {Promise} return Promise resolving a Notification document + * @throws {Error} throws Error('Login required') if user is not logged in */ notificationsSaveConfig(root, doc, { user }) { if (!user) { @@ -28,7 +28,7 @@ export default { * @param {Object} object3 - Middleware data * @param {Object} object3.user - The user making this action * @return {Promise} - * @throws {Error} throws error if user is not logged in + * @throws {Error} throws Error('Login required') if user is not logged in */ notificationsMarkAsRead(root, { ids }, { user }) { if (!user) { From 24a4fbeaf3b36d2d753d71ee450d5397aa5b1ae6 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 13 Oct 2017 14:20:25 +0800 Subject: [PATCH 053/318] #13 Refactor forms and form fields --- src/__tests__/formDb.test.js | 360 ++++++++++++++++++++++ src/__tests__/formMutations.test.js | 425 +++----------------------- src/data/resolvers/mutations/forms.js | 32 +- src/db/models/Forms.js | 19 +- 4 files changed, 423 insertions(+), 413 deletions(-) create mode 100644 src/__tests__/formDb.test.js diff --git a/src/__tests__/formDb.test.js b/src/__tests__/formDb.test.js new file mode 100644 index 000000000..6ed119235 --- /dev/null +++ b/src/__tests__/formDb.test.js @@ -0,0 +1,360 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { userFactory, formFactory, formFieldFactory, integrationFactory } from '../db/factories'; +import { Forms, Users, FormFields, Integrations } from '../db/models'; +import toBeType from 'jest-tobetype'; + +expect.extend(toBeType); + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('form creation', () => { + let _user; + + beforeEach(async () => { + _user = await userFactory({}); + }); + + afterEach(async () => { + await Users.remove({}); + await Forms.remove({}); + }); + + test(`testing if Error('createdUser must be supplied') is throwing as intended`, async () => { + expect.assertions(1); + + try { + await Forms.createForm({ + title: 'Test form', + description: 'Test form description', + }); + } catch (e) { + expect(e.message).toEqual('createdUser must be supplied'); + } + }); + + test('check if form creation method is working successfully', async () => { + let form = await Forms.createForm( + { + title: 'Test form', + description: 'Test form description', + }, + _user._id, + ); + + form = await Forms.findOne({ _id: form._id }); + + expect(form.title).toBe('Test form'); + expect(form.description).toBe('Test form description'); + expect(form.code).toBeType('string'); + expect(form.code.length).toBe(6); + // typeof form.createdDate is 'object' even though its Date + expect(form.createdDate).toBeType('object'); + expect(form.createdUserId).toBe(_user._id); + }); +}); + +describe('form update', () => { + let _user; + let _form; + + beforeEach(async () => { + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user }); + }); + + afterEach(async () => { + await Users.remove({}); + await Forms.remove({}); + }); + + test('check if form update method is working successfully', async () => { + const doc = { + title: 'Test form 2', + description: 'Test form description 2', + }; + + const formAfterUpdate = await Forms.updateForm(_form._id, doc); + + expect(formAfterUpdate.title).toBe(doc.title); + expect(formAfterUpdate.description).toBe(doc.description); + expect(formAfterUpdate.createdUserId).toBe(_form.createdUserId); + expect(formAfterUpdate.code).toBe(_form.code); + expect(_form.createdDate).toBeType('object'); + }); +}); + +describe('form remove', async () => { + let _form; + + beforeEach(async () => { + _form = await formFactory({}); + }); + + afterEach(async () => { + await Forms.remove({}); + }); + + test('check if form removal is working successfully', async () => { + await Forms.removeForm(_form._id); + + const formCount = await Forms.find({}).count(); + + expect(formCount).toBe(0); + }); +}); + +describe('test exception in remove form method', async () => { + let _user; + let _form; + + beforeEach(async () => { + _user = await userFactory({}); + + _form = await formFactory({ + title: 'Test form', + description: 'Test form description', + createdUserId: _user._id, + }); + }); + + afterEach(async () => { + await Users.remove({}); + await Forms.remove({}); + await FormFields.remove({}); + await Integrations.remove({}); + }); + + test('check if errors are being thrown as intended', async () => { + expect.assertions(2); + + await formFieldFactory(_form._id, { + type: 'input', + validation: 'number', + text: 'form field text', + description: 'form field description', + }); + + try { + await Forms.removeForm(_form._id); + } catch (e) { + expect(e.message).toEqual('You cannot delete this form. This form has some fields.'); + } + + await FormFields.remove({}); + + await integrationFactory({ + formId: _form._id, + formData: { + loadType: 'shoutbox', + fromEmail: 'test@erxes.io', + }, + }); + + try { + await Forms.removeForm(_form._id); + } catch (e) { + expect(e.message).toEqual('You cannot delete this form. This form used in integration.'); + } + }); +}); + +describe('add form field', async () => { + let _user; + let _form; + + beforeEach(async () => { + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + }); + + afterEach(async () => { + await Users.remove({}); + await FormFields.remove({}); + await Forms.remove({}); + }); + + test('check whether form fields are being created successfully', async () => { + const doc = { + type: 'input', + validation: 'number', + text: 'How old are you?', + description: 'Form field description', + options: ['This', 'should', 'not', 'be', 'here', 'tho'], + isRequired: false, + }; + + const newFormField = await FormFields.createFormField(_form._id, doc); + + expect(newFormField.formId).toEqual(_form._id); + expect(newFormField.order).toEqual(0); + expect(newFormField.type).toEqual(doc.type); + expect(newFormField.validation).toEqual(doc.validation); + expect(newFormField.text).toEqual(doc.text); + expect(newFormField.description).toEqual(doc.description); + expect(newFormField.options).toEqual(expect.arrayContaining(doc.options)); + expect(newFormField.isRequired).toEqual(doc.isRequired); + }); +}); + +describe('update form field test', async () => { + let _user; + let _form; + let _form_field; + + beforeEach(async () => { + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + _form_field = await formFieldFactory(_form._id, {}); + }); + + afterEach(async () => { + await Users.remove({}); + await FormFields.remove({}); + await Forms.remove({}); + }); + + test('check whether form fields are being updated successfully', async () => { + const doc = { + type: 'textarea', + validation: 'date', + text: 'How old are you? 1', + description: 'Form field description 1', + options: ['This', 'should', 'not', 'be', 'here', 'tho', '1'], + isRequired: true, + }; + + const updatedFormField = await FormFields.updateFormField(_form_field._id, doc); + + expect(updatedFormField.formId).toEqual(_form._id); + expect(updatedFormField.type).toEqual(doc.type); + expect(updatedFormField.validation).toEqual(doc.validation); + expect(updatedFormField.text).toEqual(doc.text); + expect(updatedFormField.description).toEqual(doc.description); + expect(updatedFormField.isRequired).toBe(doc.isRequired); + + for (let item of doc.options) { + expect(updatedFormField.options).toContain(item); + } + + expect(updatedFormField.options.length).toEqual(7); + }); +}); + +describe('remove form field', async () => { + let _user; + let _form; + let _form_field; + + beforeEach(async () => { + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + _form_field = await formFieldFactory(_form._id, {}); + }); + + afterEach(async () => { + await Users.remove({}); + await FormFields.remove({}); + await Forms.remove({}); + }); + + test('check whether form fields are being removed successfully', async () => { + await FormFields.removeFormField(_form_field._id); + + expect(await FormFields.find({}).count()).toEqual(0); + }); +}); + +describe('test of update order of form fields', async () => { + let _user; + let _form; + let _formField; + let _formField2; + let _formField3; + + /** + * Testing with an _user object and a _form object with 3 fields in it + * to test the setting the new order + */ + beforeEach(async () => { + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + _formField = await formFieldFactory(_form._id, {}); + _formField2 = await formFieldFactory(_form._id, {}); + _formField3 = await formFieldFactory(_form._id, {}); + }); + + /** + * Deleting the data that was used in test + */ + afterEach(async () => { + await Users.remove({}); + await FormFields.remove({}); + await Forms.remove({}); + }); + + test('check whether order values on form fields are being updated successfully', async () => { + expect(_formField.order).toBe(0); + expect(_formField2.order).toBe(1); + expect(_formField3.order).toBe(2); + + const orderDictArray = [ + { _id: _formField3._id, order: 10 }, + { _id: _formField2._id, order: 9 }, + { _id: _formField._id, order: 8 }, + ]; + + await Forms.updateFormFieldsOrder(orderDictArray); + const ff1 = await FormFields.findOne({ _id: _formField3._id }); + + expect(ff1.order).toBe(10); + expect(ff1.text).toBe(_formField3.text); + + const ff2 = await FormFields.findOne({ _id: _formField2._id }); + + expect(ff2.order).toBe(9); + expect(ff2.text).toBe(_formField2.text); + + const ff3 = await FormFields.findOne({ _id: _formField._id }); + expect(ff3.order).toBe(8); + expect(ff3.text).toBe(_formField.text); + }); +}); + +describe('form duplication', () => { + let _user; + let _form; + + beforeEach(async () => { + _user = await userFactory({}); + _form = await formFactory({ createdUserId: _user._id }); + await formFieldFactory(_form._id, {}); + await formFieldFactory(_form._id, {}); + await formFieldFactory(_form._id, {}); + }); + + afterEach(async () => { + await Users.remove({}); + await FormFields.remove({}); + await Forms.remove({}); + }); + + test('test whether form duplication method is working successfully', async () => { + const duplicatedForm = await Forms.duplicate(_form._id); + + expect(duplicatedForm.title).toBe(`${_form.title} duplicated`); + expect(duplicatedForm.description).toBe(_form.description); + expect(duplicatedForm.code).toBeType('string'); + expect(duplicatedForm.code.length).toEqual(6); + expect(duplicatedForm.createdUserId).toBe(_form.createdUserId); + + const formFieldsCount = await FormFields.find({}).count(); + const duplicateFormFieldsCount = await FormFields.find({ formId: duplicatedForm._id }).count(); + + expect(formFieldsCount).toEqual(6); + expect(duplicateFormFieldsCount).toEqual(3); + }); +}); diff --git a/src/__tests__/formMutations.test.js b/src/__tests__/formMutations.test.js index 4bc6e4c09..ad0346434 100644 --- a/src/__tests__/formMutations.test.js +++ b/src/__tests__/formMutations.test.js @@ -2,369 +2,16 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { userFactory, formFactory, formFieldFactory, integrationFactory } from '../db/factories'; -import { Forms, Users, FormFields, Integrations } from '../db/models'; -import mutations from '../data/resolvers/mutations'; -import toBeType from 'jest-tobetype'; - -expect.extend(toBeType); +import formMutations from '../data/resolvers/mutations/forms'; +import { userFactory } from '../db/factories'; +import { Forms, Users, FormFields } from '../db/models'; beforeAll(() => connect()); afterAll(() => disconnect()); -describe('form creation tests', () => { - let _user; - - beforeEach(async () => { - _user = await userFactory({}); - }); - - afterEach(async () => { - await Users.remove({}); - await Forms.remove({}); - }); - - test('testing if `createdUser must be supplied` error is working as intended', async () => { - expect.assertions(1); - - try { - await Forms.createForm({ - title: 'Test form', - description: 'Test form description', - }); - } catch (e) { - expect(e.message).toEqual('createdUser must be supplied'); - } - }); - - test('check if form creation method is working successfully', async () => { - let form = await Forms.createForm( - { - title: 'Test form', - description: 'Test form description', - }, - _user._id, - ); - - form = await Forms.findOne({ _id: form._id }); - - expect(form.title).toBe('Test form'); - expect(form.description).toBe('Test form description'); - expect(form.code).toBeType('string'); - expect(form.code.length).toBe(6); - // typeof form.createdDate is 'object' even though its Date - expect(form.createdDate).toBeType('object'); - expect(form.createdUserId).toBe(_user._id); - }); -}); - -describe('form update tests', () => { - let _user; - let _form; - - beforeEach(async () => { - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user }); - }); - - afterEach(async () => { - await Users.remove({}); - await Forms.remove({}); - }); - - test('check if form update method is working successfully', async () => { - const doc = { - title: 'Test form 2', - description: 'Test form description 2', - }; - - await Forms.updateForm(_form._id, doc); - - const formAfterUpdate = await Forms.findOne({ _id: _form._id }); - - expect(formAfterUpdate.title).toBe(doc.title); - expect(formAfterUpdate.description).toBe(doc.description); - expect(formAfterUpdate.createdUserId).toBe(_form.createdUserId); - expect(formAfterUpdate.code).toBe(_form.code); - expect(_form.createdDate).toBeType('object'); - }); -}); - -describe('form remove tests', async () => { - let _form; - - beforeEach(async () => { - _form = await formFactory({}); - }); - - afterEach(async () => { - await Forms.remove({}); - }); - - test('check if form removal is working successfully', async () => { - await Forms.removeForm(_form._id); - - const formCount = await Forms.find({}).count(); - - expect(formCount).toBe(0); - }); -}); - -describe('test exception in remove form method', async () => { - let _user; - let _form; - - beforeEach(async () => { - _user = await userFactory({}); - - _form = await formFactory({ - title: 'Test form', - description: 'Test form description', - createdUserId: _user._id, - }); - }); - - afterEach(async () => { - await Users.remove({}); - await Forms.remove({}); - await FormFields.remove({}); - await Integrations.remove({}); - }); - - test('check if errors are being thrown as intended', async () => { - expect.assertions(2); - - await formFieldFactory(_form._id, { - type: 'input', - validation: 'number', - text: 'form field text', - description: 'form field description', - }); - - try { - await Forms.removeForm(_form._id); - } catch (e) { - expect(e.message).toEqual('You cannot delete this form. This form has some fields.'); - } - - await FormFields.remove({}); - - await integrationFactory({ - formId: _form._id, - formData: { - loadType: 'shoutbox', - fromEmail: 'test@erxes.io', - }, - }); - - try { - await Forms.removeForm(_form._id); - } catch (e) { - expect(e.message).toEqual('You cannot delete this form. This form used in integration.'); - } - }); -}); - -describe('add form field test', async () => { - let _user; - let _form; - - beforeEach(async () => { - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - }); - - afterEach(async () => { - await Users.remove({}); - await FormFields.remove({}); - await Forms.remove({}); - }); - - test('check whether form fields are being created successfully', async () => { - const doc = { - type: 'input', - validation: 'number', - text: 'How old are you?', - description: 'Form field description', - options: ['This', 'should', 'not', 'be', 'here', 'tho'], - isRequired: false, - }; - - const newFormField = await FormFields.createFormField(_form._id, doc); - - expect(newFormField.formId).toEqual(_form._id); - expect(newFormField.order).toEqual(0); - expect(newFormField.type).toEqual(doc.type); - expect(newFormField.validation).toEqual(doc.validation); - expect(newFormField.text).toEqual(doc.text); - expect(newFormField.description).toEqual(doc.description); - expect(newFormField.options).toEqual(expect.arrayContaining(doc.options)); - expect(newFormField.isRequired).toEqual(doc.isRequired); - }); -}); - -describe('update form field test', async () => { - let _user; - let _form; - let _form_field; - - beforeEach(async () => { - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - _form_field = await formFieldFactory(_form._id, {}); - }); - - afterEach(async () => { - await Users.remove({}); - await FormFields.remove({}); - await Forms.remove({}); - }); - - test('check whether form fields are being updated successfully', async () => { - const doc = { - type: 'textarea', - validation: 'date', - text: 'How old are you? 1', - description: 'Form field description 1', - options: ['This', 'should', 'not', 'be', 'here', 'tho', '1'], - isRequired: true, - }; - - await FormFields.updateFormField(_form_field._id, doc); - - let updatedFormField = await FormFields.findOne({ _id: _form_field._id }); - - expect(updatedFormField.formId).toEqual(_form._id); - expect(updatedFormField.type).toEqual(doc.type); - expect(updatedFormField.validation).toEqual(doc.validation); - expect(updatedFormField.text).toEqual(doc.text); - expect(updatedFormField.description).toEqual(doc.description); - expect(updatedFormField.isRequired).toBe(doc.isRequired); - - for (let item of doc.options) { - expect(updatedFormField.options).toContain(item); - } - - expect(updatedFormField.options.length).toEqual(7); - }); -}); - -describe('remove form field test', async () => { - let _user; - let _form; - let _form_field; - - beforeEach(async () => { - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - _form_field = await formFieldFactory(_form._id, {}); - }); - - afterEach(async () => { - await Users.remove({}); - await FormFields.remove({}); - await Forms.remove({}); - }); - - test('check whether form fields are being removed successfully', async () => { - await FormFields.removeFormField(_form_field._id); - - expect(await FormFields.find({}).count()).toEqual(0); - }); -}); - -describe('test of update order of form fields', async () => { - let _user; - let _form; - let _formField; - let _formField2; - let _formField3; - - /** - * Testing with an _user object and a _form object with 3 fields in it - * to test the setting the new order - */ - beforeEach(async () => { - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - _formField = await formFieldFactory(_form._id, {}); - _formField2 = await formFieldFactory(_form._id, {}); - _formField3 = await formFieldFactory(_form._id, {}); - }); - - /** - * Deleting the data that was used in test - */ - afterEach(async () => { - await Users.remove({}); - await FormFields.remove({}); - await Forms.remove({}); - }); - - test('check whether order values on form fields are being updated successfully', async () => { - expect(_formField.order).toBe(0); - expect(_formField2.order).toBe(1); - expect(_formField3.order).toBe(2); - - const orderDictArray = [ - { _id: _formField3._id, order: 10 }, - { _id: _formField2._id, order: 9 }, - { _id: _formField._id, order: 8 }, - ]; - - await Forms.updateFormFieldsOrder(orderDictArray); - const ff1 = await FormFields.findOne({ _id: _formField3._id }); - - expect(ff1.order).toBe(10); - expect(ff1.text).toBe(_formField3.text); - - const ff2 = await FormFields.findOne({ _id: _formField2._id }); - - expect(ff2.order).toBe(9); - expect(ff2.text).toBe(_formField2.text); - - const ff3 = await FormFields.findOne({ _id: _formField._id }); - expect(ff3.order).toBe(8); - expect(ff3.text).toBe(_formField.text); - }); -}); - -describe('test of form duplication', () => { - let _user; - let _form; - - beforeEach(async () => { - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - await formFieldFactory(_form._id, {}); - await formFieldFactory(_form._id, {}); - await formFieldFactory(_form._id, {}); - }); - - afterEach(async () => { - await Users.remove({}); - await FormFields.remove({}); - await Forms.remove({}); - }); - - test('test whether form duplication method is working successfully', async () => { - const duplicatedForm = await Forms.duplicate(_form._id); - - expect(duplicatedForm.title).toBe(`${_form.title} duplicated`); - expect(duplicatedForm.description).toBe(_form.description); - expect(duplicatedForm.code).toBeType('string'); - expect(duplicatedForm.code.length).toEqual(6); - expect(duplicatedForm.createdUserId).toBe(_form.createdUserId); - - const formFieldsCount = await FormFields.find({}).count(); - const duplicateFormFieldsCount = await FormFields.find({ formId: duplicatedForm._id }).count(); - - expect(formFieldsCount).toEqual(6); - expect(duplicateFormFieldsCount).toEqual(3); - }); -}); - -describe('checking all form and formField mutations', () => { +describe('form and formField mutations', () => { + const _formId = 'formId'; + const _formFieldId = 'formFieldId'; let _user; beforeEach(async () => { @@ -375,40 +22,41 @@ describe('checking all form and formField mutations', () => { await Users.remove({}); }); - test(`testing all methods of form mutations`, async () => { + test(`test mutations.formsCreate`, async () => { Forms.createForm = jest.fn(); - let doc = { + const doc = { title: 'Test form', description: 'Test form description', }; - // test mutations.formsCreate ============ - await mutations.formsCreate(null, doc, { user: _user }); + await formMutations.formsCreate(null, doc, { user: _user }); expect(Forms.createForm).toBeCalledWith(doc, _user); expect(Forms.createForm.mock.calls.length).toBe(1); + }); - // mutations.formsUpdate ================ - doc = { - _id: 'test id', + test('test mutations.formUpdate', async () => { + const doc = { + _id: _formId, title: 'Test form 2', description: 'Test form description 2', }; Forms.updateForm = jest.fn(); - await mutations.formsEdit(null, doc, { user: _user }); + await formMutations.formsEdit(null, doc, { user: _user }); - const formId = doc._id; + const formId = _formId; delete doc._id; expect(Forms.updateForm).toBeCalledWith(formId, doc); expect(Forms.updateForm.mock.calls.length).toBe(1); + }); - // test mutations.formsAddFormField ================ - doc = { - formId, + test('test mutations.formsAddFormField', async () => { + const doc = { + formId: _formId, type: 'input', validation: 'number', text: 'How old are you?', @@ -419,16 +67,17 @@ describe('checking all form and formField mutations', () => { FormFields.createFormField = jest.fn(); - await mutations.formsAddFormField(null, doc, { user: _user }); + await formMutations.formsAddFormField(null, doc, { user: _user }); delete doc.formId; - expect(FormFields.createFormField).toBeCalledWith(formId, doc); + expect(FormFields.createFormField).toBeCalledWith(_formId, doc); expect(FormFields.createFormField.mock.calls.length).toBe(1); + }); - // test mutations.formsEditFormField =============== - doc = { - _id: 'test form field id', + test('test mutations.formsEditFormField', async () => { + const doc = { + _id: _formFieldId, type: 'mutation input 1', validation: 'mutation number 1', text: 'mutation - How old are you? 1', @@ -439,32 +88,32 @@ describe('checking all form and formField mutations', () => { FormFields.updateFormField = jest.fn(); - await mutations.formsEditFormField(null, doc, { user: _user }); + await formMutations.formsEditFormField(null, doc, { user: _user }); - const formFieldId = doc._id; delete doc._id; - expect(FormFields.updateFormField).toBeCalledWith(formFieldId, doc); + expect(FormFields.updateFormField).toBeCalledWith(_formFieldId, doc); expect(FormFields.updateFormField.mock.calls.length).toBe(1); + }); - // test formsRemoveFormField ================= + test('test mutations.formsRemoveFormField', async () => { FormFields.removeFormField = jest.fn(); - await mutations.formsRemoveFormField(null, { _id: formFieldId }, { user: _user }); + await formMutations.formsRemoveFormField(null, { _id: _formFieldId }, { user: _user }); - expect(FormFields.removeFormField).toBeCalledWith(formFieldId); + expect(FormFields.removeFormField).toBeCalledWith(_formFieldId); expect(FormFields.removeFormField.mock.calls.length).toBe(1); // test mutations.formsRemove =========== Forms.removeForm = jest.fn(); - await mutations.formsRemove(null, { _id: formId }, { user: _user }); + await formMutations.formsRemove(null, { _id: _formId }, { user: _user }); - expect(Forms.removeForm).toBeCalledWith(formId); + expect(Forms.removeForm).toBeCalledWith(_formId); expect(Forms.removeForm.mock.calls.length).toBe(1); }); - test('check whether order value updating mutation is working successfully', async () => { + test('test mutations.formsUpdateFormFieldsOrder', async () => { const doc = { orderDics: [ { @@ -484,18 +133,18 @@ describe('checking all form and formField mutations', () => { Forms.updateFormFieldsOrder = jest.fn(); - await mutations.formsUpdateFormFieldsOrder(null, doc, { user: _user }); + await formMutations.formsUpdateFormFieldsOrder(null, doc, { user: _user }); expect(Forms.updateFormFieldsOrder).toBeCalledWith(doc.orderDics); expect(Forms.updateFormFieldsOrder.mock.calls.length).toBe(1); }); - test('check whether form duplication mutation is working successfully', async () => { + test('test mutations.formsDuplicate', async () => { const fakeId = 'fakeFormid'; Forms.duplicate = jest.fn(); - await mutations.formsDuplicate(null, { _id: fakeId }, { user: _user }); + await formMutations.formsDuplicate(null, { _id: fakeId }, { user: _user }); expect(Forms.duplicate).toBeCalledWith(fakeId); expect(Forms.duplicate.mock.calls.length).toBe(1); diff --git a/src/data/resolvers/mutations/forms.js b/src/data/resolvers/mutations/forms.js index c5822be3a..639b9e595 100644 --- a/src/data/resolvers/mutations/forms.js +++ b/src/data/resolvers/mutations/forms.js @@ -8,8 +8,8 @@ export default { * @param {string} doc.title - Form title * @param {string} doc.description - Form description * @param {Object} doc.user - The user who created this form - * @return {Promise} returns the form promise - * @throws {Error} throws error if user is not logged in + * @return {Promise} return Promise resolving Form document + * @throws {Error} throws Error('Login required') if user is not logged in */ formsCreate(root, doc, { user }) { if (!user) { @@ -28,8 +28,8 @@ export default { * @param {string} object2.description - Form description * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user who is making this action - * @return {Promise} returns null - * @throws {Error} throws error if user is not logged in + * @return {Promise} return Promise resolving Form document + * @throws {Error} throws Error('Login required') if user is not logged in */ formsEdit(root, { _id, ...doc }, { user }) { if (!user) { @@ -46,8 +46,8 @@ export default { * @param {string} object2._id - Form id * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} null - * @throws {Error} throws error if user is not logged in + * @return {Promise} + * @throws {Error} throws Error('Login required') if user is not logged in */ formsRemove(root, { _id }, { user }) { if (!user) { @@ -70,8 +70,8 @@ export default { * @param {Boolean} object2.isRequired - Shows whether the field is required or not * @param {Object} object3 - Middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} return Promise(null) - * @throws {Error} throws error if user is not logged in + * @return {Promise} return Promise resolving new FormField document + * @throws {Error} throws Error('Login required') if user is not logged in */ formsAddFormField(root, { formId, ...formFieldDoc }, { user }) { if (!user) { @@ -93,8 +93,8 @@ export default { * @param {Boolean} object2.isRequired * @param {Object} object3 - Middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} return Promise(null) - * @throws {Error} throws error if user is not logged in + * @return {Promise} return Promise resolving updated FormField + * @throws {Error} throws Error('Login required') if user is not logged in */ formsEditFormField(root, { _id, ...formFieldDoc }, { user }) { if (!user) { @@ -111,8 +111,8 @@ export default { * @param {string} object2._id - Form field id * @param {Object} object3 - Middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} null - * @throws {Error} throws error if user is not logged in + * @return {Promise} + * @throws {Error} throws Error('Login required') if user is not logged in */ formsRemoveFormField(root, { _id }, { user }) { if (!user) { @@ -129,8 +129,8 @@ export default { * @param {Object} object2.orderDics - Dictionary containing order values for form fields * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} null - * @throws {Error} throws error if user is not logged in + * @return {Promise} + * @throws {Error} throws Error('Login required') if user is not logged in */ formsUpdateFormFieldsOrder(root, { orderDics }, { user }) { if (!user) { @@ -147,8 +147,8 @@ export default { * @param {string} object2._id - Form id * @param {Object} object3 - Middleware data * @param {Object} object3.user - The user making this action - * @return {Promise} returns form object - * @throws {Error} throws error if user is not logged in + * @return {Promise} return Promise resolving the new duplication Form document + * @throws {Error} throws Error('Login required') if user is not logged in */ formsDuplicate(root, { _id }, { user }) { if (!user) { diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index 66b6a61d4..f7cbf0bf8 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -78,7 +78,7 @@ class Form { /** * Remove a form * @param {string} _id - Form document id - * @return {Null} returns null + * @return {Promise} * @throws {Error} throws Error if this form has fields or if used in an integration */ static async removeForm(_id) { @@ -94,7 +94,7 @@ class Form { throw new Error('You cannot delete this form. This form used in integration.'); } - return await this.remove({ _id }); + return this.remove({ _id }); } /** @@ -102,7 +102,7 @@ class Form { * @param {Object[]} orderDics - dictionary containing order values with user ids * @param {string} orderDics[]._id - _id of FormField * @param {string} orderDics[].order - order of FormField - * @return {Promise} null + * @return {Null} */ static async updateFormFieldsOrder(orderDics) { // update each field's order @@ -119,7 +119,7 @@ class Form { static async duplicate(_id) { const form = await this.findOne({ _id }); - // duplicate form + // duplicate form =================== const newForm = await this.createForm( { title: `${form.title} duplicated`, @@ -128,7 +128,7 @@ class Form { form.createdUserId, ); - // duplicate fields + // duplicate fields =================== const formFields = await FormFields.find({ formId: _id }); for (let field of formFields) { @@ -192,7 +192,7 @@ class FormField { * @param {string} doc.description - FormField description * @param {String[]} doc.options - FormField select options (checkbox, radion buttons, ...) * @param {Boolean} doc.isRequired - checks whether value is filled or not on validation - * @return {Promise} - returns form field document promise + * @return {Promise} - return Promise resolving created FormField document */ static async createFormField(formId, doc) { const lastField = await FormFields.findOne({}, { order: 1 }, { sort: { order: -1 } }); @@ -216,10 +216,11 @@ class FormField { * @param {string} doc.description - FormField description * @param {String[]} doc.options - FormField select options (checkbox, radion buttons, ...) * @param {Boolean} doc.isRequired checks whether value is filled or not on validation - * @return {Promise} + * @return {Promise} return Promise resolving updated FormField document */ - static updateFormField(_id, doc) { - return this.update({ _id }, { $set: doc }, { runValidators: true }); + static async updateFormField(_id, doc) { + await this.update({ _id }, { $set: doc }, { runValidators: true }); + return this.findOne({ _id }); } /** From 6fc0cdc894232222225163f17aaa580573339bfc Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 13 Oct 2017 14:27:04 +0800 Subject: [PATCH 054/318] #13 Change names of imports --- src/__tests__/channelMutations.test.js | 9 +++---- src/__tests__/formMutations.test.js | 23 +++++++++++++++++ src/__tests__/integrationMutations.test.js | 28 ++++++++++----------- src/__tests__/notificationMutations.test.js | 10 ++++---- 4 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/__tests__/channelMutations.test.js b/src/__tests__/channelMutations.test.js index b7526fc42..561609af6 100644 --- a/src/__tests__/channelMutations.test.js +++ b/src/__tests__/channelMutations.test.js @@ -3,8 +3,7 @@ import { connect, disconnect } from '../db/connection'; import { userFactory, integrationFactory } from '../db/factories'; import { Channels, Users, Integrations } from '../db/models'; -import mutations from '../data/resolvers/mutations'; -import { sendChannelNotifications } from '../data/utils'; +import channelMutations from '../data/resolvers/mutations/channels'; beforeAll(() => connect()); afterAll(() => disconnect()); @@ -173,7 +172,7 @@ describe('test mutations', () => { Channels.createChannel = jest.fn(); try { - await mutations.channelsCreate(null, doc, { user: _user }); + await channelMutations.channelsCreate(null, doc, { user: _user }); } catch (e) { /* this error is caused by Channels.createChannel mock function; sendChannelNotifications method further in the workflow was using @@ -198,7 +197,7 @@ describe('test mutations', () => { Channels.updateChannel = jest.fn(); - await mutations.channelsEdit(null, { ...doc, _id: channelId }, { user: _user }); + await channelMutations.channelsEdit(null, { ...doc, _id: channelId }, { user: _user }); expect(Channels.updateChannel).toBeCalledWith(channelId, doc); expect(Channels.updateChannel.mock.calls.length).toBe(1); @@ -206,7 +205,7 @@ describe('test mutations', () => { // test mutations.channelsRemove ============= Channels.removeChannel = jest.fn(); - await mutations.channelsRemove(null, { _id: channelId }, { user: _user }); + await channelMutations.channelsRemove(null, { _id: channelId }, { user: _user }); expect(Channels.removeChannel).toBeCalledWith(channelId); expect(Channels.removeChannel.mock.calls.length).toEqual(1); diff --git a/src/__tests__/formMutations.test.js b/src/__tests__/formMutations.test.js index ad0346434..c4a8e61a5 100644 --- a/src/__tests__/formMutations.test.js +++ b/src/__tests__/formMutations.test.js @@ -22,6 +22,29 @@ describe('form and formField mutations', () => { await Users.remove({}); }); + test('test if `logging required` error is working as intended', () => { + expect.assertions(8); + + // Login required ================== + expect(() => formMutations.formsCreate(null, {}, {})).toThrowError('Login required'); + + expect(() => formMutations.formsEdit(null, {}, {})).toThrowError('Login required'); + + expect(() => formMutations.formsRemove(null, {}, {})).toThrowError('Login required'); + + expect(() => formMutations.formsAddFormField(null, {}, {})).toThrowError('Login required'); + + expect(() => formMutations.formsEditFormField(null, {}, {})).toThrowError('Login required'); + + expect(() => formMutations.formsRemoveFormField(null, {}, {})).toThrowError('Login required'); + + expect(() => formMutations.formsUpdateFormFieldsOrder(null, {}, {})).toThrowError( + 'Login required', + ); + + expect(() => formMutations.formsDuplicate(null, {}, {})).toThrowError('Login required'); + }); + test(`test mutations.formsCreate`, async () => { Forms.createForm = jest.fn(); diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js index e72bab29a..a6c6e0d46 100644 --- a/src/__tests__/integrationMutations.test.js +++ b/src/__tests__/integrationMutations.test.js @@ -6,7 +6,7 @@ import { connect, disconnect } from '../db/connection'; import { FORM_LOAD_TYPES, MESSENGER_DATA_AVAILABILITY } from '../data/constants'; import { userFactory } from '../db/factories'; import { Integrations, Users } from '../db/models'; -import IntegrationMutations from '../data/resolvers/mutations/integrations'; +import integrationMutations from '../data/resolvers/mutations/integrations'; beforeAll(() => connect()); afterAll(() => disconnect()); @@ -30,26 +30,26 @@ describe('mutations', () => { // Login required ================== expect(() => - IntegrationMutations.integrationsCreateMessengerIntegration(null, {}, {}), + integrationMutations.integrationsCreateMessengerIntegration(null, {}, {}), ).toThrowError('Login required'); expect(() => - IntegrationMutations.integrationsEditMessengerIntegration(null, {}, {}), + integrationMutations.integrationsEditMessengerIntegration(null, {}, {}), ).toThrowError('Login required'); expect(() => - IntegrationMutations.integrationsSaveMessengerAppearanceData(null, {}, {}), + integrationMutations.integrationsSaveMessengerAppearanceData(null, {}, {}), ).toThrowError('Login required'); - expect(() => IntegrationMutations.integrationsCreateFormIntegration(null, {}, {})).toThrowError( + expect(() => integrationMutations.integrationsCreateFormIntegration(null, {}, {})).toThrowError( 'Login required', ); - expect(() => IntegrationMutations.integrationsEditFormIntegration(null, {}, {})).toThrowError( + expect(() => integrationMutations.integrationsEditFormIntegration(null, {}, {})).toThrowError( 'Login required', ); - expect(() => IntegrationMutations.integrationsRemove(null, {}, {})).toThrowError( + expect(() => integrationMutations.integrationsRemove(null, {}, {})).toThrowError( 'Login required', ); }); @@ -62,7 +62,7 @@ describe('mutations', () => { Integrations.createMessengerIntegration = jest.fn(); - await IntegrationMutations.integrationsCreateMessengerIntegration(null, doc, { user: _user }); + await integrationMutations.integrationsCreateMessengerIntegration(null, doc, { user: _user }); expect(Integrations.createMessengerIntegration).toBeCalledWith(doc); expect(Integrations.createMessengerIntegration.mock.calls.length).toBe(1); @@ -77,7 +77,7 @@ describe('mutations', () => { Integrations.updateMessengerIntegration = jest.fn(); - await IntegrationMutations.integrationsEditMessengerIntegration(null, doc, { user: _user }); + await integrationMutations.integrationsEditMessengerIntegration(null, doc, { user: _user }); delete doc._id; @@ -94,7 +94,7 @@ describe('mutations', () => { Integrations.saveMessengerAppearanceData = jest.fn(); - await IntegrationMutations.integrationsSaveMessengerAppearanceData( + await integrationMutations.integrationsSaveMessengerAppearanceData( null, { _id: _fakeIntegrationId, @@ -132,7 +132,7 @@ describe('mutations', () => { Integrations.saveMessengerConfigs = jest.fn(); - await IntegrationMutations.integrationsSaveMessengerConfigs( + await integrationMutations.integrationsSaveMessengerConfigs( null, { _id: _fakeIntegrationId, @@ -163,7 +163,7 @@ describe('mutations', () => { formData, }; - await IntegrationMutations.integrationsCreateFormIntegration(null, doc, { user: _user }); + await integrationMutations.integrationsCreateFormIntegration(null, doc, { user: _user }); expect(Integrations.createFormIntegration).toBeCalledWith(doc); expect(Integrations.createFormIntegration.mock.calls.length).toBe(1); @@ -188,7 +188,7 @@ describe('mutations', () => { Integrations.updateFormIntegration = jest.fn(); - await IntegrationMutations.integrationsEditFormIntegration(null, doc, { user: _user }); + await integrationMutations.integrationsEditFormIntegration(null, doc, { user: _user }); delete doc._id; @@ -199,7 +199,7 @@ describe('mutations', () => { test('test Integrations.removeIntegration', async () => { Integrations.removeIntegration = jest.fn(); - await IntegrationMutations.integrationsRemove( + await integrationMutations.integrationsRemove( null, { _id: _fakeIntegrationId }, { user: _user }, diff --git a/src/__tests__/notificationMutations.test.js b/src/__tests__/notificationMutations.test.js index 40c8f6b02..803ccd649 100644 --- a/src/__tests__/notificationMutations.test.js +++ b/src/__tests__/notificationMutations.test.js @@ -3,7 +3,7 @@ import { connect, disconnect } from '../db/connection'; import { Notifications, NotificationConfigurations } from '../db/models'; -import NotificationMutations from '../data/resolvers/mutations/notifications'; +import notificationMutations from '../data/resolvers/mutations/notifications'; import { userFactory } from '../db/factories'; import { Users } from '../db/models'; @@ -25,11 +25,11 @@ describe('testing mutations', () => { expect.assertions(2); // Login required ================== - expect(() => NotificationMutations.notificationsSaveConfig(null, {}, {})).toThrowError( + expect(() => notificationMutations.notificationsSaveConfig(null, {}, {})).toThrowError( 'Login required', ); - expect(() => NotificationMutations.notificationsMarkAsRead(null, {}, {})).toThrowError( + expect(() => notificationMutations.notificationsMarkAsRead(null, {}, {})).toThrowError( 'Login required', ); }); @@ -43,7 +43,7 @@ describe('testing mutations', () => { user: _user._id, }; - await NotificationMutations.notificationsSaveConfig(null, doc, { user: _user }); + await notificationMutations.notificationsSaveConfig(null, doc, { user: _user }); expect(NotificationConfigurations.createOrUpdateConfiguration).toBeCalledWith(doc, _user); expect(NotificationConfigurations.createOrUpdateConfiguration.mock.calls.length).toBe(1); @@ -54,7 +54,7 @@ describe('testing mutations', () => { const args = { ids: ['11111', '22222'] }; - await NotificationMutations.notificationsMarkAsRead(null, args, { user: _user }); + await notificationMutations.notificationsMarkAsRead(null, args, { user: _user }); expect(Notifications.markAsRead).toBeCalledWith(args['ids']); expect(Notifications.markAsRead.mock.calls.length).toBe(1); From 81ed57d88a8671f12037f8d3cbc24c18306fa333 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 13 Oct 2017 15:11:14 +0800 Subject: [PATCH 055/318] #13 Refactor channels --- src/__tests__/channelDb.test.js | 155 +++++++++++++++++ src/__tests__/channelMutations.test.js | 203 ++++++----------------- src/data/resolvers/mutations/channels.js | 22 +-- src/data/utils.js | 1 + src/db/models/Channels.js | 2 +- 5 files changed, 221 insertions(+), 162 deletions(-) create mode 100644 src/__tests__/channelDb.test.js diff --git a/src/__tests__/channelDb.test.js b/src/__tests__/channelDb.test.js new file mode 100644 index 000000000..bcc5c24f3 --- /dev/null +++ b/src/__tests__/channelDb.test.js @@ -0,0 +1,155 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { userFactory, integrationFactory } from '../db/factories'; +import { Channels, Users, Integrations } from '../db/models'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('test channel creation error', () => { + test(`check if Error('userId must be supplied') is being thrown as intended`, async () => { + try { + Channels.createChannel({ + name: 'Channel test', + }); + } catch (e) { + expect(e.message).toBe('userId must be supplied'); + } + }); +}); + +describe('channel creation', () => { + let _user; + let _user2; + let _integration; + + beforeEach(async () => { + _user = await userFactory({}); + _integration = await integrationFactory({}); + _user2 = await userFactory({}); + }); + + afterEach(async () => { + await Channels.remove({}); + await Users.remove({}); + await Integrations.remove({}); + }); + + test('check if channel is getting created successfully', async () => { + const doc = { + name: 'Channel test', + description: 'test channel descripion', + memberIds: [_user2._id], + integrationIds: [_integration._id], + }; + + const channel = await Channels.createChannel(doc, _user._id); + + expect(channel.name).toEqual(doc.name); + expect(channel.description).toEqual(doc.description); + expect(channel.memberIds.length).toBe(2); + expect(channel.integrationIds.length).toEqual(1); + expect(channel.integrationIds[0]).toEqual(_integration._id); + expect(channel.userId).toEqual(doc.userId); + expect(channel.conversationCount).toEqual(0); + expect(channel.openConversationCount).toEqual(0); + }); +}); + +describe('channel update', () => { + let _user; + let _user2; + let _integration; + let _channelDoc; + let _channel; + + /** + * Before each test create test data + * containing 2 users and an integration + */ + beforeEach(async () => { + _user = await userFactory({}); + _integration = await integrationFactory({}); + _user2 = await userFactory({}); + + _channelDoc = { + name: 'Channel test', + description: 'Channel test description', + userId: _user._id, + memberIds: [_user2._id], + integrationIds: [_integration._id], + }; + + _channel = await Channels.createChannel(_channelDoc, _user); + }); + + /** + * After each test remove the test data + */ + afterEach(async () => { + await Channels.remove({}); + await Users.remove({}); + await Integrations.remove({}); + }); + + test(`check if Channel update method and + Channel.preSave filter is working successfully`, async () => { + // test Channels.createChannel and Channels.preSave ============= + let channel = await Channels.findOne({ _id: _channel._id }); + + expect(channel.name).toEqual(_channelDoc.name); + expect(channel.description).toEqual(_channelDoc.description); + expect(channel.memberIds.length).toBe(2); + expect(channel.memberIds[0]).toBe(_user2._id); + expect(channel.memberIds[1]).toBe(_user._id); + expect(channel.integrationIds.length).toEqual(1); + expect(channel.integrationIds[0]).toEqual(_integration._id); + + expect(channel.userId).toEqual(_channelDoc.userId); + expect(channel.conversationCount).toEqual(0); + expect(channel.openConversationCount).toEqual(0); + + // test Channels.updateChannel and Channels.preSave on update ========== + _channelDoc.memberIds = [_user._id]; + + channel = await Channels.updateChannel(channel._id, _channelDoc); + + expect(channel.memberIds.length).toBe(1); + expect(channel.memberIds[0]).toBe(_user._id); + + // testing whether the updated field is not overwriting whole document ======== + channel = await Channels.updateChannel(channel._id, { + name: 'Channel test 2', + }); + + expect(channel.description).toBe('Channel test description'); + }); +}); + +describe('channel remove', () => { + let _channel; + + beforeEach(async () => { + const user = await userFactory({}); + _channel = await Channels.createChannel( + { + name: 'Channel test', + }, + user._id, + ); + }); + + afterEach(async () => { + await Channels.remove({}); + }); + + test('check if channel remove method is working successfully', async () => { + await Channels.removeChannel(_channel._id); + + const channelCount = await Channels.find({}).count(); + + expect(channelCount).toBe(0); + }); +}); diff --git a/src/__tests__/channelMutations.test.js b/src/__tests__/channelMutations.test.js index 561609af6..06b4dcd11 100644 --- a/src/__tests__/channelMutations.test.js +++ b/src/__tests__/channelMutations.test.js @@ -1,166 +1,50 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ + import { connect, disconnect } from '../db/connection'; -import { userFactory, integrationFactory } from '../db/factories'; -import { Channels, Users, Integrations } from '../db/models'; +import { userFactory } from '../db/factories'; +import { Channels, Users } from '../db/models'; import channelMutations from '../data/resolvers/mutations/channels'; beforeAll(() => connect()); afterAll(() => disconnect()); -describe('test channel creation error', () => { - test('check if Error is being thrown as intended', async () => { - try { - Channels.createChannel({ - name: 'Channel test', - }); - } catch (e) { - expect(e.message).toBe('userId must be supplied'); - } - }); -}); - -describe('test successful channel creation', () => { +describe('mutations', () => { + const _channelId = 'fakeChannelId'; let _user; - let _user2; - let _integration; - - beforeEach(async () => { - _user = await userFactory({}); - _integration = await integrationFactory({}); - _user2 = await userFactory({}); - }); - - afterEach(async () => { - await Channels.remove({}); - await Users.remove({}); - await Integrations.remove({}); - }); - - test('check if channel is getting created successfully', async () => { - const doc = { - name: 'Channel test', - description: 'test channel descripion', - memberIds: [_user2._id], - integrationIds: [_integration._id], - }; - - const channel = await Channels.createChannel(doc, _user._id); - - expect(channel.name).toEqual(doc.name); - expect(channel.description).toEqual(doc.description); - expect(channel.memberIds.length).toBe(2); - expect(channel.integrationIds.length).toEqual(1); - expect(channel.integrationIds[0]).toEqual(_integration._id); - expect(channel.userId).toEqual(doc.userId); - expect(channel.conversationCount).toEqual(0); - expect(channel.openConversationCount).toEqual(0); - }); -}); -describe('channel update tests', () => { - let _user; - let _user2; - let _integration; - let _channelDoc; - let _channel; - - /** - * Before each test create test data - * containing 2 users and an integration - */ beforeEach(async () => { _user = await userFactory({}); - _integration = await integrationFactory({}); - _user2 = await userFactory({}); - - _channelDoc = { - name: 'Channel test', - description: 'Channel test description', - userId: _user._id, - memberIds: [_user2._id], - integrationIds: [_integration._id], - }; - - _channel = await Channels.createChannel(_channelDoc, _user); }); - /** - * After each test remove the test data - */ afterEach(async () => { - await Channels.remove({}); await Users.remove({}); - await Integrations.remove({}); - }); - - test(`check if Channel update method and - Channel.preSave filter is working successfully`, async () => { - // test Channels.createChannel and Channels.preSave ============= - let channel = await Channels.findOne({ _id: _channel._id }); - - expect(channel.name).toEqual(_channelDoc.name); - expect(channel.description).toEqual(_channelDoc.description); - expect(channel.memberIds.length).toBe(2); - expect(channel.memberIds[0]).toBe(_user2._id); - expect(channel.memberIds[1]).toBe(_user._id); - expect(channel.integrationIds.length).toEqual(1); - expect(channel.integrationIds[0]).toEqual(_integration._id); - - expect(channel.userId).toEqual(_channelDoc.userId); - expect(channel.conversationCount).toEqual(0); - expect(channel.openConversationCount).toEqual(0); - - // test Channels.updateChannel and Channels.preSave on update ========== - _channelDoc.memberIds = [_user._id]; - await Channels.updateChannel(channel._id, _channelDoc); - channel = await Channels.findOne({ _id: channel._id }); - expect(channel.memberIds.length).toBe(1); - expect(channel.memberIds[0]).toBe(_user._id); - - // testing whether the updated field is not overwriting whole document ======== - await Channels.updateChannel(channel._id, { - name: 'Channel test 2', - }); - - expect(channel.description).toBe('Channel test description'); }); -}); -describe('channel remove test', () => { - let _channel; + test(`test if Error('Login required') error is working as intended`, async () => { + expect.assertions(3); - beforeEach(async () => { - const user = await userFactory({}); - _channel = await Channels.createChannel( - { - name: 'Channel test', - }, - user._id, - ); - }); - - afterEach(async () => { - await Channels.remove({}); - }); - - test('checking if channel remove method is working successfully', async () => { - await Channels.removeChannel(_channel._id); - const channelCount = await Channels.find({}).count(); - expect(channelCount).toBe(0); - }); -}); + try { + await channelMutations.channelsCreate(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } -describe('test mutations', () => { - let _user; + try { + await channelMutations.channelsEdit(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } - beforeEach(async () => { - _user = await userFactory({}); + try { + await channelMutations.channelsRemove(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } }); - test('testing if mutations.channelsCreate is working successfully', async () => { - expect.assertions(6); - // test mutations.channelsCreate ================== + test('test mutations.channelsCreate', async () => { + expect.assertions(2); let doc = { name: 'Channel test', @@ -183,11 +67,10 @@ describe('test mutations', () => { expect(Channels.createChannel.mock.calls.length).toBe(1); } } + }); - // test mutations.channelsUpdate ========= - const channelId = 'fakeChannelId'; - - doc = { + test('test mutations.channelsUpdate', async () => { + const doc = { name: 'Channel test 1', description: 'Channel test description 1', userId: 'fakeUserId1', @@ -197,17 +80,35 @@ describe('test mutations', () => { Channels.updateChannel = jest.fn(); - await channelMutations.channelsEdit(null, { ...doc, _id: channelId }, { user: _user }); - - expect(Channels.updateChannel).toBeCalledWith(channelId, doc); - expect(Channels.updateChannel.mock.calls.length).toBe(1); + try { + await channelMutations.channelsEdit( + null, + { + ...doc, + _id: _channelId, + }, + { user: _user }, + ); + } catch (e) { + /* this error is caused by Channels.updateChannel mock function; + sendChannelNotifications method further in the workflow was using + the object returned by Channels.updateChannel, since we mocked it, + returns null */ + if (e.message === `Cannot read property '_id' of undefined`) { + expect(Channels.updateChannel).toBeCalledWith(_channelId, doc); + expect(Channels.updateChannel.mock.calls.length).toBe(1); + } else { + throw e; + } + } + }); - // test mutations.channelsRemove ============= + test('test mutations.channelsRemove', async () => { Channels.removeChannel = jest.fn(); - await channelMutations.channelsRemove(null, { _id: channelId }, { user: _user }); + await channelMutations.channelsRemove(null, { _id: _channelId }, { user: _user }); - expect(Channels.removeChannel).toBeCalledWith(channelId); + expect(Channels.removeChannel).toBeCalledWith(_channelId); expect(Channels.removeChannel.mock.calls.length).toEqual(1); }); }); diff --git a/src/data/resolvers/mutations/channels.js b/src/data/resolvers/mutations/channels.js index dac334447..1f77edfec 100644 --- a/src/data/resolvers/mutations/channels.js +++ b/src/data/resolvers/mutations/channels.js @@ -11,7 +11,7 @@ export default { * @param {String[]} doc.memberIds - Members assigned to the channel being created * @param {String[]} doc.integrationIds - Integrations related to the channel * @param {Object|string} user - User making this action - * @return {Promise} returns channel object + * @return {Promise} return Promise resolving created Channel document * @throws {Error} throws Error('Login required') if user is not logged in */ async channelsCreate(root, doc, { user }) { @@ -19,9 +19,9 @@ export default { throw new Error('Login required'); } - const channel = Channels.createChannel(doc, user); + const channel = await Channels.createChannel(doc, user); - sendChannelNotifications({ + await sendChannelNotifications({ userId: channel.userId, memberIds: channel.memberIds, channelId: channel._id, @@ -41,21 +41,23 @@ export default { * @param {string[]} doc.integrationIds - Integration related to this channel * @param {Object} object3 - Graphql input data * @param {Object|string} object3.user - user making this action - * @return {Promise} returns null + * @return {Promise} return Promise resolving the updated Channel document * @throws {Error} throws Error('Login required') if user is not logged in */ - channelsEdit(root, { _id, ...doc }, { user }) { + async channelsEdit(root, { _id, ...doc }, { user }) { if (!user) { throw new Error('Login required'); } - sendChannelNotifications({ - channelId: _id, - memberIds: doc.memberIds, + const channel = Channels.updateChannel(_id, doc); + + await sendChannelNotifications({ + channelId: channel._id, + memberIds: channel.memberIds, userId: user, }); - return Channels.updateChannel(_id, doc); + return channel; }, /** @@ -65,7 +67,7 @@ export default { * @param {string} object2._id - Channel id * @param {string} object3 - Middleware data * @param {Object|String} object3.user - User making this action - * @return {Promise} null + * @return {Promise} * @throws {Error} throws Error('Login required') if user is not logged in */ channelsRemove(root, { _id }, { user }) { diff --git a/src/data/utils.js b/src/data/utils.js index 74dea0aaa..5343d2c4e 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -95,6 +95,7 @@ export const sendEmail = async ({ toEmails, fromEmail, title, templateArgs }) => * @param {String} channelId * @param {Array} memberIds * @param {String} userId + * @return {Promise} */ export const sendChannelNotifications = async ({ channelId, memberIds, userId }) => { memberIds = memberIds || []; diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index fb67919b3..3ce30d7ca 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -29,7 +29,7 @@ const ChannelSchema = mongoose.Schema({ class Channel { /** * Pre save filter method that adds userId to memberIds if it does not contain it - * @param {Object} doc - Channel object + * @param {Channel} doc - Channel object * @return {Null} */ static preSave(doc) { From 415e0c787af65b7a54ab0c7fdbeb7ead0f4602cb Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 13 Oct 2017 15:14:57 +0800 Subject: [PATCH 056/318] #13 Refactor forms, integrations, channels, notifications --- src/__tests__/integrationDb.test.js | 23 ++++++++--------------- src/__tests__/notificationDb.test.js | 4 +--- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/__tests__/integrationDb.test.js b/src/__tests__/integrationDb.test.js index 5b3c8aeb2..7c24f87e4 100644 --- a/src/__tests__/integrationDb.test.js +++ b/src/__tests__/integrationDb.test.js @@ -62,9 +62,7 @@ describe('messenger integration model edit method', () => { kind: 'new kind', }; - await Integrations.updateMessengerIntegration(_integration._id, doc); - - const updatedIntegration = await Integrations.findOne({ _id: _integration._id }); + const updatedIntegration = await Integrations.updateMessengerIntegration(_integration._id, doc); expect(updatedIntegration.name).toBe(doc.name); expect(updatedIntegration.brandId).toBe(doc.brandId); @@ -189,13 +187,11 @@ describe('edit form integration', () => { loadType: FORM_LOAD_TYPES.SHOUTBOX, }; - await Integrations.updateFormIntegration(_form_integration._id, { + const integration = await Integrations.updateFormIntegration(_form_integration._id, { ...mainDoc, formData, }); - const integration = await Integrations.findOne({ _id: _form_integration._id }); - expect(integration.name).toEqual(mainDoc.name); expect(integration.formId).toEqual(_form2._id); expect(integration.brandId).toEqual(_brand2._id); @@ -256,9 +252,7 @@ describe('save integration messenger appearance test', () => { logo: faker.random.word(), }; - await Integrations.saveMessengerAppearanceData(_integration._id, uiOptions); - - const integration = await Integrations.findOne({ _id: _integration._id }); + const integration = await Integrations.saveMessengerAppearanceData(_integration._id, uiOptions); expect(integration.uiOptions.color).toEqual(uiOptions.color); expect(integration.uiOptions.wallpaper).toEqual(uiOptions.wallpaper); @@ -308,9 +302,7 @@ describe('save integration messenger configurations test', () => { thankYouMessage: 'Thank you', }; - await Integrations.saveMessengerConfigs(_integration._id, messengerData); - - const integration = await Integrations.findOne({ _id: _integration._id }); + const integration = await Integrations.saveMessengerConfigs(_integration._id, messengerData); expect(integration.messengerData.notifyCustomer).toEqual(messengerData.notifyCustomer); expect(integration.messengerData.availabilityMethod).toEqual(messengerData.availabilityMethod); @@ -352,9 +344,10 @@ describe('save integration messenger configurations test', () => { thankYouMessage: 'Gracias', }; - await Integrations.saveMessengerConfigs(_integration._id, newMessengerData); - - const updatedIntegration = await Integrations.findOne({ _id: _integration._id }); + const updatedIntegration = await Integrations.saveMessengerConfigs( + _integration._id, + newMessengerData, + ); expect(updatedIntegration.messengerData.notifyCustomer).toEqual( newMessengerData.notifyCustomer, diff --git a/src/__tests__/notificationDb.test.js b/src/__tests__/notificationDb.test.js index 9d4e09734..1563e96af 100644 --- a/src/__tests__/notificationDb.test.js +++ b/src/__tests__/notificationDb.test.js @@ -80,9 +80,7 @@ describe('Notification model tests', () => { receiver: user3, }; - await Notifications.updateNotification(notification._id, doc); - - notification = await Notifications.findOne({ _id: notification._id }); + notification = await Notifications.updateNotification(notification._id, doc); expect(notification.notifType).toEqual(doc.notifType); expect(notification.title).toEqual(doc.title); From de7f4e15e4befd46bfadf1e3cbc7d316b06dc65c Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 13 Oct 2017 16:53:16 +0800 Subject: [PATCH 057/318] Remove email template logic from sendEmail --- src/data/utils.js | 47 ++--------------------------------------------- 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/src/data/utils.js b/src/data/utils.js index 5343d2c4e..2a85cc4ec 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -1,39 +1,7 @@ import nodemailer from 'nodemailer'; -import Handlebars from 'handlebars'; -import fs from 'fs'; import { MODULES } from './constants'; import { Channels, Notifications, Users } from '../db/models'; -/** - * Read template file with via utf-8 - * @param {String} assetPath - * @return {String} file content - */ -const getTemplateContent = assetPath => { - // TODO: test this method - fs.readFile(assetPath, 'utf8', (err, data) => { - if (err) { - throw err; - } - - return data; - }); -}; - -/** - * SendEmail template helper - * @param {Object} data data - * @param {String} templateName - * @return email with template as text - */ -const applyTemplate = async (data, templateName) => { - let template = await getTemplateContent(`emailTemplates/${templateName}.html`); - - template = Handlebars.compile(template); - - return template(data); -}; - /** * Send email * @param {Array} args.toEmails @@ -62,25 +30,14 @@ export const sendEmail = async ({ toEmails, fromEmail, title, templateArgs }) => }, }); - const { isCustom, data, name } = templateArgs; - - // generate email content by given template - const content = await applyTemplate(data, name); - - let text = ''; - - if (isCustom) { - text = content; - } else { - text = await applyTemplate({ content }, 'base'); - } + const { data } = templateArgs; return toEmails.map(toEmail => { const mailOptions = { from: fromEmail, to: toEmail, subject: title, - text, + text: data, }; return transporter.sendMail(mailOptions, (error, info) => { From 46c579027f52da1235791e4465d84df0663f24dc Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Fri, 13 Oct 2017 17:07:02 +0800 Subject: [PATCH 058/318] Seperate db, mutation tests --- src/__tests__/engageMessageDb.test.js | 79 +++++++ src/__tests__/engageMessageMutations.test.js | 113 ++++++++++ src/__tests__/engageMessages.test.js | 207 ------------------ .../{tags.test.js => tagMutations.test.js} | 2 +- src/data/resolvers/mutations/engageUtils.js | 28 --- src/data/resolvers/mutations/engages.js | 36 +-- src/data/schema/engage.js | 12 +- src/db/factories.js | 23 +- src/db/models/Engages.js | 28 ++- test.config.json | 4 - 10 files changed, 256 insertions(+), 276 deletions(-) create mode 100644 src/__tests__/engageMessageDb.test.js create mode 100644 src/__tests__/engageMessageMutations.test.js delete mode 100644 src/__tests__/engageMessages.test.js rename src/__tests__/{tags.test.js => tagMutations.test.js} (97%) delete mode 100644 test.config.json diff --git a/src/__tests__/engageMessageDb.test.js b/src/__tests__/engageMessageDb.test.js new file mode 100644 index 000000000..2d946b3b2 --- /dev/null +++ b/src/__tests__/engageMessageDb.test.js @@ -0,0 +1,79 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { userFactory, segmentsFactory, engageMessageFactory } from '../db/factories'; +import { EngageMessages, Users, Segments } from '../db/models'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('engage messages model', () => { + let _user; + let _segment; + let _message; + + beforeEach(async () => { + _user = await userFactory({}); + _segment = await segmentsFactory({}); + _message = await engageMessageFactory({}); + }); + + afterEach(async () => { + await Users.remove({}); + await Segments.remove({}); + await EngageMessages.remove({}); + }); + + test('create messages', async () => { + const doc = { + kind: 'manual', + title: 'Message test', + fromUserId: _user._id, + segmentId: _segment._id, + isLive: true, + isDraft: false, + }; + + const message = await EngageMessages.createEngageMessage(doc); + expect(message.kind).toEqual(doc.kind); + expect(message.title).toEqual(doc.title); + expect(message.fromUserId).toEqual(_user._id); + expect(message.segmentId).toEqual(_segment._id); + expect(message.isLive).toEqual(doc.isLive); + expect(message.isDraft).toEqual(doc.isDraft); + }); + + test('update messages', async () => { + const message = await EngageMessages.updateEngageMessage(_message._id, { + title: 'Message test updated', + fromUserId: _user._id, + segmentId: _segment._id, + }); + + expect(message.title).toEqual('Message test updated'); + expect(message.fromUserId).toEqual(_user._id); + expect(message.segmentId).toEqual(_segment._id); + }); + + test('remove a message', async () => { + await EngageMessages.removeEngageMessage(_message._id); + const messagesCounts = await EngageMessages.find({}).count(); + expect(messagesCounts).toBe(0); + }); + + test('Engage message set live', async () => { + await EngageMessages.engageMessageSetLive(_message._id); + const message = await EngageMessages.findOne({ _id: _message._id }); + + expect(message.isLive).toEqual(true); + expect(message.isDraft).toEqual(false); + }); + + test('Engage message set pause', async () => { + await EngageMessages.engageMessageSetPause(_message._id); + const message = await EngageMessages.findOne({ _id: _message._id }); + + expect(message.isLive).toEqual(false); + }); +}); diff --git a/src/__tests__/engageMessageMutations.test.js b/src/__tests__/engageMessageMutations.test.js new file mode 100644 index 000000000..3ada53792 --- /dev/null +++ b/src/__tests__/engageMessageMutations.test.js @@ -0,0 +1,113 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { userFactory, segmentsFactory, engageMessageFactory } from '../db/factories'; +import { EngageMessages, Users, Segments } from '../db/models'; +import mutations from '../data/resolvers/mutations'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('mutations', () => { + let _user; + let _segment; + let _message; + let _doc = null; + + beforeEach(async () => { + _user = await userFactory({}); + _segment = await segmentsFactory({}); + _message = await engageMessageFactory({}); + _doc = { + kind: 'manual', + method: 'email', + title: 'Message test', + fromUserId: _user._id, + segmentId: _segment._id, + }; + }); + + afterEach(async () => { + _doc = null; + await Users.remove({}); + await Segments.remove({}); + await EngageMessages.remove({}); + }); + + test('messages create', async () => { + EngageMessages.createEngageMessage = jest.fn(); + await mutations.engageMessageAdd(null, _doc, { user: _user }); + + expect(EngageMessages.createEngageMessage).toBeCalledWith(_doc); + expect(EngageMessages.createEngageMessage.mock.calls.length).toBe(1); + }); + + test('Create message login required', async () => { + expect.assertions(1); + try { + await mutations.engageMessageAdd({}, _doc, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('messages update', async () => { + EngageMessages.updateEngageMessage = jest.fn(); + await mutations.engageMessageUpdate(null, { _id: _message._id, ..._doc }, { user: _user }); + + expect(EngageMessages.updateEngageMessage).toBeCalledWith(_message._id, _doc); + expect(EngageMessages.updateEngageMessage.mock.calls.length).toBe(1); + + EngageMessages.updateEngageMessage.mockClear(); + }); + + test('Update message login required', async () => { + expect.assertions(1); + try { + await mutations.engageMessageUpdate({}, { _id: _message._id, ..._doc }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('messages remove', async () => { + await mutations.engageMessageRemove(null, _message._id, { user: _user }); + + const messagesCounts = await EngageMessages.find({}).count(); + expect(messagesCounts).toBe(0); + }); + + test('Remove message login required', async () => { + expect.assertions(1); + try { + await mutations.engageMessageRemove({}, _message._id, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('set live', async () => { + EngageMessages.engageMessageSetLive = jest.fn(); + await mutations.engageMessageSetLive(null, _message._id, { user: _user }); + + expect(EngageMessages.engageMessageSetLive).toBeCalledWith(_message._id); + expect(EngageMessages.engageMessageSetLive.mock.calls.length).toBe(1); + }); + + test('set pause', async () => { + EngageMessages.engageMessageSetPause = jest.fn(); + await mutations.engageMessageSetPause(null, _message._id, { user: _user }); + + expect(EngageMessages.engageMessageSetPause).toBeCalledWith(_message._id); + expect(EngageMessages.engageMessageSetPause.mock.calls.length).toBe(1); + }); + + test('set live manual', async () => { + EngageMessages.engageMessageSetLive = jest.fn(); + await mutations.engageMessageSetLive(null, _message._id, { user: _user }); + + expect(EngageMessages.engageMessageSetLive).toBeCalledWith(_message._id); + expect(EngageMessages.engageMessageSetLive.mock.calls.length).toBe(1); + }); +}); diff --git a/src/__tests__/engageMessages.test.js b/src/__tests__/engageMessages.test.js deleted file mode 100644 index bdcadffa7..000000000 --- a/src/__tests__/engageMessages.test.js +++ /dev/null @@ -1,207 +0,0 @@ -/* eslint-env jest */ -/* eslint-disable no-underscore-dangle */ - -import { connect, disconnect } from '../db/connection'; -import { userFactory, segmentsFactory } from '../db/factories'; -import { EngageMessages, Users, Segments } from '../db/models'; -import mutations from '../data/resolvers/mutations'; - -beforeAll(() => connect()); -afterAll(() => disconnect()); - -describe('engage messages models', () => { - let _user; - let _segment = segmentsFactory(); - - beforeEach(async () => { - _user = await userFactory({}); - _segment = await segmentsFactory({}); - }); - - afterEach(async () => { - await Users.remove({}); - await Segments.remove({}); - await EngageMessages.remove({}); - }); - - test('create messages', async () => { - const doc = { - kind: 'manual', - title: 'Message test', - fromUserId: _user._id, - segmentId: _segment._id, - isLive: true, - isDraft: false, - }; - - const message = await EngageMessages.createMessage(doc); - expect(message.kind).toEqual(doc.kind); - expect(message.title).toEqual(doc.title); - expect(message.fromUserId).toEqual(_user._id); - expect(message.segmentId).toEqual(_segment._id); - expect(message.isLive).toEqual(doc.isLive); - expect(message.isDraft).toEqual(doc.isDraft); - }); - - test('update messages', async () => { - const doc = { - kind: 'manual', - title: 'Message test', - fromUserId: _user._id, - segmentId: _segment._id, - isLive: true, - isDraft: false, - }; - - let message = await EngageMessages.createMessage(doc); - - doc.title = 'Message test updated'; - doc.isLive = false; - doc.isDraft = true; - - await EngageMessages.updateMessage(message._id, doc); - message = await EngageMessages.findOne({ _id: message._id }); - - expect(message.kind).toEqual(doc.kind); - expect(message.title).toEqual(doc.title); - expect(message.fromUserId).toEqual(_user._id); - expect(message.segmentId).toEqual(_segment._id); - expect(message.isLive).toEqual(doc.isLive); - expect(message.isDraft).toEqual(doc.isDraft); - }); - - test('remove a message', async () => { - const _message = await EngageMessages.createMessage({ - kind: 'manual', - title: 'Message test', - fromUserId: _user._id, - }); - - await EngageMessages.removeMessage(_message._id); - const messagesCounts = await EngageMessages.find({}).count(); - expect(messagesCounts).toBe(0); - }); -}); - -describe('mutations', () => { - let _user; - let _segment = segmentsFactory(); - let _doc = null; - let messageId; - - beforeEach(async () => { - _user = await userFactory({}); - _segment = await segmentsFactory({}); - _doc = { - kind: 'manual', - method: 'email', - title: 'Message test', - fromUserId: _user._id, - segmentId: _segment._id, - }; - }); - - afterEach(async () => { - _doc = null; - await Users.remove({}); - await Segments.remove({}); - await EngageMessages.remove({}); - }); - - test('messages add', async () => { - const _message = await mutations.messagesAdd(null, _doc, { user: _user }); - expect(_message.kind).toEqual(_doc.kind); - expect(_message.title).toEqual(_doc.title); - expect(_message.fromUserId).toEqual(_user._id); - expect(_message.segmentId).toEqual(_segment._id); - expect(_message.isLive).toEqual(_doc.isLive); - expect(_message.isDraft).toEqual(_doc.isDraft); - }); - - test('Create message login required', async () => { - expect.assertions(1); - try { - await mutations.messagesAdd({}, _doc, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } - }); - - test('messages edit', async () => { - let message = await EngageMessages.createMessage(_doc); - messageId = message._id; - - _doc.title = 'Message test updated'; - _doc.isLive = false; - _doc.isDraft = true; - - await mutations.messageEdit(null, { _id: message._id, ..._doc }, { user: _user }); - message = await EngageMessages.findOne({ _id: message._id }); - - expect(message.kind).toEqual(_doc.kind); - expect(message.title).toEqual(_doc.title); - expect(message.fromUserId).toEqual(_user._id); - expect(message.segmentId).toEqual(_segment._id); - expect(message.isLive).toEqual(_doc.isLive); - expect(message.isDraft).toEqual(_doc.isDraft); - }); - - test('Update message login required', async () => { - expect.assertions(1); - try { - await mutations.messagesAdd({}, { _id: messageId, ..._doc }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } - }); - - test('messages remove', async () => { - const _message = await EngageMessages.createMessage(_doc); - messageId = _message._id; - - await mutations.messagesRemove(null, _message._id, { user: _user }); - - const messagesCounts = await EngageMessages.find({}).count(); - expect(messagesCounts).toBe(0); - }); - - test('Remove message login required', async () => { - expect.assertions(1); - try { - await mutations.messagesAdd({}, messageId, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } - }); - - test('set live', async () => { - _doc.isLive = false; - _doc.isDraft = true; - - let _message = await EngageMessages.createMessage(_doc); - - _message = await mutations.messagesSetLive(null, _message._id, { user: _user }); - expect(_message.isLive).toEqual(true); - expect(_message.isDraft).toEqual(false); - }); - - test('set pause', async () => { - _doc.isLive = true; - - let _message = await EngageMessages.createMessage(_doc); - - _message = await mutations.messagesSetPause(null, _message._id, { user: _user }); - expect(_message.isLive).toEqual(false); - }); - - test('set live manual', async () => { - _doc.isLive = false; - _doc.isDraft = true; - - let _message = await EngageMessages.createMessage(_doc); - - _message = await mutations.messagesSetLiveManual(null, _message._id, { user: _user }); - expect(_message.isLive).toEqual(true); - expect(_message.isDraft).toEqual(false); - }); -}); diff --git a/src/__tests__/tags.test.js b/src/__tests__/tagMutations.test.js similarity index 97% rename from src/__tests__/tags.test.js rename to src/__tests__/tagMutations.test.js index 6b4ca15b1..df75fb769 100644 --- a/src/__tests__/tags.test.js +++ b/src/__tests__/tagMutations.test.js @@ -97,7 +97,7 @@ describe('Tags mutations', () => { isDraft: false, }; - const message = await EngageMessages.createMessage(doc); + const message = await EngageMessages.createEngageMessage(doc); await tagsMutations.tagsTag( {}, { type: 'engageMessage', targetIds: [message._id], tagIds: [_tag._id] }, diff --git a/src/data/resolvers/mutations/engageUtils.js b/src/data/resolvers/mutations/engageUtils.js index 3b7777a45..3e1226616 100644 --- a/src/data/resolvers/mutations/engageUtils.js +++ b/src/data/resolvers/mutations/engageUtils.js @@ -27,9 +27,6 @@ const findCustomers = ({ customerIds, segmentId }) => { let customerQuery = { _id: { $in: customerIds || [] } }; // TODO - // if (segmentId) { - // customerQuery = customerQueryBuilder.segments(Segments.findOne(segmentId)); - // } return Customers.find(customerQuery).fetch(); }; @@ -108,31 +105,6 @@ const sendViaMessenger = message => { saveMatchedCustomerIds(message._id, customers); // TODO - // customers.forEach(customer => { - // // replace keys in content - // const replacedContent = replaceKeys({ content, customer, user }); - - // // create conversation - // const conversationId = createConversation({ - // userId: fromUserId, - // customerId: customer._id, - // integrationId: integration._id, - // content: replacedContent, - // }); - - // // create message - // createMessage({ - // engageData: { - // messageId: message._id, - // fromUserId, - // ...message.messenger, - // }, - // conversationId, - // userId: fromUserId, - // customerId: customer._id, - // content: replacedContent, - // }); - // }); }; export const send = message => { diff --git a/src/data/resolvers/mutations/engages.js b/src/data/resolvers/mutations/engages.js index 9bcb58fc9..4d3220859 100644 --- a/src/data/resolvers/mutations/engages.js +++ b/src/data/resolvers/mutations/engages.js @@ -16,10 +16,10 @@ export default { * @param {[String]} doc.tagIds * @return {Promise} message object */ - messagesAdd(root, doc, { user }) { + engageMessageAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); - return EngageMessages.createMessage(doc); + return EngageMessages.createEngageMessage(doc); }, /** @@ -37,12 +37,10 @@ export default { * @param {[String]} doc.tagIds * @return {Promise} message object */ - async messageEdit(root, { _id, ...doc }, { user }) { + async engageMessageUpdate(root, { _id, ...doc }, { user }) { if (!user) throw new Error('Login required'); - await EngageMessages.updateMessage(_id, doc); - - return EngageMessages.findOne({ _id }); + return EngageMessages.updateEngageMessage(_id, doc); }, /** @@ -50,14 +48,10 @@ export default { * @param {String} id * @return {Promise} null */ - async messagesRemove(root, _id, { user }) { + async engageMessageRemove(root, _id, { user }) { if (!user) throw new Error('Login required'); - const engageObj = await EngageMessages.findOne({ _id }); - - if (!engageObj) throw new Error(`Message not found with id ${_id}`); - - return engageObj.remove(); + return EngageMessages.removeEngageMessage(_id); }, /** @@ -65,12 +59,10 @@ export default { * @param {String} id * @return {Promise} message object */ - async messagesSetLive(root, _id, { user }) { + async engageMessageSetLive(root, _id, { user }) { if (!user) throw new Error('Login required'); - await EngageMessages.updateMessage(_id, { isLive: true, isDraft: false }); - - return EngageMessages.findOne({ _id }); + return EngageMessages.engageMessageSetLive(_id); }, /** @@ -78,12 +70,10 @@ export default { * @param {String} id * @return {Promise} message object */ - async messagesSetPause(root, _id, { user }) { + async engageMessageSetPause(root, _id, { user }) { if (!user) throw new Error('Login required'); - await EngageMessages.updateMessage(_id, { isLive: false }); - - return EngageMessages.findOne({ _id }); + return EngageMessages.engageMessageSetPause(_id); }, /** @@ -91,11 +81,9 @@ export default { * @param {String} id * @return {Promise} message object */ - async messagesSetLiveManual(root, _id, { user }) { + async engageMessageSetLiveManual(root, _id, { user }) { if (!user) throw new Error('Login required'); - await EngageMessages.updateMessage(_id, { isLive: true, isDraft: false }); - - return EngageMessages.findOne({ _id }); + return EngageMessages.engageMessageSetLive(_id); }, }; diff --git a/src/data/schema/engage.js b/src/data/schema/engage.js index d6643a98a..76e2d7f41 100644 --- a/src/data/schema/engage.js +++ b/src/data/schema/engage.js @@ -31,12 +31,12 @@ export const queries = ` `; export const mutations = ` - messagesAdd(title: String!, kind: String!, + engageMessageAdd(title: String!, kind: String!, segmentId: String!, method: String!, fromUserId: String!): EngageMessage - messageEdit(_id: String!, title: String!, kind: String!, + engageMessageUpdate(_id: String!, title: String!, kind: String!, segmentId: String!, method: String!, fromUserId: String!): EngageMessage - messagesRemove(ids: [String!]!): EngageMessage - messagesSetLive(_id: String!): EngageMessage - messagesSetPause(_id: String!): EngageMessage - messagesSetLiveManual(_id: String!): EngageMessage + engageMessageRemove(ids: [String!]!): EngageMessage + engageMessageSetLive(_id: String!): EngageMessage + engageMessageSetPause(_id: String!): EngageMessage + engageMessageSetLiveManual(_id: String!): EngageMessage `; diff --git a/src/db/factories.js b/src/db/factories.js index cad1fb23b..c881530e4 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -1,7 +1,15 @@ import faker from 'faker'; import Random from 'meteor-random'; -import { Users, Tags, Segments, Brands, EmailTemplates, ResponseTemplates } from './models'; +import { + Users, + Tags, + Segments, + Brands, + EmailTemplates, + ResponseTemplates, + EngageMessages, +} from './models'; export const userFactory = (params = {}) => { const user = new Users({ @@ -25,6 +33,19 @@ export const tagsFactory = (params = { type: 'engageMessage' }) => { return tag.save(); }; +export const engageMessageFactory = (params = {}) => { + const engageMessage = new EngageMessages({ + kind: 'manual', + title: faker.random.word(), + fromUserId: params.userId || faker.random.word(), + segmentId: params.segmentId || faker.random.word(), + isLive: true, + isDraft: false, + }); + + return engageMessage.save(); +}; + export const segmentsFactory = () => { const segment = new Segments({ name: faker.random.word(), diff --git a/src/db/models/Engages.js b/src/db/models/Engages.js index f7337f1ea..0c92b593a 100644 --- a/src/db/models/Engages.js +++ b/src/db/models/Engages.js @@ -63,7 +63,7 @@ class Message { * @param {Object} doc object * @return {Promise} Newly created message object */ - static createMessage(doc) { + static createEngageMessage(doc) { return this.create({ ...doc, deliveryReports: {}, @@ -72,12 +72,30 @@ class Message { }); } - static updateMessage(_id, doc) { - return this.update({ _id }, { $set: doc }); + static async updateEngageMessage(_id, doc) { + await this.update({ _id }, { $set: doc }); + + return this.findOne({ _id }); } - static removeMessage(_id) { - return this.remove({ _id }); + static async engageMessageSetLive(_id) { + await this.update({ _id }, { $set: { isLive: true, isDraft: false } }); + + return this.findOne({ _id }); + } + + static async engageMessageSetPause(_id) { + await this.update({ _id }, { $set: { isLive: false } }); + + return this.findOne({ _id }); + } + + static async removeEngageMessage(_id) { + const messageObj = await this.findOne({ _id }); + + if (!messageObj) throw new Error(`Engage message not found with id ${_id}`); + + return messageObj.remove(); } } diff --git a/test.config.json b/test.config.json deleted file mode 100644 index 0f7b36321..000000000 --- a/test.config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "testRegex": ".*test.js$", - "testEnvironment": "node" -} \ No newline at end of file From 60928c2847857cb7c83ebc921f26e34101afef2e Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Fri, 13 Oct 2017 22:25:30 +0800 Subject: [PATCH 059/318] Tag mutation name change --- src/data/schema/tag.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/schema/tag.js b/src/data/schema/tag.js index 23a451247..e5245ed13 100644 --- a/src/data/schema/tag.js +++ b/src/data/schema/tag.js @@ -15,7 +15,7 @@ export const queries = ` export const mutations = ` tagsAdd(name: String!, type: String!, colorCode: String): Tag - tagsEdit(_id: String!, name: String!, type: String!, colorCode: String): Tag + tagsUpdate(_id: String!, name: String!, type: String!, colorCode: String): Tag tagsRemove(ids: [String!]!): Tag tagsTag(type: String!, targetIds: [String!]!, tagIds: [String!]!): Tag `; From e1f364a707d1afe4f86d0a115ac8a0955ee7f9c6 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 14 Oct 2017 10:40:09 +0800 Subject: [PATCH 060/318] Add customersAdd mutation --- src/__tests__/customerMutations.test.js | 13 ++++++++++ src/data/resolvers/mutations/customers.js | 10 ++++++++ src/data/schema/customer.js | 1 + src/db/models/Customers.js | 30 +++++++---------------- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/__tests__/customerMutations.test.js b/src/__tests__/customerMutations.test.js index 50be8759a..eb9f92454 100644 --- a/src/__tests__/customerMutations.test.js +++ b/src/__tests__/customerMutations.test.js @@ -26,6 +26,19 @@ describe('Customers mutations', () => { await Users.remove({}); }); + test('Create customer', async () => { + // Login required + expect(() => customerMutations.customersAdd({}, {}, {})).toThrowError('Login required'); + + // valid + const doc = { name: 'name', email: 'dombo@yahoo.com' }; + + const customerObj = await customerMutations.customersAdd({}, doc, { user: _user }); + + expect(customerObj.name).toBe(doc.name); + expect(customerObj.email).toBe(doc.email); + }); + test('Edit customer login required', async () => { expect.assertions(1); diff --git a/src/data/resolvers/mutations/customers.js b/src/data/resolvers/mutations/customers.js index 1c6b19044..efd717edd 100644 --- a/src/data/resolvers/mutations/customers.js +++ b/src/data/resolvers/mutations/customers.js @@ -1,6 +1,16 @@ import { Customers } from '../../../db/models'; export default { + /** + * Create new customer + * @return {Promise} customer object + */ + customersAdd(root, doc, { user }) { + if (!user) throw new Error('Login required'); + + return Customers.createCustomer(doc); + }, + /** * Update customer * @return {Promise} customer object diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index cefc49886..01b8e8c6f 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -49,5 +49,6 @@ const fields = ` `; export const mutations = ` + customersAdd(${fields}): Customer customersEdit(_id: String!, ${fields}): Customer `; diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index d5c4b4b66..5d3dbcc45 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -74,26 +74,6 @@ const facebookSchema = mongoose.Schema( { _id: false }, ); -/* - * internal note schema - */ -const internalNoteSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - content: { - type: String, - }, - createdBy: { - type: String, - }, - createdDate: { - type: Date, - }, -}); - const CustomerSchema = mongoose.Schema({ _id: { type: String, @@ -112,13 +92,21 @@ const CustomerSchema = mongoose.Schema({ companyIds: [String], customFieldsData: Object, - internalNotes: [internalNoteSchema], messengerData: messengerSchema, twitterData: twitterSchema, facebookData: facebookSchema, }); class Customer { + /** + * Create a customer + * @param {Object} customerObj object + * @return {Promise} Newly created customer object + */ + static createCustomer(doc) { + return this.create(doc); + } + /* * Update customer * @param {String} _id customer id to update From cbeee2f60c7474106976f5355231012e4810100c Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 14 Oct 2017 15:29:52 +0800 Subject: [PATCH 061/318] Separate customer tests --- src/__tests__/customerDb.test.js | 73 +++++++++++++++++++++++ src/__tests__/customerMutations.test.js | 62 +++++++++++-------- src/data/resolvers/mutations/customers.js | 14 +++++ src/data/schema/company.js | 1 + src/data/schema/customer.js | 1 + src/db/factories.js | 1 + src/db/models/Companies.js | 2 +- src/db/models/Customers.js | 25 ++++++-- 8 files changed, 150 insertions(+), 29 deletions(-) create mode 100644 src/__tests__/customerDb.test.js diff --git a/src/__tests__/customerDb.test.js b/src/__tests__/customerDb.test.js new file mode 100644 index 000000000..71e70cf2a --- /dev/null +++ b/src/__tests__/customerDb.test.js @@ -0,0 +1,73 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Customers } from '../db/models'; +import { customerFactory } from '../db/factories'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Customers model tests', () => { + let _customer; + + beforeEach(async () => { + _customer = await customerFactory(); + }); + + afterEach(async () => { + // Clearing test data + await Customers.remove({}); + }); + + test('Create customer', async () => { + const doc = { name: 'name', email: 'dombo@yahoo.com' }; + + const customerObj = await Customers.createCustomer(doc); + + expect(customerObj.name).toBe(doc.name); + expect(customerObj.email).toBe(doc.email); + }); + + test('Update customer', async () => { + const doc = { + name: 'Dombo', + email: 'dombo@yahoo.com', + phone: '242442200', + }; + + const customerObj = await Customers.updateCustomer(_customer._id, doc); + + expect(customerObj.name).toBe(doc.name); + expect(customerObj.email).toBe(doc.email); + expect(customerObj.phone).toBe(doc.phone); + }); + + test('Mark customer as inactive', async () => { + const customer = await customerFactory({ + messengerData: { isActive: true, lastSeenAt: null }, + }); + + const customerObj = await Customers.markCustomerAsNotActive(customer._id); + + expect(customerObj.messengerData.isActive).toBe(false); + expect(customerObj.messengerData.lastSeenAt).toBeDefined(); + }); + + test('Add company', async () => { + let customer = await customerFactory({}); + + // call add company + const company = await Customers.addCompany({ + _id: customer._id, + name: 'name', + website: 'website', + }); + + customer = await Customers.findOne({ _id: customer._id }); + + expect(customer.companyIds.length).toBe(1); + expect(customer.companyIds[0]).toEqual([company._id]); + }); +}); diff --git a/src/__tests__/customerMutations.test.js b/src/__tests__/customerMutations.test.js index eb9f92454..27157c748 100644 --- a/src/__tests__/customerMutations.test.js +++ b/src/__tests__/customerMutations.test.js @@ -22,31 +22,39 @@ describe('Customers mutations', () => { afterEach(async () => { // Clearing test data - await Customers.remove({}); await Users.remove({}); + await Customers.remove({}); }); - test('Create customer', async () => { - // Login required - expect(() => customerMutations.customersAdd({}, {}, {})).toThrowError('Login required'); + test('Check login required', async () => { + expect.assertions(3); - // valid - const doc = { name: 'name', email: 'dombo@yahoo.com' }; + const check = async fn => { + try { + await fn({}, {}, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }; + + // add + check(customerMutations.customersAdd); - const customerObj = await customerMutations.customersAdd({}, doc, { user: _user }); + // edit + check(customerMutations.customersEdit); - expect(customerObj.name).toBe(doc.name); - expect(customerObj.email).toBe(doc.email); + // add company + check(customerMutations.customersAddCompany); }); - test('Edit customer login required', async () => { - expect.assertions(1); + test('Create customer', async () => { + Customers.createCustomer = jest.fn(); + + const doc = { name: 'name', email: 'dombo@yahoo.com' }; + + await customerMutations.customersAdd({}, doc, { user: _user }); - try { - await customerMutations.customersEdit({}, { _id: _customer.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + expect(Customers.createCustomer).toBeCalledWith(doc); }); test('Edit customer valid', async () => { @@ -56,14 +64,20 @@ describe('Customers mutations', () => { phone: '242442200', }; - const customerObj = await customerMutations.customersEdit( - {}, - { _id: _customer._id, ...doc }, - { user: _user }, - ); + Customers.updateCustomer = jest.fn(); + + await customerMutations.customersEdit({}, { _id: _customer._id, ...doc }, { user: _user }); + + expect(Customers.updateCustomer).toBeCalledWith(_customer._id, doc); + }); + + test('Add company', async () => { + Customers.addCompany = jest.fn(); + + const doc = { name: 'name', website: 'http://company.com' }; + + await customerMutations.customersAddCompany({}, doc, { user: _user }); - expect(customerObj.name).toBe(doc.name); - expect(customerObj.email).toBe(doc.email); - expect(customerObj.phone).toBe(doc.phone); + expect(Customers.addCompany).toBeCalledWith(doc); }); }); diff --git a/src/data/resolvers/mutations/customers.js b/src/data/resolvers/mutations/customers.js index efd717edd..7ba49f346 100644 --- a/src/data/resolvers/mutations/customers.js +++ b/src/data/resolvers/mutations/customers.js @@ -20,4 +20,18 @@ export default { return Customers.updateCustomer(_id, doc); }, + + /** + * Add new companyId to customer's companyIds list + * @param {Object} args - Graphql input data + * @param {String} args._id - Customer id + * @param {String} args.name - Company name + * @param {String} args.website - Company website + * @return {Promise} newly created customer + */ + async customersAddCompany(root, args, { user }) { + if (!user) throw new Error('Login required'); + + return Customers.addCompany(args); + }, }; diff --git a/src/data/schema/company.js b/src/data/schema/company.js index 06e666ff6..92da5f35d 100644 --- a/src/data/schema/company.js +++ b/src/data/schema/company.js @@ -46,4 +46,5 @@ export const mutations = ` companiesAdd(${commonFields}): Company companiesEdit(_id: String!, ${commonFields}): Company companiesRemove(_id: String!): Company + companiesAddCustomer(_id: String!, name: String!, email: String): Customer `; diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index 01b8e8c6f..d39355954 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -51,4 +51,5 @@ const fields = ` export const mutations = ` customersAdd(${fields}): Customer customersEdit(_id: String!, ${fields}): Customer + customersAddCompany(_id: String!, name: String!, website: String): Company `; diff --git a/src/db/factories.js b/src/db/factories.js index 1d4c3f381..5c2aeb166 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -115,6 +115,7 @@ export const customerFactory = (params = {}) => { name: params.name || faker.random.word(), email: params.email || faker.internet.email(), phone: params.phone || faker.random.word(), + messengerData: params.messengerData || {}, }); return customer.save(); diff --git a/src/db/models/Companies.js b/src/db/models/Companies.js index ba3e79be2..81a7449d9 100644 --- a/src/db/models/Companies.js +++ b/src/db/models/Companies.js @@ -11,7 +11,7 @@ const CompanySchema = mongoose.Schema({ name: { type: String, label: 'Name', - optional: true, + unique: true, }, size: { diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index 5d3dbcc45..dce4e950e 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -1,5 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +import { Companies } from './'; /* * messenger schema @@ -121,12 +122,12 @@ class Customer { /** * Mark customer as inactive - * @param {String} customerId + * @param {String} _id * @return {Promise} Updated customer */ - static markCustomerAsNotActive(customerId) { - return this.findByIdAndUpdate( - customerId, + static async markCustomerAsNotActive(_id) { + await this.findByIdAndUpdate( + _id, { $set: { 'messengerData.isActive': false, @@ -135,6 +136,22 @@ class Customer { }, { new: true }, ); + + return this.findOne({ _id }); + } + + /* + * Create new company and add to customer's company list + * @return {Promise} newly created company + */ + static async addCompany({ _id, name, website }) { + // create company + const company = await Companies.createCompany({ name, website }); + + // add to companyIds list + await this.findByIdAndUpdate(_id, { $addToSet: { companyIds: company._id } }); + + return company; } } From c4d7ec27e09d410186b9752c158a3322c8f53a84 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 14 Oct 2017 16:13:53 +0800 Subject: [PATCH 062/318] Separate company tests --- src/__tests__/companyDb.test.js | 71 ++++++++++++++++ src/__tests__/companyMutations.test.js | 98 +++++++++-------------- src/__tests__/customerDb.test.js | 3 +- src/data/resolvers/mutations/companies.js | 12 ++- src/data/schema/company.js | 1 - src/db/models/Companies.js | 19 ++--- 6 files changed, 128 insertions(+), 76 deletions(-) create mode 100644 src/__tests__/companyDb.test.js diff --git a/src/__tests__/companyDb.test.js b/src/__tests__/companyDb.test.js new file mode 100644 index 000000000..130f17ffb --- /dev/null +++ b/src/__tests__/companyDb.test.js @@ -0,0 +1,71 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Companies } from '../db/models'; +import { companyFactory } from '../db/factories'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +const check = (companyObj, doc) => { + expect(companyObj.name).toBe(doc.name); + expect(companyObj.email).toBe(doc.email); + expect(companyObj.website).toBe(doc.website); + expect(companyObj.size).toBe(doc.size); + expect(companyObj.industry).toBe(doc.industry); + expect(companyObj.plan).toBe(doc.plan); +}; + +const generateDoc = () => ({ + name: 'name', + website: 'http://company.com', + size: 1, + industry: 'industry', + plan: 'pro', +}); + +describe('Companies model tests', () => { + let _company; + + beforeEach(async () => { + _company = await companyFactory(); + }); + + afterEach(async () => { + // Clearing test data + await Companies.remove({}); + }); + + test('Create company', async () => { + const doc = generateDoc(); + + const companyObj = await Companies.createCompany(doc); + + check(companyObj, doc); + }); + + test('Update company', async () => { + const doc = generateDoc(); + + const companyObj = await Companies.updateCompany(_company._id, doc); + + check(companyObj, doc); + }); + + test('Add customer', async () => { + let company = await companyFactory({}); + + // call add customer + const customer = await Companies.addCustomer({ + _id: company._id, + name: 'name', + website: 'website', + }); + + company = await Companies.findOne({ _id: company._id }); + + expect(customer.companyIds).toEqual(expect.arrayContaining([company._id])); + }); +}); diff --git a/src/__tests__/companyMutations.test.js b/src/__tests__/companyMutations.test.js index 9b3a04806..be7b6762a 100644 --- a/src/__tests__/companyMutations.test.js +++ b/src/__tests__/companyMutations.test.js @@ -10,26 +10,6 @@ beforeAll(() => connect()); afterAll(() => disconnect()); -/* - * Generate test data - */ -const generateData = () => ({ - name: 'New company', - size: 10, - industry: 'Mining', - website: 'https://www.mining.com', -}); - -/* - * Check values - */ -const checkValues = (companyObj, doc) => { - expect(companyObj.name).toBe(doc.name); - expect(companyObj.size).toBe(doc.size); - expect(companyObj.industry).toBe(doc.industry); - expect(companyObj.website).toBe(doc.website); -}; - describe('Companies mutations', () => { let _user; let _company; @@ -42,64 +22,62 @@ describe('Companies mutations', () => { afterEach(async () => { // Clearing test data - await Companies.remove({}); await Users.remove({}); + await Companies.remove({}); }); - test('Create company', async () => { - // Login required - expect(() => companyMutations.companiesAdd({}, {}, {})).toThrowError('Login required'); + test('Check login required', async () => { + expect.assertions(3); + + const check = async fn => { + try { + await fn({}, {}, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }; - // valid - const data = generateData(); + // add + check(companyMutations.companiesAdd); - const companyObj = await companyMutations.companiesAdd({}, data, { user: _user }); + // edit + check(companyMutations.companiesEdit); - checkValues(companyObj, data); + // add company + check(companyMutations.companiesAddCustomer); }); - test('Edit company login required', async () => { - expect.assertions(1); + test('Create company', async () => { + Companies.createCompany = jest.fn(); + + const doc = { name: 'name', email: 'dombo@yahoo.com' }; + + await companyMutations.companiesAdd({}, doc, { user: _user }); - try { - await companyMutations.companiesEdit({}, { _id: _company.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + expect(Companies.createCompany).toBeCalledWith(doc); }); test('Edit company valid', async () => { - const data = generateData(); + const doc = { + name: 'Dombo', + email: 'dombo@yahoo.com', + phone: '242442200', + }; - const companyObj = await companyMutations.companiesEdit( - {}, - { _id: _company._id, ...data }, - { user: _user }, - ); + Companies.updateCompany = jest.fn(); - checkValues(companyObj, data); - }); - - test('Remove company login required', async () => { - expect.assertions(1); + await companyMutations.companiesEdit({}, { _id: _company._id, ...doc }, { user: _user }); - try { - await companyMutations.companiesRemove({}, { _id: _company.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + expect(Companies.updateCompany).toBeCalledWith(_company._id, doc); }); - test('Remove company valid', async () => { - const companyDeletedObj = await companyMutations.companiesRemove( - {}, - { _id: _company.id }, - { user: _user }, - ); + test('Add customer', async () => { + Companies.addCustomer = jest.fn(); + + const doc = { name: 'name', email: 'name@gmail.com' }; - expect(companyDeletedObj.id).toBe(_company.id); + await companyMutations.companiesAddCustomer({}, doc, { user: _user }); - const companyObj = await Companies.findOne({ _id: _company.id }); - expect(companyObj).toBeNull(); + expect(Companies.addCustomer).toBeCalledWith(doc); }); }); diff --git a/src/__tests__/customerDb.test.js b/src/__tests__/customerDb.test.js index 71e70cf2a..e2dabca2d 100644 --- a/src/__tests__/customerDb.test.js +++ b/src/__tests__/customerDb.test.js @@ -67,7 +67,6 @@ describe('Customers model tests', () => { customer = await Customers.findOne({ _id: customer._id }); - expect(customer.companyIds.length).toBe(1); - expect(customer.companyIds[0]).toEqual([company._id]); + expect(customer.companyIds).toEqual(expect.arrayContaining([company._id])); }); }); diff --git a/src/data/resolvers/mutations/companies.js b/src/data/resolvers/mutations/companies.js index 97149b3ea..cbbe048c1 100644 --- a/src/data/resolvers/mutations/companies.js +++ b/src/data/resolvers/mutations/companies.js @@ -22,12 +22,16 @@ export default { }, /** - * Delete company - * @return {Promise} + * Add new companyId to company's companyIds list + * @param {Object} args - Graphql input data + * @param {String} args._id - Customer id + * @param {String} args.name - Customer name + * @param {String} args.email - Customer email + * @return {Promise} newly created customer */ - async companiesRemove(root, { _id }, { user }) { + async companiesAddCustomer(root, args, { user }) { if (!user) throw new Error('Login required'); - return Companies.removeCompany(_id); + return Companies.addCustomer(args); }, }; diff --git a/src/data/schema/company.js b/src/data/schema/company.js index 92da5f35d..c9704892b 100644 --- a/src/data/schema/company.js +++ b/src/data/schema/company.js @@ -45,6 +45,5 @@ const commonFields = ` export const mutations = ` companiesAdd(${commonFields}): Company companiesEdit(_id: String!, ${commonFields}): Company - companiesRemove(_id: String!): Company companiesAddCustomer(_id: String!, name: String!, email: String): Customer `; diff --git a/src/db/models/Companies.js b/src/db/models/Companies.js index 81a7449d9..b0dba51a6 100644 --- a/src/db/models/Companies.js +++ b/src/db/models/Companies.js @@ -1,5 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +import { Customers } from './'; const CompanySchema = mongoose.Schema({ _id: { @@ -79,16 +80,16 @@ class Company { } /* - * Remove company - * @param {String} _id company id to remove - * @return {Promise} + * Create new customer and add to customer's customer list + * @return {Promise} newly created customer */ - static async removeCompany(_id) { - const companyObj = await this.findOne({ _id }); - - if (!companyObj) throw new Error(`Company not found with id ${_id}`); - - return companyObj.remove(); + static async addCustomer({ _id, name, email }) { + // create customer + return await Customers.createCustomer({ + name, + email, + companyIds: [_id], + }); } } From 75f7b2c86959073bcd5dcb2caa061fd1c312b8f2 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 14 Oct 2017 16:40:34 +0800 Subject: [PATCH 063/318] Separate segment tests --- src/__tests__/segmentDb.test.js | 94 ++++++++++++++++++++++++++ src/__tests__/segmentMutations.test.js | 86 ++++++++--------------- 2 files changed, 123 insertions(+), 57 deletions(-) create mode 100644 src/__tests__/segmentDb.test.js diff --git a/src/__tests__/segmentDb.test.js b/src/__tests__/segmentDb.test.js new file mode 100644 index 000000000..514b2c073 --- /dev/null +++ b/src/__tests__/segmentDb.test.js @@ -0,0 +1,94 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Segments, Users } from '../db/models'; +import { segmentFactory } from '../db/factories'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +/* + * Generate test data + */ +const generateData = () => ({ + name: 'New users', + description: 'New users', + subOf: 'DFSAFDSAFDFFFD', + color: '#fdfdfd', + connector: 'any', + conditions: [ + { + field: 'messengerData.sessionCount', + operator: 'e', + value: '10', + dateUnit: 'days', + type: 'string', + }, + ], +}); + +/* + * Check values + */ +const checkValues = (segmentObj, doc) => { + expect(segmentObj.name).toBe(doc.name); + expect(segmentObj.description).toBe(doc.description); + expect(segmentObj.subOf).toBe(doc.subOf); + expect(segmentObj.color).toBe(doc.color); + expect(segmentObj.connector).toBe(doc.connector); + + expect(segmentObj.conditions.field).toEqual(doc.conditions.field); + expect(segmentObj.conditions.operator).toEqual(doc.conditions.operator); + expect(segmentObj.conditions.value).toEqual(doc.conditions.value); + expect(segmentObj.conditions.dateUnit).toEqual(doc.conditions.dateUnit); + expect(segmentObj.conditions.type).toEqual(doc.conditions.type); +}; + +describe('Segments mutations', () => { + let _segment; + + beforeEach(async () => { + // Creating test data + _segment = await segmentFactory(); + }); + + afterEach(async () => { + // Clearing test data + await Segments.remove({}); + await Users.remove({}); + }); + + test('Create segment', async () => { + // valid + const data = generateData(); + + const segmentObj = await Segments.createSegment(data); + + checkValues(segmentObj, data); + }); + + test('Update segment valid', async () => { + const data = generateData(); + + const segmentObj = await Segments.updateSegment(_segment._id, data); + + checkValues(segmentObj, data); + }); + + test('Remove segment valid', async () => { + try { + await Segments.removeSegment('DFFFDSFD'); + } catch (e) { + expect(e.message).toBe('Segment not found with id DFFFDSFD'); + } + + const segmentDeletedObj = await Segments.removeSegment({ _id: _segment.id }); + + expect(segmentDeletedObj.id).toBe(_segment.id); + + const segmentObj = await Segments.findOne({ _id: _segment.id }); + expect(segmentObj).toBeNull(); + }); +}); diff --git a/src/__tests__/segmentMutations.test.js b/src/__tests__/segmentMutations.test.js index 40d45dec1..9b5fe43e4 100644 --- a/src/__tests__/segmentMutations.test.js +++ b/src/__tests__/segmentMutations.test.js @@ -13,7 +13,7 @@ afterAll(() => disconnect()); /* * Generate test data */ -const generateData = () => ({ +const doc = { name: 'New users', description: 'New users', subOf: 'DFSAFDSAFDFFFD', @@ -28,23 +28,6 @@ const generateData = () => ({ type: 'string', }, ], -}); - -/* - * Check values - */ -const checkValues = (segmentObj, doc) => { - expect(segmentObj.name).toBe(doc.name); - expect(segmentObj.description).toBe(doc.description); - expect(segmentObj.subOf).toBe(doc.subOf); - expect(segmentObj.color).toBe(doc.color); - expect(segmentObj.connector).toBe(doc.connector); - - expect(segmentObj.conditions.field).toEqual(doc.conditions.field); - expect(segmentObj.conditions.operator).toEqual(doc.conditions.operator); - expect(segmentObj.conditions.value).toEqual(doc.conditions.value); - expect(segmentObj.conditions.dateUnit).toEqual(doc.conditions.dateUnit); - expect(segmentObj.conditions.type).toEqual(doc.conditions.type); }; describe('Segments mutations', () => { @@ -63,59 +46,48 @@ describe('Segments mutations', () => { await Users.remove({}); }); - test('Create segment', async () => { - // Login required - expect(() => segmentMutations.segmentsAdd({}, {}, {})).toThrowError('Login required'); + test('Check login required', async () => { + expect.assertions(3); + + const check = async fn => { + try { + await fn({}, {}, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }; - // valid - const data = generateData(); + // add + check(segmentMutations.segmentsAdd); - const segmentObj = await segmentMutations.segmentsAdd({}, data, { user: _user }); + // edit + check(segmentMutations.segmentsEdit); - checkValues(segmentObj, data); + // add company + check(segmentMutations.segmentsRemove); }); - test('Edit segment login required', async () => { - expect.assertions(1); + test('Create segment', async () => { + Segments.createSegment = jest.fn(); + + await segmentMutations.segmentsAdd({}, doc, { user: _user }); - try { - await segmentMutations.segmentsEdit({}, { _id: _segment.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + expect(Segments.createSegment).toBeCalledWith(doc); }); test('Edit segment valid', async () => { - const data = generateData(); + Segments.updateSegment = jest.fn(); - const segmentObj = await segmentMutations.segmentsEdit( - {}, - { _id: _segment._id, ...data }, - { user: _user }, - ); + await segmentMutations.segmentsEdit({}, { _id: _segment._id, ...doc }, { user: _user }); - checkValues(segmentObj, data); + expect(Segments.updateSegment).toBeCalledWith(_segment._id, doc); }); - test('Remove segment login required', async () => { - expect.assertions(1); + test('Remove segment valid', async () => { + Segments.removeSegment = jest.fn(); - try { - await segmentMutations.segmentsRemove({}, { _id: _segment.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } - }); + await segmentMutations.segmentsRemove({}, { _id: _segment.id }, { user: _user }); - test('Remove segment valid', async () => { - const segmentDeletedObj = await segmentMutations.segmentsRemove( - {}, - { _id: _segment.id }, - { user: _user }, - ); - expect(segmentDeletedObj.id).toBe(_segment.id); - - const segmentObj = await Segments.findOne({ _id: _segment.id }); - expect(segmentObj).toBeNull(); + expect(Segments.removeSegment).toBeCalledWith(_segment.id); }); }); From 2e8a915cf257fbdae7fa0afaf6e63f89d84b2d56 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 14 Oct 2017 16:46:57 +0800 Subject: [PATCH 064/318] Refactor field mutation tests --- src/__tests__/fieldMutations.test.js | 80 ++++++++++------------------ 1 file changed, 28 insertions(+), 52 deletions(-) diff --git a/src/__tests__/fieldMutations.test.js b/src/__tests__/fieldMutations.test.js index b4ccf2159..8bb253fc7 100644 --- a/src/__tests__/fieldMutations.test.js +++ b/src/__tests__/fieldMutations.test.js @@ -14,25 +14,13 @@ afterAll(() => disconnect()); /* * Generate test data */ -const generateData = () => ({ +const doc = { type: 'input', validation: 'number', text: faker.random.word(), description: faker.random.word(), isRequired: false, order: 0, -}); - -/* - * Check values - */ -const checkValues = (fieldObj, doc) => { - expect(fieldObj.type).toBe(doc.type); - expect(fieldObj.validation).toBe(doc.validation); - expect(fieldObj.text).toBe(doc.text); - expect(fieldObj.description).toBe(doc.description); - expect(fieldObj.isRequired).toBe(doc.isRequired); - expect(fieldObj.order).toBe(doc.order); }; describe('Fields mutations', () => { @@ -51,60 +39,48 @@ describe('Fields mutations', () => { await Users.remove({}); }); - test('Create field', async () => { - // Login required - expect(() => fieldMutations.fieldsAdd({}, {}, {})).toThrowError('Login required'); + test('Check login required', async () => { + expect.assertions(3); - // valid - const doc = generateData(); + const check = async fn => { + try { + await fn({}, {}, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }; - const fieldObj = await fieldMutations.fieldsAdd({}, doc, { user: _user }); + // add + check(fieldMutations.fieldsAdd); - checkValues(fieldObj, doc); - }); - - test('Edit field login required', async () => { - expect.assertions(1); + // edit + check(fieldMutations.fieldsEdit); - try { - await fieldMutations.fieldsEdit({}, { _id: _field.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + // add company + check(fieldMutations.fieldsRemove); }); - test('Edit field valid', async () => { - const doc = generateData(); + test('Create field', async () => { + Fields.createField = jest.fn(); - const fieldObj = await fieldMutations.fieldsEdit( - {}, - { _id: _field._id, ...doc }, - { user: _user }, - ); + await fieldMutations.fieldsAdd({}, doc, { user: _user }); - checkValues(fieldObj, doc); + expect(Fields.createField).toBeCalledWith(doc); }); - test('Remove field login required', async () => { - expect.assertions(1); + test('Update field valid', async () => { + Fields.updateField = jest.fn(); + + await fieldMutations.fieldsEdit({}, { _id: _field._id, ...doc }, { user: _user }); - try { - await fieldMutations.fieldsRemove({}, { _id: _field.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + expect(Fields.updateField).toBeCalledWith(_field._id, doc); }); test('Remove field valid', async () => { - const fieldDeletedObj = await fieldMutations.fieldsRemove( - {}, - { _id: _field.id }, - { user: _user }, - ); + Fields.removeField = jest.fn(); - expect(fieldDeletedObj.id).toBe(_field.id); + await fieldMutations.fieldsRemove({}, { _id: _field.id }, { user: _user }); - const fieldObj = await Fields.findOne({ _id: _field.id }); - expect(fieldObj).toBeNull(); + expect(Fields.removeField).toBeCalledWith(_field._id); }); }); From 8bc683044ee994c6e77f2b5937a79a447d138d69 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 14 Oct 2017 17:04:24 +0800 Subject: [PATCH 065/318] Add fieldUpdate, fieldRemove tests --- src/__tests__/fieldDb.test.js | 38 ++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/__tests__/fieldDb.test.js b/src/__tests__/fieldDb.test.js index f2695579d..4b71e2ffa 100644 --- a/src/__tests__/fieldDb.test.js +++ b/src/__tests__/fieldDb.test.js @@ -13,9 +13,11 @@ afterAll(() => disconnect()); * Field related tests */ describe('Fields', () => { + let _field; + beforeEach(async () => { // creating field with contentType other than customer - await fieldFactory({ contentType: 'form', order: 1 }); + _field = await fieldFactory({ contentType: 'form', order: 1 }); }); afterEach(() => { @@ -87,4 +89,38 @@ describe('Fields', () => { expect(updatedField1.order).toBe(10); expect(updatedField2.order).toBe(11); }); + + test('Update field valid', async () => { + const doc = await fieldFactory(); + + doc._id = undefined; + + const fieldObj = await Fields.updateField(_field._id, doc); + + // check updates + expect(fieldObj.contentType).toBe(doc.contentType); + expect(fieldObj.contentTypeId).toBe(doc.contentTypeId); + expect(fieldObj.type).toBe(doc.type); + expect(fieldObj.validation).toBe(doc.validation); + expect(fieldObj.text).toBe(doc.text); + expect(fieldObj.description).toBe(doc.description); + expect(fieldObj.options).toEqual(expect.arrayContaining(doc.options)); + expect(fieldObj.isRequired).toBe(doc.isRequired); + expect(fieldObj.order).toBe(doc.order); + }); + + test('Remove field valid', async () => { + try { + await Fields.removeField('DFFFDSFD'); + } catch (e) { + expect(e.message).toBe('Field not found with id DFFFDSFD'); + } + + const fieldDeletedObj = await Fields.removeField({ _id: _field.id }); + + expect(fieldDeletedObj.id).toBe(_field.id); + + const fieldObj = await Fields.findOne({ _id: _field.id }); + expect(fieldObj).toBeNull(); + }); }); From 648da716a47a26e4856b663728b39a6a707a8f48 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 14 Oct 2017 17:10:33 +0800 Subject: [PATCH 066/318] Add fieldsUpdate order mutation test --- src/__tests__/fieldMutations.test.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/__tests__/fieldMutations.test.js b/src/__tests__/fieldMutations.test.js index 8bb253fc7..bfcd19d75 100644 --- a/src/__tests__/fieldMutations.test.js +++ b/src/__tests__/fieldMutations.test.js @@ -40,7 +40,7 @@ describe('Fields mutations', () => { }); test('Check login required', async () => { - expect.assertions(3); + expect.assertions(4); const check = async fn => { try { @@ -58,6 +58,9 @@ describe('Fields mutations', () => { // add company check(fieldMutations.fieldsRemove); + + // update order + check(fieldMutations.fieldsUpdateOrder); }); test('Create field', async () => { @@ -83,4 +86,14 @@ describe('Fields mutations', () => { expect(Fields.removeField).toBeCalledWith(_field._id); }); + + test('Update order', async () => { + Fields.updateOrder = jest.fn(); + + const orders = [{ _id: 'DFADF', order: 1 }]; + + await fieldMutations.fieldsUpdateOrder({}, { _id: _field._id, orders }, { user: _user }); + + expect(Fields.updateOrder).toBeCalledWith(orders); + }); }); From f32240c66c9cb503f3ad62b3fe282aa2b21ae3de Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 14 Oct 2017 20:40:41 +0800 Subject: [PATCH 067/318] #13 email test --- package.json | 1 + private/emailTemplates/base.html | 164 ++++++++++++ private/emailTemplates/conversationCron.html | 242 ++++++++++++++++++ .../emailTemplates/conversationDetail.html | 20 ++ private/emailTemplates/invitation.html | 18 ++ private/emailTemplates/notification.html | 30 +++ src/__tests__/toolsSendEmail.test.js | 42 +++ src/data/utils.js | 46 +++- src/db/factories.js | 16 ++ yarn.lock | 6 + 10 files changed, 575 insertions(+), 10 deletions(-) create mode 100644 private/emailTemplates/base.html create mode 100644 private/emailTemplates/conversationCron.html create mode 100644 private/emailTemplates/conversationDetail.html create mode 100644 private/emailTemplates/invitation.html create mode 100644 private/emailTemplates/notification.html create mode 100644 src/__tests__/toolsSendEmail.test.js diff --git a/package.json b/package.json index 123aebf3f..a91d9a18c 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "cors": "^2.8.1", "dotenv": "^4.0.0", "express": "^4.15.2", + "fs-readfile-promise": "^3.0.0", "graphql": "^0.10.1", "graphql-server-core": "^0.8.2", "graphql-server-express": "^0.8.2", diff --git a/private/emailTemplates/base.html b/private/emailTemplates/base.html new file mode 100644 index 000000000..56a8def36 --- /dev/null +++ b/private/emailTemplates/base.html @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + +
+
+ + + + + + +
+
+ + + + + + +
+ + + + + + +
+
+
+
+
+
+ + + + + + +
{{{ content }}}
+
+ +
+ + + + + + +
+ +
+
+ +
+ + + + + + +
+
+ + + + + + +
+
+

+ {{{ signature }}} +

+
+
+
+
+
+
+ + + diff --git a/private/emailTemplates/conversationCron.html b/private/emailTemplates/conversationCron.html new file mode 100644 index 000000000..d266abd93 --- /dev/null +++ b/private/emailTemplates/conversationCron.html @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ {{brand.name}} +
+
+
+ +
+
+ + +
+ + + + + + +
+ +
+ + + + + +
+ + + + + +
+ + + + + + +
+

{{{question.content}}}

+
+ + + + + + + + +
+ {{customer.name}} + {{question.createdAt}}
+
+ {{#each answers}} + + + + + +
+ + + + +
+

{{{content}}}

+
+ + + + + + + + + + +
{{createdAt}} + {{user.details.fullName}} +
+
+ +
+ {{/each}} +
+
+ +
+
+ + +
+ + + + + + +
+ + + +
+
+ +
+ + + diff --git a/private/emailTemplates/conversationDetail.html b/private/emailTemplates/conversationDetail.html new file mode 100644 index 000000000..fa468c575 --- /dev/null +++ b/private/emailTemplates/conversationDetail.html @@ -0,0 +1,20 @@ + + + + + + +
+
+

+ {{ conversationDetail.title }} +
+

+

+ {{#each conversationDetail.messages}} +

{{{ content }}}

+ {{/each}} +

+

{{conversationDetail.date}}

+
+
diff --git a/private/emailTemplates/invitation.html b/private/emailTemplates/invitation.html new file mode 100644 index 000000000..5640aaefb --- /dev/null +++ b/private/emailTemplates/invitation.html @@ -0,0 +1,18 @@ + + + + + + +
+
+

+ Username: {{ username }} +
+

+

+ Password: {{ password }} +
+

+
+
diff --git a/private/emailTemplates/notification.html b/private/emailTemplates/notification.html new file mode 100644 index 000000000..d55833e79 --- /dev/null +++ b/private/emailTemplates/notification.html @@ -0,0 +1,30 @@ + + + + + + +
+
+

+ {{ notification.title }} +
+

+

+ {{{ notification.content }}} +

+

{{notification.date}}

+ + + + + + + +
+ + View notification + +
+
+
diff --git a/src/__tests__/toolsSendEmail.test.js b/src/__tests__/toolsSendEmail.test.js new file mode 100644 index 000000000..ce18825c5 --- /dev/null +++ b/src/__tests__/toolsSendEmail.test.js @@ -0,0 +1,42 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { sendEmail } from '../data/utils'; +import mailer from 'nodemailer/lib/mailer'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('data/tools/sendEmail', () => { + beforeEach(() => {}); + + afterEach(() => {}); + + test('check whether email is being sent successfully', async () => { + const doc = { + test: 'Nofiticaton test', + content: 'Notification content', + date: new Date(), + link: 'https://www.google.com', + }; + + mailer.sendMail = jest.fn(); + + await sendEmail({ + toEmails: ['javkhlan.sh@gmail.com'], + fromEmail: 'test@erxes.io', + title: 'Notification', + template: { + name: 'notification', + data: { + notification: doc, + }, + }, + }); + + expect(mailer.sendMail).toBeCalledWith(doc); + }); +}); + +jest.fn(() => ({ _id: 'fdfdf' })); diff --git a/src/data/utils.js b/src/data/utils.js index 2a85cc4ec..867e5a470 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -1,7 +1,25 @@ import nodemailer from 'nodemailer'; +import Handlebars from 'handlebars'; +import readFile from 'fs-readfile-promise'; import { MODULES } from './constants'; import { Channels, Notifications, Users } from '../db/models'; +/** + * SendEmail template helper + * @param {Object} data data + * @param {String} templateName + * @return email with template as text + */ +const applyTemplate = async (data, templateName) => { + const emailTemplatePath = `${__dirname}/../../private/emailTemplates/${templateName}.html`; + + let template = await readFile(emailTemplatePath); + + template = Handlebars.compile(template.toString()); + + return template(data); +}; + /** * Send email * @param {Array} args.toEmails @@ -10,14 +28,13 @@ import { Channels, Notifications, Users } from '../db/models'; * @param {String} args.templateArgs.name * @param {Object} args.templateArgs.data * @param {Boolean} args.isCustom - * @return {Promise} null + * @return {Promise} */ -export const sendEmail = async ({ toEmails, fromEmail, title, templateArgs }) => { - // TODO: test this method +export const sendEmail = async ({ toEmails, fromEmail, title, template }) => { const { MAIL_SERVICE, MAIL_USER, MAIL_PASS, NODE_ENV } = process.env; + // do not send email it is running in test mode const isTest = NODE_ENV == 'test'; - // do not send email it is running in test mode if (isTest) { return; } @@ -30,20 +47,29 @@ export const sendEmail = async ({ toEmails, fromEmail, title, templateArgs }) => }, }); - const { data } = templateArgs; + const { isCustom, data, name } = template; + + // generate email content by given template + let html = await applyTemplate(data, name); + + if (!isCustom) { + html = await applyTemplate({ html }, 'base'); + } return toEmails.map(toEmail => { const mailOptions = { from: fromEmail, to: toEmail, subject: title, - text: data, + html, + mail_settings: { + sandbox_mode: { + enable: true, + }, + }, }; - return transporter.sendMail(mailOptions, (error, info) => { - console.log(error); // eslint-disable-line - console.log(info); // eslint-disable-line - }); + return transporter.sendMail(mailOptions); }); }; diff --git a/src/db/factories.js b/src/db/factories.js index 31a400315..138d67a30 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -136,3 +136,19 @@ export const notificationFactory = params => { receiver: params.receiver || userFactory({}), }); }; + +export function messageFactory(params = {}) { + const obj = Object.assign( + { + userId: Random.id(), + conversationId: Random.id(), + customerId: Random.id(), + content: faker.lorem.sentence, + createdAt: faker.date.past(), + }, + params, + ); + const message = new Messages(obj); + + return message.save(); +} diff --git a/yarn.lock b/yarn.lock index 0a1aa0fa7..5bfef9bd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1776,6 +1776,12 @@ fs-readdir-recursive@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" +fs-readfile-promise@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fs-readfile-promise/-/fs-readfile-promise-3.0.0.tgz#8f66593bc196e4b6c16f4a156c4fcd7cc31cafd3" + dependencies: + graceful-fs "^4.1.2" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" From deb3f8a221395fe82fad3d1d448db7c2925d005b Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 14 Oct 2017 20:53:04 +0800 Subject: [PATCH 068/318] #13 Added sendMail --- src/__tests__/toolsSendEmail.test.js | 7 ------- src/data/utils.js | 18 ++++++++---------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/__tests__/toolsSendEmail.test.js b/src/__tests__/toolsSendEmail.test.js index ce18825c5..a539ae45b 100644 --- a/src/__tests__/toolsSendEmail.test.js +++ b/src/__tests__/toolsSendEmail.test.js @@ -3,7 +3,6 @@ import { connect, disconnect } from '../db/connection'; import { sendEmail } from '../data/utils'; -import mailer from 'nodemailer/lib/mailer'; beforeAll(() => connect()); afterAll(() => disconnect()); @@ -21,8 +20,6 @@ describe('data/tools/sendEmail', () => { link: 'https://www.google.com', }; - mailer.sendMail = jest.fn(); - await sendEmail({ toEmails: ['javkhlan.sh@gmail.com'], fromEmail: 'test@erxes.io', @@ -34,9 +31,5 @@ describe('data/tools/sendEmail', () => { }, }, }); - - expect(mailer.sendMail).toBeCalledWith(doc); }); }); - -jest.fn(() => ({ _id: 'fdfdf' })); diff --git a/src/data/utils.js b/src/data/utils.js index 867e5a470..fd9bc5bee 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -35,9 +35,9 @@ export const sendEmail = async ({ toEmails, fromEmail, title, template }) => { // do not send email it is running in test mode const isTest = NODE_ENV == 'test'; - if (isTest) { - return; - } + // if (isTest) { + // return; + // } const transporter = nodemailer.createTransport({ service: MAIL_SERVICE, @@ -53,7 +53,7 @@ export const sendEmail = async ({ toEmails, fromEmail, title, template }) => { let html = await applyTemplate(data, name); if (!isCustom) { - html = await applyTemplate({ html }, 'base'); + html = await applyTemplate({ content: html }, 'base'); } return toEmails.map(toEmail => { @@ -62,14 +62,12 @@ export const sendEmail = async ({ toEmails, fromEmail, title, template }) => { to: toEmail, subject: title, html, - mail_settings: { - sandbox_mode: { - enable: true, - }, - }, }; - return transporter.sendMail(mailOptions); + return transporter.sendMail(mailOptions, (error, info) => { + console.log(error); // eslint-disable-line + console.log(info); // eslint-disable-line + }); }); }; From 03e4d1598ff8ae77bbe9bb1d5f62f6ced206b72c Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 15 Oct 2017 10:28:55 +0800 Subject: [PATCH 069/318] Add contentType on segment --- src/__tests__/segmentDb.test.js | 2 ++ src/constants.js | 6 ++++++ src/data/resolvers/queries/segments.js | 4 ++-- src/data/schema/segment.js | 6 ++++-- src/db/models/Segments.js | 5 +++++ 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/__tests__/segmentDb.test.js b/src/__tests__/segmentDb.test.js index 514b2c073..c5b93dc68 100644 --- a/src/__tests__/segmentDb.test.js +++ b/src/__tests__/segmentDb.test.js @@ -13,6 +13,7 @@ afterAll(() => disconnect()); * Generate test data */ const generateData = () => ({ + contentType: 'customer', name: 'New users', description: 'New users', subOf: 'DFSAFDSAFDFFFD', @@ -33,6 +34,7 @@ const generateData = () => ({ * Check values */ const checkValues = (segmentObj, doc) => { + expect(segmentObj.contentType).toBe(doc.contentType); expect(segmentObj.name).toBe(doc.name); expect(segmentObj.description).toBe(doc.description); expect(segmentObj.subOf).toBe(doc.subOf); diff --git a/src/constants.js b/src/constants.js index 54f8a597a..696787e82 100644 --- a/src/constants.js +++ b/src/constants.js @@ -10,3 +10,9 @@ export const INTERNAL_NOTE_CONTENT_TYPES = { COMPANY: 'company', ALL_LIST: ['customer', 'company'], }; + +export const SEGMENT_CONTENT_TYPES = { + CUSTOMER: 'customer', + COMPANY: 'company', + ALL_LIST: ['customer', 'company'], +}; diff --git a/src/data/resolvers/queries/segments.js b/src/data/resolvers/queries/segments.js index 33368c131..eb4ba9382 100644 --- a/src/data/resolvers/queries/segments.js +++ b/src/data/resolvers/queries/segments.js @@ -5,8 +5,8 @@ export default { * Segments list * @return {Promise} segment objects */ - segments() { - return Segments.find({}); + segments(root, { contentType }) { + return Segments.find({ contentType }); }, /** diff --git a/src/data/schema/segment.js b/src/data/schema/segment.js index 6d8d8ef50..c661d2bdc 100644 --- a/src/data/schema/segment.js +++ b/src/data/schema/segment.js @@ -9,7 +9,8 @@ export const types = ` type Segment { _id: String! - name: String + contentType: String! + name: String! description: String subOf: String color: String @@ -22,12 +23,13 @@ export const types = ` `; export const queries = ` - segments: [Segment] + segments(contentType: String!): [Segment] segmentDetail(_id: String): Segment segmentsGetHeads: [Segment] `; const commonFields = ` + contentType: String!, name: String!, description: String, subOf: String, diff --git a/src/db/models/Segments.js b/src/db/models/Segments.js index dc9d5807e..73aed2219 100644 --- a/src/db/models/Segments.js +++ b/src/db/models/Segments.js @@ -1,5 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +import { SEGMENT_CONTENT_TYPES } from '../../constants'; const ConditionSchema = mongoose.Schema( { @@ -26,6 +27,10 @@ const SegmentSchema = mongoose.Schema({ unique: true, default: () => Random.id(), }, + contentType: { + type: String, + enum: SEGMENT_CONTENT_TYPES.ALL_LIST, + }, name: String, description: String, subOf: String, From 76ea02b2ddbf05fa9c835c6e8762092747c23588 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 15 Oct 2017 13:46:43 +0800 Subject: [PATCH 070/318] Add validate method on Field --- package.json | 3 +- src/__tests__/fieldDb.test.js | 66 +++++++++++++++++++++++++++++++++++ src/data/schema/segment.js | 5 ++- src/db/models/Fields.js | 44 +++++++++++++++++++++++ yarn.lock | 4 +++ 5 files changed, 118 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 123aebf3f..84256d3aa 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "passport-anonymous": "^1.0.1", "passport-http-bearer": "^1.0.1", "subscriptions-transport-ws": "^0.7.3", - "underscore": "^1.8.3" + "underscore": "^1.8.3", + "validator": "^9.0.0" }, "devDependencies": { "babel-cli": "^6.24.0", diff --git a/src/__tests__/fieldDb.test.js b/src/__tests__/fieldDb.test.js index 4b71e2ffa..e7d8a39e6 100644 --- a/src/__tests__/fieldDb.test.js +++ b/src/__tests__/fieldDb.test.js @@ -110,6 +110,8 @@ describe('Fields', () => { }); test('Remove field valid', async () => { + expect.assertions(3); + try { await Fields.removeField('DFFFDSFD'); } catch (e) { @@ -123,4 +125,68 @@ describe('Fields', () => { const fieldObj = await Fields.findOne({ _id: _field.id }); expect(fieldObj).toBeNull(); }); + + test('Validate submission: invalid values', async () => { + expect.assertions(4); + + const expectError = async (message, value) => { + try { + await Fields.validate({ _id: _field._id, value }); + } catch (e) { + expect(e.message).toBe(`${_field.text}: ${message}`); + } + }; + + const changeValidation = validation => { + _field.validation = validation; + return _field.save(); + }; + + // required ===== + _field.isRequired = true; + await _field.save(); + expectError('required', ''); + + // email ===== + await changeValidation('email'); + expectError('Invalid email', 'wrongValue'); + + // number ===== + await changeValidation('number'); + expectError('Invalid number', 'wrongValue'); + + // date ===== + await changeValidation('date'); + expectError('Invalid date', 'wrongValue'); + }); + + test('Validate submission: valid values', async () => { + const expectValid = async value => { + const res = await Fields.validate({ _id: _field._id, value }); + expect(res).toBe('valid'); + }; + + const changeValidation = validation => { + _field.validation = validation; + return _field.save(); + }; + + // required ===== + _field.isRequired = true; + await changeValidation(null); + expectValid('value'); + + // email ===== + await changeValidation('email'); + expectValid('email@gmail.com'); + + // number ===== + await changeValidation('number'); + expectValid('2.333'); + expectValid('2'); + + // date ===== + await changeValidation('date'); + expectValid('2017-01-01'); + }); }); diff --git a/src/data/schema/segment.js b/src/data/schema/segment.js index c661d2bdc..9a49a7fa9 100644 --- a/src/data/schema/segment.js +++ b/src/data/schema/segment.js @@ -29,17 +29,16 @@ export const queries = ` `; const commonFields = ` - contentType: String!, name: String!, description: String, subOf: String, color: String, connector: String, - conditions: SegmentCondition + conditions: [SegmentCondition] `; export const mutations = ` - segmentsAdd(${commonFields}): Segment + segmentsAdd(contentType: String!, ${commonFields}): Segment segmentsEdit(_id: String!, ${commonFields}): Segment segmentsRemove(_id: String!): Segment `; diff --git a/src/db/models/Fields.js b/src/db/models/Fields.js index dc6adaeac..e059bc718 100644 --- a/src/db/models/Fields.js +++ b/src/db/models/Fields.js @@ -4,6 +4,7 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +import validator from 'validator'; import { FIELD_CONTENT_TYPES } from '../../constants'; import { Forms } from './'; @@ -124,6 +125,49 @@ class Field { return this.find({ _id: { $in: ids } }).sort({ order: 1 }); } + + /* + * Validate per field according to it's validation and type + * + * @param {String} _id - Field id + * @param {String|Date|Number} value - Submitted value + * @throw Validation error + * @return {String} - valid indicator + */ + static async validate({ _id, value }) { + const field = await this.findOne({ _id }); + + const { type, validation } = field; + + // throw error helper + const throwError = message => { + throw new Error(`${field.text}: ${message}`); + }; + + // required + if (field.isRequired && !value) { + throwError('required'); + } + + if (value) { + // email + if ((type === 'email' || validation === 'email') && !validator.isEmail(value)) { + throwError('Invalid email'); + } + + // number + if (validation === 'number' && !validator.isFloat(value)) { + throwError('Invalid number'); + } + + // date + if (validation === 'date' && !validator.isISO8601(value)) { + throwError('Invalid date'); + } + } + + return 'valid'; + } } FieldSchema.loadClass(Field); diff --git a/yarn.lock b/yarn.lock index 0a1aa0fa7..98555ec84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4458,6 +4458,10 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" +validator@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-9.0.0.tgz#6c1ef955e007af704adea86ae8a76da84a6c172e" + vary@^1, vary@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" From 2ec736273d3f4f1aa67626b79238d5313eb1d4a2 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 15 Oct 2017 15:43:52 +0800 Subject: [PATCH 071/318] Validate customer data when customer save --- src/__tests__/customerDb.test.js | 38 +++++++++++++++++++++++++++++--- src/__tests__/fieldDb.test.js | 26 ++++++++++++++++++++++ src/db/factories.js | 1 + src/db/models/Customers.js | 12 ++++++++-- src/db/models/Fields.js | 21 ++++++++++++++++++ 5 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/__tests__/customerDb.test.js b/src/__tests__/customerDb.test.js index e2dabca2d..85c49b112 100644 --- a/src/__tests__/customerDb.test.js +++ b/src/__tests__/customerDb.test.js @@ -3,7 +3,7 @@ import { connect, disconnect } from '../db/connection'; import { Customers } from '../db/models'; -import { customerFactory } from '../db/factories'; +import { fieldFactory, customerFactory } from '../db/factories'; beforeAll(() => connect()); @@ -21,7 +21,7 @@ describe('Customers model tests', () => { await Customers.remove({}); }); - test('Create customer', async () => { + test('Create customer: successful', async () => { const doc = { name: 'name', email: 'dombo@yahoo.com' }; const customerObj = await Customers.createCustomer(doc); @@ -30,7 +30,7 @@ describe('Customers model tests', () => { expect(customerObj.email).toBe(doc.email); }); - test('Update customer', async () => { + test('Update customer: successful', async () => { const doc = { name: 'Dombo', email: 'dombo@yahoo.com', @@ -69,4 +69,36 @@ describe('Customers model tests', () => { expect(customer.companyIds).toEqual(expect.arrayContaining([company._id])); }); + + test('Create customer: with customer fields validation error', async () => { + expect.assertions(1); + + const field = await fieldFactory({ validation: 'number' }); + + try { + await Customers.createCustomer({ + name: 'name', + email: 'dombo@yahoo.com', + customFieldsData: { [field._id]: 'invalid number' }, + }); + } catch (e) { + expect(e.message).toBe(`${field.text}: Invalid number`); + } + }); + + test('Update customer: with customer fields validation error', async () => { + expect.assertions(1); + + const field = await fieldFactory({ validation: 'number' }); + + try { + await Customers.updateCustomer(_customer._id, { + name: 'name', + email: 'dombo@yahoo.com', + customFieldsData: { [field._id]: 'invalid number' }, + }); + } catch (e) { + expect(e.message).toBe(`${field.text}: Invalid number`); + } + }); }); diff --git a/src/__tests__/fieldDb.test.js b/src/__tests__/fieldDb.test.js index e7d8a39e6..57587a3e7 100644 --- a/src/__tests__/fieldDb.test.js +++ b/src/__tests__/fieldDb.test.js @@ -126,6 +126,18 @@ describe('Fields', () => { expect(fieldObj).toBeNull(); }); + test('Validate submission: field not found', async () => { + expect.assertions(1); + + const _id = 'INVALID_ID'; + + try { + await Fields.validate({ _id, value: '' }); + } catch (e) { + expect(e.message).toBe(`Field not found with the _id of ${_id}`); + } + }); + test('Validate submission: invalid values', async () => { expect.assertions(4); @@ -189,4 +201,18 @@ describe('Fields', () => { await changeValidation('date'); expectValid('2017-01-01'); }); + + // test('Validate fields: invalid values', async () => { + // expect.assertions(1); + // + // // required ===== + // _field.isRequired = true; + // await _field.save(); + // + // try { + // await Fields.validateMulti({ _id: _field._id, value: '' }) + // } catch (e) { + // expect(e.message).toBe(`${_field.text}: required`); + // } + // }); }); diff --git a/src/db/factories.js b/src/db/factories.js index 5c2aeb166..178736583 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -116,6 +116,7 @@ export const customerFactory = (params = {}) => { email: params.email || faker.internet.email(), phone: params.phone || faker.random.word(), messengerData: params.messengerData || {}, + customFieldsData: params.customFieldsData || {}, }); return customer.save(); diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index dce4e950e..1d96e71dd 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -1,6 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -import { Companies } from './'; +import { Fields, Companies } from './'; /* * messenger schema @@ -104,7 +104,10 @@ class Customer { * @param {Object} customerObj object * @return {Promise} Newly created customer object */ - static createCustomer(doc) { + static async createCustomer(doc) { + // validate custom field values + await Fields.validateMulti(doc.customFieldsData || {}); + return this.create(doc); } @@ -115,6 +118,9 @@ class Customer { * @return {Promise} updated customer object */ static async updateCustomer(_id, doc) { + // validate custom field values + await Fields.validateMulti(doc.customFieldsData || {}); + await this.update({ _id }, { $set: doc }); return this.findOne({ _id }); @@ -142,6 +148,8 @@ class Customer { /* * Create new company and add to customer's company list + * @param {String} name - Company name + * @param {String} website - Company website * @return {Promise} newly created company */ static async addCompany({ _id, name, website }) { diff --git a/src/db/models/Fields.js b/src/db/models/Fields.js index e059bc718..2ac71043c 100644 --- a/src/db/models/Fields.js +++ b/src/db/models/Fields.js @@ -137,6 +137,10 @@ class Field { static async validate({ _id, value }) { const field = await this.findOne({ _id }); + if (!field) { + throw new Error(`Field not found with the _id of ${_id}`); + } + const { type, validation } = field; // throw error helper @@ -168,6 +172,23 @@ class Field { return 'valid'; } + + /* + * Validate multiple fields + * + * @param {Object} data - field._id, value mapping + * @return {String} - valid indicator + */ + static async validateMulti(data) { + const ids = Object.keys(data); + + // validate individual fields + for (let _id of ids) { + await this.validate({ _id, value: data[_id] }); + } + + return 'valid'; + } } FieldSchema.loadClass(Field); From fcb2ae497a30d065bbd61f9c0d9f4fa3ac157d7c Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 15 Oct 2017 17:22:11 +0800 Subject: [PATCH 072/318] Check duplicated customer email --- src/__tests__/customerDb.test.js | 10 ++++++++++ src/db/models/Customers.js | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/src/__tests__/customerDb.test.js b/src/__tests__/customerDb.test.js index 85c49b112..c6243a6f2 100644 --- a/src/__tests__/customerDb.test.js +++ b/src/__tests__/customerDb.test.js @@ -30,6 +30,16 @@ describe('Customers model tests', () => { expect(customerObj.email).toBe(doc.email); }); + test('Create customer: duplicated email', async () => { + expect.assertions(1); + + try { + await Customers.createCustomer({ name: 'name', email: _customer.email }); + } catch (e) { + expect(e.message).toBe('Duplicated email'); + } + }); + test('Update customer: successful', async () => { const doc = { name: 'Dombo', diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index 1d96e71dd..acf72acc7 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -105,6 +105,13 @@ class Customer { * @return {Promise} Newly created customer object */ static async createCustomer(doc) { + const previousEntry = await Customers.findOne({ email: doc.email }); + + // check duplication + if (previousEntry) { + throw new Error('Duplicated email'); + } + // validate custom field values await Fields.validateMulti(doc.customFieldsData || {}); From 701bc339ed6fe5398a3cd9e71ad3f8fa6f223ea8 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 15 Oct 2017 17:36:20 +0800 Subject: [PATCH 073/318] Check duplicated entry in customer update --- src/__tests__/customerDb.test.js | 13 +++++++++++++ src/db/models/Customers.js | 10 ++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/__tests__/customerDb.test.js b/src/__tests__/customerDb.test.js index c6243a6f2..5ef64f8c6 100644 --- a/src/__tests__/customerDb.test.js +++ b/src/__tests__/customerDb.test.js @@ -41,12 +41,25 @@ describe('Customers model tests', () => { }); test('Update customer: successful', async () => { + expect.assertions(4); + + const previousCustomer = await customerFactory({ email: 'dombo@yahoo.com' }); + const doc = { name: 'Dombo', email: 'dombo@yahoo.com', phone: '242442200', }; + try { + await Customers.updateCustomer(_customer._id, doc); + } catch (e) { + expect(e.message).toBe('Duplicated email'); + } + + // remove previous duplicated entry + await Customers.remove({ _id: previousCustomer._id }); + const customerObj = await Customers.updateCustomer(_customer._id, doc); expect(customerObj.name).toBe(doc.name); diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index acf72acc7..229ddbaf8 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -125,6 +125,16 @@ class Customer { * @return {Promise} updated customer object */ static async updateCustomer(_id, doc) { + const previousEntry = await Customers.findOne({ + _id: { $ne: _id }, + email: doc.email, + }); + + // check duplication + if (previousEntry) { + throw new Error('Duplicated email'); + } + // validate custom field values await Fields.validateMulti(doc.customFieldsData || {}); From 7fe30b59435732c2f57067b35c4a3610cc51c20b Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 15 Oct 2017 18:05:19 +0800 Subject: [PATCH 074/318] Check duplication in company create, update --- src/__tests__/companyDb.test.js | 23 +++++++++++++++++++++++ src/__tests__/customerDb.test.js | 24 ++++++++++++------------ src/db/factories.js | 2 +- src/db/models/Companies.js | 19 ++++++++++++++++++- src/db/models/Customers.js | 4 ++-- 5 files changed, 56 insertions(+), 16 deletions(-) diff --git a/src/__tests__/companyDb.test.js b/src/__tests__/companyDb.test.js index 130f17ffb..5436a1076 100644 --- a/src/__tests__/companyDb.test.js +++ b/src/__tests__/companyDb.test.js @@ -39,6 +39,15 @@ describe('Companies model tests', () => { }); test('Create company', async () => { + expect.assertions(7); + + // check duplication + try { + await Companies.createCompany({ name: _company.name }); + } catch (e) { + expect(e.message).toBe('Duplicated name'); + } + const doc = generateDoc(); const companyObj = await Companies.createCompany(doc); @@ -47,8 +56,22 @@ describe('Companies model tests', () => { }); test('Update company', async () => { + expect.assertions(7); + const doc = generateDoc(); + const previousCompany = await companyFactory({ name: doc.name }); + + // test duplication + try { + await Companies.updateCompany(_company._id, doc); + } catch (e) { + expect(e.message).toBe('Duplicated name'); + } + + // remove previous duplicated entry + await Companies.remove({ _id: previousCompany._id }); + const companyObj = await Companies.updateCompany(_company._id, doc); check(companyObj, doc); diff --git a/src/__tests__/customerDb.test.js b/src/__tests__/customerDb.test.js index 5ef64f8c6..e6e67634c 100644 --- a/src/__tests__/customerDb.test.js +++ b/src/__tests__/customerDb.test.js @@ -21,26 +21,25 @@ describe('Customers model tests', () => { await Customers.remove({}); }); - test('Create customer: successful', async () => { - const doc = { name: 'name', email: 'dombo@yahoo.com' }; - - const customerObj = await Customers.createCustomer(doc); - - expect(customerObj.name).toBe(doc.name); - expect(customerObj.email).toBe(doc.email); - }); - - test('Create customer: duplicated email', async () => { - expect.assertions(1); + test('Create customer', async () => { + expect.assertions(3); + // check duplication try { await Customers.createCustomer({ name: 'name', email: _customer.email }); } catch (e) { expect(e.message).toBe('Duplicated email'); } + + const doc = { name: 'name', email: 'dombo@yahoo.com' }; + + const customerObj = await Customers.createCustomer(doc); + + expect(customerObj.name).toBe(doc.name); + expect(customerObj.email).toBe(doc.email); }); - test('Update customer: successful', async () => { + test('Update customer', async () => { expect.assertions(4); const previousCustomer = await customerFactory({ email: 'dombo@yahoo.com' }); @@ -51,6 +50,7 @@ describe('Customers model tests', () => { phone: '242442200', }; + // test duplication try { await Customers.updateCustomer(_customer._id, doc); } catch (e) { diff --git a/src/db/factories.js b/src/db/factories.js index 178736583..e041b78ce 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -101,7 +101,7 @@ export const internalNoteFactory = (params = {}) => { export const companyFactory = (params = {}) => { const company = new Companies({ - name: faker.random.word(), + name: params.name || faker.random.word(), size: params.size || faker.random.number(), industry: params.industry || Random.id(), website: params.website || Random.id(), diff --git a/src/db/models/Companies.js b/src/db/models/Companies.js index b0dba51a6..a42888621 100644 --- a/src/db/models/Companies.js +++ b/src/db/models/Companies.js @@ -63,7 +63,14 @@ class Company { * @param {Object} companyObj object * @return {Promise} Newly created company object */ - static createCompany(doc) { + static async createCompany(doc) { + const previousEntry = await this.findOne({ name: doc.name }); + + // check duplication + if (previousEntry) { + throw new Error('Duplicated name'); + } + return this.create(doc); } @@ -74,6 +81,16 @@ class Company { * @return {Promise} updated company object */ static async updateCompany(_id, doc) { + const previousEntry = await this.findOne({ + _id: { $ne: _id }, + name: doc.name, + }); + + // check duplication + if (previousEntry) { + throw new Error('Duplicated name'); + } + await this.update({ _id }, { $set: doc }); return this.findOne({ _id }); diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index 229ddbaf8..3b366fb23 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -105,7 +105,7 @@ class Customer { * @return {Promise} Newly created customer object */ static async createCustomer(doc) { - const previousEntry = await Customers.findOne({ email: doc.email }); + const previousEntry = await this.findOne({ email: doc.email }); // check duplication if (previousEntry) { @@ -125,7 +125,7 @@ class Customer { * @return {Promise} updated customer object */ static async updateCustomer(_id, doc) { - const previousEntry = await Customers.findOne({ + const previousEntry = await this.findOne({ _id: { $ne: _id }, email: doc.email, }); From 1790ee59eca252b196c50e777b7c70fb8cc0251d Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 15 Oct 2017 18:26:07 +0800 Subject: [PATCH 075/318] Refactor internal notes tests --- src/__tests__/internalNoteDb.test.js | 80 +++++++++++++++++++++ src/__tests__/internalNoteMutations.test.js | 74 ++++++++----------- 2 files changed, 108 insertions(+), 46 deletions(-) create mode 100644 src/__tests__/internalNoteDb.test.js diff --git a/src/__tests__/internalNoteDb.test.js b/src/__tests__/internalNoteDb.test.js new file mode 100644 index 000000000..176cf87d8 --- /dev/null +++ b/src/__tests__/internalNoteDb.test.js @@ -0,0 +1,80 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import faker from 'faker'; +import { connect, disconnect } from '../db/connection'; +import { InternalNotes, Users } from '../db/models'; +import { userFactory, internalNoteFactory } from '../db/factories'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +/* + * Generate test data + */ +const generateData = () => ({ + contentType: 'customer', + contentTypeId: 'DFDFAFSFSDDSF', + content: faker.random.word(), +}); + +/* + * Check values + */ +const checkValues = (internalNoteObj, doc) => { + expect(internalNoteObj.contentType).toBe(doc.contentType); + expect(internalNoteObj.contentTypeId).toBe(doc.contentTypeId); + expect(internalNoteObj.content).toBe(doc.content); +}; + +describe('InternalNotes mutations', () => { + let _user; + let _internalNote; + + beforeEach(async () => { + // Creating test data + _user = await userFactory(); + _internalNote = await internalNoteFactory(); + }); + + afterEach(async () => { + // Clearing test data + await InternalNotes.remove({}); + await Users.remove({}); + }); + + test('Create internalNote', async () => { + // valid + const doc = generateData(); + + const internalNoteObj = await InternalNotes.createInternalNote(doc, _user); + + checkValues(internalNoteObj, doc); + expect(internalNoteObj.createdUserId).toBe(_user._id); + }); + + test('Edit internalNote valid', async () => { + const doc = generateData(); + + const internalNoteObj = await InternalNotes.updateInternalNote(_internalNote._id, doc); + + checkValues(internalNoteObj, doc); + }); + + test('Remove internalNote valid', async () => { + try { + await InternalNotes.removeInternalNote('DFFFDSFD'); + } catch (e) { + expect(e.message).toBe('InternalNote not found with id DFFFDSFD'); + } + + let count = await InternalNotes.find({ _id: _internalNote._id }).count(); + expect(count).toBe(1); + + await InternalNotes.removeInternalNote(_internalNote._id); + + count = await InternalNotes.find({ _id: _internalNote._id }).count(); + expect(count).toBe(0); + }); +}); diff --git a/src/__tests__/internalNoteMutations.test.js b/src/__tests__/internalNoteMutations.test.js index 2b9e5d4ff..0a5b4b7f2 100644 --- a/src/__tests__/internalNoteMutations.test.js +++ b/src/__tests__/internalNoteMutations.test.js @@ -14,19 +14,10 @@ afterAll(() => disconnect()); /* * Generate test data */ -const generateData = () => ({ +const doc = { contentType: 'customer', contentTypeId: 'DFDFAFSFSDDSF', content: faker.random.word(), -}); - -/* - * Check values - */ -const checkValues = (internalNoteObj, doc) => { - expect(internalNoteObj.contentType).toBe(doc.contentType); - expect(internalNoteObj.contentTypeId).toBe(doc.contentTypeId); - expect(internalNoteObj.content).toBe(doc.content); }; describe('InternalNotes mutations', () => { @@ -45,61 +36,52 @@ describe('InternalNotes mutations', () => { await Users.remove({}); }); - test('Create internalNote', async () => { - // Login required - expect(() => internalNoteMutations.internalNotesAdd({}, {}, {})).toThrowError('Login required'); + test('Check login required', async () => { + expect.assertions(3); + + const check = async fn => { + try { + await fn({}, {}, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }; - // valid - const doc = generateData(); + // add + check(internalNoteMutations.internalNotesAdd); - const internalNoteObj = await internalNoteMutations.internalNotesAdd({}, doc, { user: _user }); + // edit + check(internalNoteMutations.internalNotesEdit); - checkValues(internalNoteObj, doc); - expect(internalNoteObj.createdUserId).toBe(_user._id); + // add company + check(internalNoteMutations.internalNotesRemove); }); - test('Edit internalNote login required', async () => { - expect.assertions(1); + test('Create internalNote', async () => { + InternalNotes.createInternalNote = jest.fn(); + + await internalNoteMutations.internalNotesAdd({}, doc, { user: _user }); - try { - await internalNoteMutations.internalNotesEdit({}, { _id: _internalNote.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + expect(InternalNotes.createInternalNote).toBeCalledWith(doc, _user); }); test('Edit internalNote valid', async () => { - const doc = generateData(); + InternalNotes.updateInternalNote = jest.fn(); - const internalNoteObj = await internalNoteMutations.internalNotesEdit( + await internalNoteMutations.internalNotesEdit( {}, { _id: _internalNote._id, ...doc }, { user: _user }, ); - checkValues(internalNoteObj, doc); - }); - - test('Remove internalNote login required', async () => { - expect.assertions(1); - - try { - await internalNoteMutations.internalNotesRemove({}, { _id: _internalNote.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + expect(InternalNotes.updateInternalNote).toBeCalledWith(_internalNote._id, doc); }); test('Remove internalNote valid', async () => { - const internalNoteDeletedObj = await internalNoteMutations.internalNotesRemove( - {}, - { _id: _internalNote.id }, - { user: _user }, - ); + InternalNotes.removeInternalNote = jest.fn(); - expect(internalNoteDeletedObj.id).toBe(_internalNote.id); + await internalNoteMutations.internalNotesRemove({}, { _id: _internalNote.id }, { user: _user }); - const internalNoteObj = await InternalNotes.findOne({ _id: _internalNote.id }); - expect(internalNoteObj).toBeNull(); + expect(InternalNotes.removeInternalNote).toBeCalledWith(_internalNote.id); }); }); From 883c165283d12e5ad1f151937874aa49e33170b5 Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Sun, 15 Oct 2017 19:36:25 +0800 Subject: [PATCH 076/318] add notification and sendEmail in conversation --- .../conversationMessageMutations.test.js | 36 ++++- src/data/resolvers/mutations/conversations.js | 147 ++++++++++++++++-- src/db/factories.js | 13 +- src/db/models/Conversations.js | 53 +------ 4 files changed, 178 insertions(+), 71 deletions(-) diff --git a/src/__tests__/conversationMessageMutations.test.js b/src/__tests__/conversationMessageMutations.test.js index ec583f438..77d541aed 100644 --- a/src/__tests__/conversationMessageMutations.test.js +++ b/src/__tests__/conversationMessageMutations.test.js @@ -3,7 +3,13 @@ import { connect, disconnect } from '../db/connection'; import { Conversations, ConversationMessages, Users } from '../db/models'; -import { conversationFactory, conversationMessageFactory, userFactory } from '../db/factories'; +import { + conversationFactory, + conversationMessageFactory, + userFactory, + integrationFactory, + customerFactory, +} from '../db/factories'; import conversationMutations from '../data/resolvers/mutations/conversations'; beforeAll(() => connect()); @@ -15,12 +21,22 @@ describe('Conversation message mutations', () => { let _conversationMessage; let _user; let _doc; + let _integration; + let _customer; beforeEach(async () => { // Creating test data _conversation = await conversationFactory(); _conversationMessage = await conversationMessageFactory(); _user = await userFactory(); + _customer = await customerFactory(); + _integration = await integrationFactory({ kind: 'form' }); + + _conversation.integrationId = _integration._id; + _conversation.customerId = _customer._id; + _conversation.assignedUserId = _user._id; + await _conversation.save(); + _doc = { content: _conversationMessage.content, attachments: _conversationMessage.attachments, @@ -113,7 +129,8 @@ describe('Conversation message mutations', () => { } }); - test('Create conversation message', async () => { + test('Add conversation message', async () => { + expect.assertions(3); ConversationMessages.addMessage = jest.fn(() => ({ _id: 'messageObject', })); @@ -122,6 +139,19 @@ describe('Conversation message mutations', () => { expect(ConversationMessages.addMessage.mock.calls.length).toBe(1); expect(ConversationMessages.addMessage).toBeCalledWith(_doc, _user._id); + + // integration kind form test + _doc['internal'] = false; + await conversationMutations.conversationMessageAdd({}, _doc, { user: _user }); + + try { + // integration not found test + _conversation.integrationId = 'test'; + await _conversation.save(); + await conversationMutations.conversationMessageAdd({}, _doc, { user: _user }); + } catch (e) { + expect(e.message).toEqual('Integration not found'); + } }); // if user assigned to conversation @@ -162,7 +192,7 @@ describe('Conversation message mutations', () => { test('Change conversation status', async () => { Conversations.changeStatusConversation = jest.fn(); - const status = 'new'; + const status = 'closed'; await conversationMutations.conversationsChangeStatus( {}, { _ids: [_conversation._id], status: status }, diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index a2a988115..42b96bdef 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -1,7 +1,33 @@ -import { Conversations, ConversationMessages } from '../../../db/models'; +import { Conversations, ConversationMessages, Integrations, Customers } from '../../../db/models'; import { pubsub } from '../subscriptions'; -import { CONVERSATION_STATUSES } from '../../constants'; -import { sendEmail } from '../../utils'; +import { CONVERSATION_STATUSES, KIND_CHOICES } from '../../constants'; +import { sendEmail, sendNotification } from '../../utils'; +import { _ } from 'underscore'; + +/** + * conversation notrification receiver ids + * @param {object} conversation object + * @param {String} currentUserId String + * @return {list} userIds + */ +const conversationNotifReceivers = (conversation, currentUserId) => { + let userIds = []; + + // assigned user can get notifications + if (conversation.assignedUserId) { + userIds.push(conversation.assignedUserId); + } + + // participated users can get notifications + if (conversation.participatedUserIds) { + userIds = _.union(userIds, conversation.participatedUserIds); + } + + // exclude current user + userIds = _.without(userIds, currentUserId); + + return userIds; +}; /** * Publish updated conversation @@ -17,10 +43,13 @@ const conversationsChanged = async (_ids, type) => { conversationChanged: { conversationId: _id, type }, }); - pubsub.publish('conversationsChanged', { - conversationsChanged: { customerId: conversation.customerId, type }, - }); + if (conversation) { + pubsub.publish('conversationsChanged', { + conversationsChanged: { customerId: conversation.customerId, type }, + }); + } } + return _ids; }; /** @@ -34,7 +63,8 @@ const conversationMessageCreated = async (message, conversationId) => { conversationMessageInserted: message, }); - const conversation = Conversations.findOne({ _id: conversationId }); + const conversation = await Conversations.findOne({ _id: conversationId }); + pubsub.publish('conversationsChanged', { conversationsChanged: { customerId: conversation.customerId, type: 'newMessage' }, }); @@ -51,6 +81,59 @@ export default { const message = await ConversationMessages.addMessage(doc, user._id); + const conversation = await Conversations.findOne({ _id: doc.conversationId }); + const title = 'You have a new message.'; + + // send notification + sendNotification({ + createdUser: user._id, + notifType: 'conversationAddMessage', + title, + content: doc.content, + link: `/inbox/details/${conversation._id}`, + receivers: conversationNotifReceivers(conversation, user._id), + }); + + // do not send internal message to third service integrations + if (doc.internal) { + return message; + } + + const integration = await Integrations.findOne({ _id: conversation.integrationId }); + + if (!integration) { + throw new Error('Integration not found'); + } + + const kind = integration.kind; + + // send reply to twitter + if (kind === KIND_CHOICES.TWITTER) { + // TODO: return tweetReply(conversation, strip(content)); + } + + const customer = await Customers.findOne({ _id: conversation.customerId }); + + // if conversation's integration kind is form then send reply to + // customer's email + const email = customer ? customer.email : ''; + + if (kind === KIND_CHOICES.FORM && email) { + sendEmail({ + to: email, + title: 'Reply', + template: { + data: doc.content, + }, + }); + } + + // send reply to facebook + if (kind === KIND_CHOICES.FACEBOOK) { + // when facebook kind is feed, assign commentId in extraData + // TODO: facebookReply(conversation, strip(content), messageId); + } + await conversationMessageCreated(message, doc.conversationId); return message._id; @@ -70,6 +153,22 @@ export default { // notify graphl subscription await conversationsChanged(conversationIds, 'statusChanged'); + const updatedConversations = await Conversations.find({ _id: { $in: conversationIds } }); + + for (let conversation of updatedConversations) { + const content = 'Assigned user has changed'; + + // send notification + sendNotification({ + createdUser: user._id, + notifType: 'conversationAssigneeChange', + title: content, + content, + link: `/inbox/details/${conversation._id}`, + receivers: conversationNotifReceivers(conversation, this.userId), + }); + } + return 'done'; }, @@ -105,10 +204,10 @@ export default { // notify graphl subscription await conversationsChanged(_ids, 'statusChanged'); - conversations.forEach(async conversation => { + for (let conversation of conversations) { if (status === CONVERSATION_STATUSES.CLOSED) { - const customer = conversation.customer(); - const integration = conversation.integration(); + const customer = await Customers.findOne({ _id: conversation.customerId }); + const integration = await Integrations.findOne({ _id: conversation.integrationId }); const messengerData = integration.messengerData || {}; const notifyCustomer = messengerData.notifyCustomer || false; @@ -116,16 +215,32 @@ export default { // send email to customer sendEmail({ to: customer.email, - title: 'Conversation detail', - content: await ConversationMessages.find({ conversationId: conversation._id }), + subject: 'Conversation detail', + templateArgs: { + name: 'conversationDetail', + data: { + conversationDetail: { + title: 'Conversation detail', + messages: await ConversationMessages.find({ conversationId: conversation._id }), + date: new Date(), + }, + }, + }, }); } } - }); - - const content = 'Conversation status has changed.'; - // TODO: send notification + const content = 'Conversation status has changed.'; + + sendNotification({ + createdUser: user._id, + notifType: 'conversationStateChange', + title: content, + content, + link: `/inbox/details/${conversation._id}`, + receivers: conversationNotifReceivers(conversation, user._id), + }); + } return 'done'; }, diff --git a/src/db/factories.js b/src/db/factories.js index 78c4cecce..7ef7b936e 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -15,6 +15,7 @@ import { FormFields, NotificationConfigurations, Notifications, + Customers, } from './models'; export const userFactory = (params = {}) => { @@ -93,14 +94,14 @@ export const conversationMessageFactory = (params = {}) => { return conversationMessage.save(); }; -export const integrationFactory = params => { +export const integrationFactory = (params = {}) => { const kind = params.kind || 'messenger'; return Integrations.create({ name: faker.random.word(), kind: kind, brandId: params.brandId || Random.id(), formId: params.formId || Random.id(), - messengerData: params.messengerData || { welcomeMessage: 'welcome' }, + messengerData: params.messengerData || { welcomeMessage: 'welcome', notifyCustomer: true }, formData: params.formData === 'form' ? params.formData @@ -168,3 +169,11 @@ export const notificationFactory = params => { receiver: params.receiver || userFactory({}), }); }; + +export const customerFactory = (params = {}) => { + const customer = new Customers({ + name: params.name || faker.name.findName(), + email: params.email || faker.internet.email(), + }); + return customer.save(); +}; diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index fdfeebc0d..ec05fab17 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -2,10 +2,8 @@ import strip from 'strip'; import mongoose from 'mongoose'; import Random from 'meteor-random'; -import { CONVERSATION_STATUSES, FACEBOOK_DATA_KINDS, KIND_CHOICES } from '../../data/constants'; +import { CONVERSATION_STATUSES, FACEBOOK_DATA_KINDS } from '../../data/constants'; -import { Integrations, Customers } from './'; -import { sendEmail } from '../../data/utils'; import { Users } from '../../db/models'; const TwitterDirectMessageSchema = mongoose.Schema({ @@ -160,13 +158,7 @@ class Conversation { { multi: true }, ); - const updatedConversations = await Conversations.find({ _id: { $in: conversationIds } }); - - // send notification - updatedConversations.forEach(conversation => { - const content = 'Assigned user has changed'; - // TODO: sendNotification - }); + return Conversations.find({ _id: { $in: conversationIds } }); } /** @@ -343,46 +335,7 @@ class Message { // setting conversation's content to last message await this.update({ _id: doc.conversationId }, { $set: { content } }); - // TODO: send notification - - // do not send internal message to third service integrations - if (doc.internal) { - return this.createMessage({ ...doc, userId }); - } - - const integration = await Integrations.findOne({ _id: conversation.integrationId }); - - if (!integration) throw new Error('Integration not found'); - - const kind = integration.kind; - - // send reply to twitter - if (kind === KIND_CHOICES.TWITTER) { - // TODO: return tweetReply(conversation, strip(content)); - } - - const message = await this.createMessage({ ...doc, userId }); - const customer = await Customers.findOne({ _id: conversation.customerId }); - - // if conversation's integration kind is form then send reply to - // customer's email - const email = customer ? customer.email : ''; - - if (kind === KIND_CHOICES.FORM && email) { - sendEmail({ - to: email, - title: 'Reply', - content, - }); - } - - // send reply to facebook - if (kind === KIND_CHOICES.FACEBOOK) { - // when facebook kind is feed, assign commentId in extraData - // TODO: facebookReply(conversation, strip(content), messageId); - } - - return message; + return this.createMessage({ ...doc, userId }); } } From 23bf54c56a3acf021ecb7781c73bed9d9f225063 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 15 Oct 2017 20:52:29 +0800 Subject: [PATCH 077/318] Refactor validate value logic of fields --- src/__tests__/fieldDb.test.js | 38 ++++++++++++++++++----------------- src/db/models/Customers.js | 8 ++++---- src/db/models/Fields.js | 27 ++++++++++++++++--------- 3 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/__tests__/fieldDb.test.js b/src/__tests__/fieldDb.test.js index 57587a3e7..906358fb6 100644 --- a/src/__tests__/fieldDb.test.js +++ b/src/__tests__/fieldDb.test.js @@ -132,7 +132,7 @@ describe('Fields', () => { const _id = 'INVALID_ID'; try { - await Fields.validate({ _id, value: '' }); + await Fields.clean(_id, ''); } catch (e) { expect(e.message).toBe(`Field not found with the _id of ${_id}`); } @@ -143,7 +143,7 @@ describe('Fields', () => { const expectError = async (message, value) => { try { - await Fields.validate({ _id: _field._id, value }); + await Fields.clean(_field._id, value); } catch (e) { expect(e.message).toBe(`${_field.text}: ${message}`); } @@ -174,8 +174,8 @@ describe('Fields', () => { test('Validate submission: valid values', async () => { const expectValid = async value => { - const res = await Fields.validate({ _id: _field._id, value }); - expect(res).toBe('valid'); + const res = await Fields.clean(_field._id, value); + expect(res).toBe(value); }; const changeValidation = validation => { @@ -198,21 +198,23 @@ describe('Fields', () => { expectValid('2'); // date ===== + // date values must be convert to date object await changeValidation('date'); - expectValid('2017-01-01'); + const res = await Fields.clean(_field._id, '2017-01-01'); + expect(res).toEqual(expect.any(Date)); }); - // test('Validate fields: invalid values', async () => { - // expect.assertions(1); - // - // // required ===== - // _field.isRequired = true; - // await _field.save(); - // - // try { - // await Fields.validateMulti({ _id: _field._id, value: '' }) - // } catch (e) { - // expect(e.message).toBe(`${_field.text}: required`); - // } - // }); + test('Validate fields: invalid values', async () => { + expect.assertions(1); + + // required ===== + _field.isRequired = true; + await _field.save(); + + try { + await Fields.cleanMulti({ [_field._id]: '' }); + } catch (e) { + expect(e.message).toBe(`${_field.text}: required`); + } + }); }); diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index 3b366fb23..30ebc6c03 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -112,8 +112,8 @@ class Customer { throw new Error('Duplicated email'); } - // validate custom field values - await Fields.validateMulti(doc.customFieldsData || {}); + // clean custom field values + doc.customFieldsData = await Fields.cleanMulti(doc.customFieldsData || {}); return this.create(doc); } @@ -135,8 +135,8 @@ class Customer { throw new Error('Duplicated email'); } - // validate custom field values - await Fields.validateMulti(doc.customFieldsData || {}); + // clean custom field values + doc.customFieldsData = await Fields.cleanMulti(doc.customFieldsData || {}); await this.update({ _id }, { $set: doc }); diff --git a/src/db/models/Fields.js b/src/db/models/Fields.js index 2ac71043c..9cc7a3d9b 100644 --- a/src/db/models/Fields.js +++ b/src/db/models/Fields.js @@ -128,15 +128,18 @@ class Field { /* * Validate per field according to it's validation and type + * fixes values if necessary * * @param {String} _id - Field id * @param {String|Date|Number} value - Submitted value * @throw Validation error * @return {String} - valid indicator */ - static async validate({ _id, value }) { + static async clean(_id, _value) { const field = await this.findOne({ _id }); + let value = _value; + if (!field) { throw new Error(`Field not found with the _id of ${_id}`); } @@ -160,34 +163,40 @@ class Field { } // number - if (validation === 'number' && !validator.isFloat(value)) { + if (validation === 'number' && !validator.isFloat(value.toString())) { throwError('Invalid number'); } // date - if (validation === 'date' && !validator.isISO8601(value)) { - throwError('Invalid date'); + if (validation === 'date') { + if (!validator.isISO8601(value)) { + throwError('Invalid date'); + } + + value = new Date(value); } } - return 'valid'; + return value; } /* - * Validate multiple fields + * Validates multiple fields, fixes values if necessary * * @param {Object} data - field._id, value mapping * @return {String} - valid indicator */ - static async validateMulti(data) { + static async cleanMulti(data) { const ids = Object.keys(data); + const fixedValues = {}; + // validate individual fields for (let _id of ids) { - await this.validate({ _id, value: data[_id] }); + fixedValues[_id] = await this.clean(_id, data[_id]); } - return 'valid'; + return fixedValues; } } From 614d3df30a682d17e817eb13fa58525685855a46 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 15 Oct 2017 21:26:52 +0800 Subject: [PATCH 078/318] Validate custom field values before company save --- src/__tests__/companyDb.test.js | 32 +++++++++++++++++++++++++++++++- src/db/models/Companies.js | 8 +++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/__tests__/companyDb.test.js b/src/__tests__/companyDb.test.js index 5436a1076..5fdd1d75c 100644 --- a/src/__tests__/companyDb.test.js +++ b/src/__tests__/companyDb.test.js @@ -3,7 +3,7 @@ import { connect, disconnect } from '../db/connection'; import { Companies } from '../db/models'; -import { companyFactory } from '../db/factories'; +import { companyFactory, fieldFactory } from '../db/factories'; beforeAll(() => connect()); @@ -91,4 +91,34 @@ describe('Companies model tests', () => { expect(customer.companyIds).toEqual(expect.arrayContaining([company._id])); }); + + test('Create company: with company fields validation error', async () => { + expect.assertions(1); + + const field = await fieldFactory({ validation: 'number' }); + + try { + await Companies.createCompany({ + name: 'name', + customFieldsData: { [field._id]: 'invalid number' }, + }); + } catch (e) { + expect(e.message).toBe(`${field.text}: Invalid number`); + } + }); + + test('Update company: with company fields validation error', async () => { + expect.assertions(1); + + const field = await fieldFactory({ validation: 'number' }); + + try { + await Companies.updateCompany(_company._id, { + name: 'name', + customFieldsData: { [field._id]: 'invalid number' }, + }); + } catch (e) { + expect(e.message).toBe(`${field.text}: Invalid number`); + } + }); }); diff --git a/src/db/models/Companies.js b/src/db/models/Companies.js index a42888621..cc32031c6 100644 --- a/src/db/models/Companies.js +++ b/src/db/models/Companies.js @@ -1,6 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -import { Customers } from './'; +import { Fields, Customers } from './'; const CompanySchema = mongoose.Schema({ _id: { @@ -71,6 +71,9 @@ class Company { throw new Error('Duplicated name'); } + // clean custom field values + doc.customFieldsData = await Fields.cleanMulti(doc.customFieldsData || {}); + return this.create(doc); } @@ -91,6 +94,9 @@ class Company { throw new Error('Duplicated name'); } + // clean custom field values + doc.customFieldsData = await Fields.cleanMulti(doc.customFieldsData || {}); + await this.update({ _id }, { $set: doc }); return this.findOne({ _id }); From 95ee04194d34870ed2529abd46c069600f5c9589 Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Sun, 15 Oct 2017 22:19:42 +0800 Subject: [PATCH 079/318] add jest mock function test in brands, emailTemplates, responseTemplates --- src/__tests__/brandDb.test.js | 79 +++++++++++ src/__tests__/brandMutations.test.js | 126 +++++++++--------- src/__tests__/conversationMessageDB.test.js | 69 +++++++++- src/__tests__/emailTemplateDb.test.js | 57 ++++++++ src/__tests__/emailTemplateMutations.test.js | 82 +++++++----- src/__tests__/responseTemplateDb.test.js | 68 ++++++++++ .../responseTemplateMutations.test.js | 111 +++++++-------- src/data/resolvers/mutations/brands.js | 27 ++-- .../resolvers/mutations/emailTemplates.js | 19 ++- .../resolvers/mutations/responseTemplates.js | 19 ++- src/data/schema/brand.js | 2 +- src/db/models/Brands.js | 39 +++++- src/db/models/Conversations.js | 12 +- src/db/models/EmailTemplates.js | 29 ++++ src/db/models/ResponseTemplates.js | 31 +++++ 15 files changed, 564 insertions(+), 206 deletions(-) create mode 100644 src/__tests__/brandDb.test.js create mode 100644 src/__tests__/emailTemplateDb.test.js create mode 100644 src/__tests__/responseTemplateDb.test.js diff --git a/src/__tests__/brandDb.test.js b/src/__tests__/brandDb.test.js new file mode 100644 index 000000000..f66063626 --- /dev/null +++ b/src/__tests__/brandDb.test.js @@ -0,0 +1,79 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Brands, Users } from '../db/models'; +import { brandFactory, userFactory } from '../db/factories'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Brands mutations', () => { + let _brand; + let _user; + + beforeEach(async () => { + // Creating test data + _brand = await brandFactory(); + _user = await userFactory(); + }); + + afterEach(async () => { + // Clearing test data + await Brands.remove({}); + await Users.remove({}); + }); + + test('Create brand', async () => { + const brandObj = await Brands.createBrand({ + code: _brand.code, + name: _brand.name, + description: _brand.description, + userId: _user.id, + }); + expect(brandObj).toBeDefined(); + expect(brandObj.code).toBe(_brand.code); + expect(brandObj.name).toBe(_brand.name); + expect(brandObj.userId).toBe(_user._id); + + // invalid data + expect(() => { + Brands.createBrand({ code: '', name: _brand.name, userId: _user.id }); + }).toThrowError('Code is required field'); + }); + + test('Update brand', async () => { + const _brandUpdateObj = await brandFactory(); + + // update brand object + const brandObj = await Brands.updateBrand(_brand.id, { + code: _brandUpdateObj.code, + name: _brandUpdateObj.name, + description: _brandUpdateObj.description, + }); + + expect(brandObj.code).toBe(_brandUpdateObj.code); + expect(brandObj.name).toBe(_brandUpdateObj.name); + expect(brandObj.description).toBe(_brandUpdateObj.description); + }); + + test('Delete brand', async () => { + await Brands.removeBrand(_brand.id); + + expect(await Brands.findOne({ _id: _brand.id }).count()).toBe(0); + + try { + await Brands.removeBrand('test'); + } catch (e) { + expect(e.message).toBe('Brand not found with id test'); + } + }); + + test('Update brand email config', async () => { + const brandObj = await Brands.updateEmailConfig(_brand.id, _brand.emailConfig); + + expect(brandObj.emailConfig.type).toBe(_brand.emailConfig.type); + expect(brandObj.emailConfig.template).toBe(_brand.emailConfig.template); + }); +}); diff --git a/src/__tests__/brandMutations.test.js b/src/__tests__/brandMutations.test.js index 91dd84f40..e7cf3aecb 100644 --- a/src/__tests__/brandMutations.test.js +++ b/src/__tests__/brandMutations.test.js @@ -26,89 +26,87 @@ describe('Brands mutations', () => { await Users.remove({}); }); - test('Create brand', async () => { - const brandObj = await brandMutations.brandsAdd( - {}, - { code: _brand.code, name: _brand.name, description: _brand.description }, - { user: _user }, - ); - expect(brandObj).toBeDefined(); - expect(brandObj.code).toBe(_brand.code); - expect(brandObj.name).toBe(_brand.name); - expect(brandObj.userId).toBe(_user._id); - - // invalid data - expect(() => - brandMutations.brandsAdd({}, { code: '', name: _brand.name }, { user: _user }), - ).toThrowError('Code is required field'); - - // Login required - expect(() => - brandMutations.brandsAdd({}, { code: _brand.code, name: brandObj.name }, {}), - ).toThrowError('Login required'); - }); - - test('Update brand', async () => { - // get new brand object - const _brandUpdate = await brandFactory(); + test('Check login required mutations', async () => { + expect.assertions(4); - // update brand object - const brandObj = await brandMutations.brandsEdit( - {}, - { - _id: _brand.id, - code: _brandUpdate.code, - name: _brandUpdate.name, - description: _brandUpdate.description, - }, - { user: _user }, - ); - - // check changes - expect(brandObj.code).toBe(_brandUpdate.code); - expect(brandObj.name).toBe(_brandUpdate.name); - expect(brandObj.description).toBe(_brandUpdate.description); - }); + // brands add + try { + await brandMutations.brandsAdd({}, { code: _brand.code, name: _brand.name }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } - test('Update brand login required', async () => { - expect.assertions(1); + // brands edit try { await brandMutations.brandsEdit({}, { _id: _brand.id }, {}); } catch (e) { expect(e.message).toEqual('Login required'); } - }); - test('Delete brand', async () => { - const brandDeletedObj = await brandMutations.brandsRemove( - {}, - { _id: _brand.id }, - { user: _user }, - ); - expect(brandDeletedObj.id).toBe(_brand.id); - - const brandObj = await Brands.findOne({ _id: _brand.id }); - expect(brandObj).toBeNull(); - }); - - test('Delete brand login required', async () => { - expect.assertions(1); + // brands remove try { await brandMutations.brandsRemove({}, { _id: _brand.id }, {}); } catch (e) { expect(e.message).toEqual('Login required'); } + + // brands update email config + try { + await brandMutations.brandsConfigEmail({}, { _id: _brand.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Create brand', async () => { + const _doc = { + code: _brand.code, + name: _brand.name, + description: _brand.description, + }; + + Brands.createBrand = jest.fn(); + + await brandMutations.brandsAdd({}, _doc, { user: _user }); + + expect(Brands.createBrand.mock.calls.length).toBe(1); + expect(Brands.createBrand).toBeCalledWith({ userId: _user._id, ..._doc }); + }); + + test('Update brand', async () => { + Brands.updateBrand = jest.fn(); + + const _doc = { + code: 'test', + name: 'test', + description: 'test', + }; + + // update brand object + await brandMutations.brandsEdit({}, { _id: _brand._id, ..._doc }, { user: _user }); + + expect(Brands.updateBrand.mock.calls.length).toBe(1); + expect(Brands.updateBrand).toBeCalledWith(_brand._id, _doc); + }); + + test('Delete brand', async () => { + Brands.removeBrand = jest.fn(); + + await brandMutations.brandsRemove({}, { _id: _brand.id }, { user: _user }); + expect(Brands.removeBrand.mock.calls.length).toBe(1); + expect(Brands.removeBrand).toBeCalledWith(_brand._id); }); test('Update brand email config', async () => { - const brandObj = await brandMutations.brandsConfigEmail( + Brands.updateEmailConfig = jest.fn(); + + await brandMutations.brandsConfigEmail( {}, { _id: _brand.id, emailConfig: _brand.emailConfig }, - { user: _brand.userId }, + { user: _user._id }, ); - expect(brandObj).toBeDefined(); - expect(brandObj.emailConfig.type).toBe(_brand.emailConfig.type); - expect(brandObj.emailConfig.template).toBe(_brand.emailConfig.template); + expect(Brands.updateEmailConfig.mock.calls.length).toBe(1); + expect(Brands.updateEmailConfig).toBeCalledWith(_brand._id, _brand.emailConfig); }); }); diff --git a/src/__tests__/conversationMessageDB.test.js b/src/__tests__/conversationMessageDB.test.js index cba2bcfb4..32876d590 100644 --- a/src/__tests__/conversationMessageDB.test.js +++ b/src/__tests__/conversationMessageDB.test.js @@ -5,6 +5,7 @@ import { connect, disconnect } from '../db/connection'; import { Conversations, ConversationMessages, Users } from '../db/models'; import { conversationFactory, conversationMessageFactory, userFactory } from '../db/factories'; import conversationMutations from '../data/resolvers/mutations/conversations'; +import { CONVERSATION_STATUSES } from '../data/constants'; beforeAll(() => connect()); @@ -21,6 +22,7 @@ describe('Conversation message db', () => { _conversation = await conversationFactory(); _conversationMessage = await conversationMessageFactory(); _user = await userFactory(); + _doc = { content: _conversationMessage.content, attachments: _conversationMessage.attachments, @@ -43,6 +45,37 @@ describe('Conversation message db', () => { await Users.remove({}); }); + test('Create conversation', async () => { + const _number = (await Conversations.find().count()) + 1; + const conversation = await Conversations.createConversation({ + content: _conversation.content, + assignedUserId: _user._id, + integrationId: 'test', + participatedUserIds: [_user._id], + readUserIds: [_user._id], + }); + + expect(conversation).toBeDefined(); + expect(conversation.content).toBe(_conversation.content); + expect(conversation.status).toBe(CONVERSATION_STATUSES.NEW); + expect(conversation.number).toBe(_number); + expect(conversation.messageCount).toBe(0); + }); + + test('Check conversation existance', async () => { + const { selector, conversations } = await Conversations.checkExistanceConversations([ + _conversation._id, + ]); + expect(conversations[0]._id).toBe(_conversation._id); + expect(selector).toEqual({ _id: { $in: [_conversation._id] } }); + + // wrong conversation ids + try { + await Conversations.checkExistanceConversations(['test']); + } catch (e) { + expect(e.message).toEqual('Conversation not found.'); + } + }); test('Create conversation message', async () => { const messageObj = await ConversationMessages.addMessage(_doc, _user); @@ -58,6 +91,23 @@ describe('Conversation message db', () => { expect(messageObj.formWidgetData).toBe(_conversationMessage.formWidgetData); expect(messageObj.facebookData._id).toBe(_conversationMessage.facebookData._id); expect(messageObj.userId).toBe(_user._id); + + try { + // without content + _doc.attachments = []; + _doc.content = ''; + await ConversationMessages.addMessage(_doc, _user); + } catch (e) { + expect(e.message).toEqual('Content is required'); + } + + try { + // without conversation + _doc.conversationId = 'test'; + await ConversationMessages.addMessage(_doc, _user); + } catch (e) { + expect(e.message).toEqual('Conversation not found with id test'); + } }); // if user assigned to conversation @@ -67,6 +117,13 @@ describe('Conversation message db', () => { const conversationObj = await Conversations.findOne({ _id: _conversation._id }); expect(conversationObj.assignedUserId).toBe(_user._id); + + // without assign user + try { + await Conversations.assignUserConversation([_conversation._id], undefined); + } catch (e) { + expect(e.message).toEqual('User not found with id undefined'); + } }); test('Unassign employee from conversation', async () => { @@ -143,6 +200,9 @@ describe('Conversation message db', () => { test('Conversation mark as read', async () => { // first user read this conversation + _conversation.readUserIds = ''; + _conversation.save(); + await Conversations.markAsReadConversation(_conversation._id, _user._id); const conversationObj = await Conversations.findOne({ _id: _conversation._id }); @@ -152,6 +212,13 @@ describe('Conversation message db', () => { const second_user = await userFactory(); // multiple users read conversation - await Conversations.markAsReadConversation(_conversation._id, second_user._id, false); + await Conversations.markAsReadConversation(_conversation._id, second_user._id); + + try { + // without conversation + await Conversations.markAsReadConversation('test', second_user._id); + } catch (e) { + expect(e.message).toEqual('Conversation not found with id test'); + } }); }); diff --git a/src/__tests__/emailTemplateDb.test.js b/src/__tests__/emailTemplateDb.test.js new file mode 100644 index 000000000..563ec2037 --- /dev/null +++ b/src/__tests__/emailTemplateDb.test.js @@ -0,0 +1,57 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { EmailTemplates } from '../db/models'; +import { emailTemplateFactory } from '../db/factories'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Email template mutations', () => { + let _emailTemplate; + + beforeEach(async () => { + // Creating test data + _emailTemplate = await emailTemplateFactory(); + }); + + afterEach(async () => { + // Clearing test data + await EmailTemplates.remove({}); + }); + + test('Create email template', async () => { + const emailTemplateObj = await EmailTemplates.create({ + name: _emailTemplate.name, + content: _emailTemplate.content, + }); + expect(emailTemplateObj).toBeDefined(); + expect(emailTemplateObj.name).toBe(_emailTemplate.name); + expect(emailTemplateObj.content).toBe(_emailTemplate.content); + }); + + test('Update email template', async () => { + const doc = { + name: _emailTemplate.name, + content: _emailTemplate.content, + }; + + const emailTemplateObj = await EmailTemplates.updateEmailTemplate(_emailTemplate.id, doc); + expect(emailTemplateObj.name).toBe(_emailTemplate.name); + expect(emailTemplateObj.content).toBe(_emailTemplate.content); + }); + + test('Delete email template', async () => { + await EmailTemplates.removeEmailTemplate(_emailTemplate.id); + + expect(await EmailTemplates.find({ _id: _emailTemplate.id }).count()).toBe(0); + + try { + await EmailTemplates.removeEmailTemplate('test'); + } catch (e) { + expect(e.message).toBe('Email template not found with id test'); + } + }); +}); diff --git a/src/__tests__/emailTemplateMutations.test.js b/src/__tests__/emailTemplateMutations.test.js index 28a588405..9eb6ef6e2 100644 --- a/src/__tests__/emailTemplateMutations.test.js +++ b/src/__tests__/emailTemplateMutations.test.js @@ -26,63 +26,71 @@ describe('Email template mutations', () => { await Users.remove({}); }); - test('Create email template', async () => { - const emailTemplateObj = await emailTemplateMutations.emailTemplateAdd( - {}, - { name: _emailTemplate.name, content: _emailTemplate.content }, - { user: _user }, - ); - expect(emailTemplateObj).toBeDefined(); - expect(emailTemplateObj.name).toBe(_emailTemplate.name); - expect(emailTemplateObj.content).toBe(_emailTemplate.content); + test('Email templates login required functions', async () => { + expect.assertions(3); - // Login required test - expect(() => + // add email template + try { emailTemplateMutations.emailTemplateAdd( {}, { name: _emailTemplate.name, content: _emailTemplate.content }, {}, - ), - ).toThrowError('Login required'); + ); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + + // update email template + try { + await emailTemplateMutations.emailTemplateEdit({}, { _id: _emailTemplate.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + + // remove email template + try { + await emailTemplateMutations.emailTemplateRemove({}, { _id: _emailTemplate.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Create email template', async () => { + EmailTemplates.create = jest.fn(); + + const _doc = { name: _emailTemplate.name, content: _emailTemplate.content }; + + await emailTemplateMutations.emailTemplateAdd({}, _doc, { user: _user }); + + expect(EmailTemplates.create.mock.calls.length).toBe(1); + expect(EmailTemplates.create).toBeCalledWith(_doc); }); test('Update email template', async () => { - const emailTemplateObj = await emailTemplateMutations.emailTemplateEdit( + EmailTemplates.updateEmailTemplate = jest.fn(); + + const _doc = { name: _emailTemplate.name, content: _emailTemplate.content }; + + await emailTemplateMutations.emailTemplateEdit( {}, - { _id: _emailTemplate.id, name: _emailTemplate.name, content: _emailTemplate.content }, + { _id: _emailTemplate.id, ..._doc }, { user: _user }, ); - expect(emailTemplateObj).toBeDefined(); - expect(emailTemplateObj.id).toBe(_emailTemplate.id); - expect(emailTemplateObj.name).toBe(_emailTemplate.name); - expect(emailTemplateObj.content).toBe(_emailTemplate.content); - }); - test('Update email template login required', async () => { - expect.assertions(1); - try { - await emailTemplateMutations.emailTemplateEdit({}, { _id: _emailTemplate.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + expect(EmailTemplates.updateEmailTemplate.mock.calls.length).toBe(1); + expect(EmailTemplates.updateEmailTemplate).toBeCalledWith(_emailTemplate.id, _doc); }); test('Delete email template', async () => { + EmailTemplates.removeEmailTemplate = jest.fn(); + await emailTemplateMutations.emailTemplateRemove( {}, { _id: _emailTemplate.id }, { user: _user }, ); - const count = await EmailTemplates.find({ _id: _emailTemplate.id }).count(); - expect(count).toBe(0); - }); - test('Delete email template login required', async () => { - expect.assertions(1); - try { - await emailTemplateMutations.emailTemplateRemove({}, { _id: _emailTemplate.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + expect(EmailTemplates.removeEmailTemplate.mock.calls.length).toBe(1); + expect(EmailTemplates.removeEmailTemplate).toBeCalledWith(_emailTemplate.id); }); }); diff --git a/src/__tests__/responseTemplateDb.test.js b/src/__tests__/responseTemplateDb.test.js new file mode 100644 index 000000000..01b5b9c30 --- /dev/null +++ b/src/__tests__/responseTemplateDb.test.js @@ -0,0 +1,68 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { ResponseTemplates } from '../db/models'; +import { responseTemplateFactory } from '../db/factories'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Response template mutations', () => { + let _responseTemplate; + + beforeEach(async () => { + // Creating test data + _responseTemplate = await responseTemplateFactory(); + }); + + afterEach(async () => { + // Clearing test data + await ResponseTemplates.remove({}); + }); + + test('Create response template', async () => { + const responseTemplateObj = await ResponseTemplates.create({ + name: _responseTemplate.name, + content: _responseTemplate.content, + brandId: _responseTemplate.brandId, + files: _responseTemplate.files, + }); + + expect(responseTemplateObj).toBeDefined(); + expect(responseTemplateObj.name).toBe(_responseTemplate.name); + expect(responseTemplateObj.content).toBe(_responseTemplate.content); + expect(responseTemplateObj.brandId).toBe(_responseTemplate.brandId); + expect(responseTemplateObj.files[0]).toBe(_responseTemplate.files[0]); + }); + + test('Update response template', async () => { + const responseTemplateObj = await ResponseTemplates.updateResponseTemplate( + _responseTemplate.id, + { + name: _responseTemplate.name, + content: _responseTemplate.content, + brandId: _responseTemplate.brandId, + files: _responseTemplate.files, + }, + ); + + expect(responseTemplateObj.id).toBe(_responseTemplate.id); + expect(responseTemplateObj.name).toBe(_responseTemplate.name); + expect(responseTemplateObj.content).toBe(_responseTemplate.content); + expect(responseTemplateObj.brandId).toBe(_responseTemplate.brandId); + expect(responseTemplateObj.files[0]).toBe(_responseTemplate.files[0]); + }); + + test('Delete response template', async () => { + await ResponseTemplates.removeResponseTemplate({ _id: _responseTemplate.id }); + expect(await ResponseTemplates.findOne({ _id: _responseTemplate.id }).count()).toBe(0); + + try { + await ResponseTemplates.removeResponseTemplate('test'); + } catch (e) { + expect(e.message).toBe('Response template not found with id test'); + } + }); +}); diff --git a/src/__tests__/responseTemplateMutations.test.js b/src/__tests__/responseTemplateMutations.test.js index 1eda530be..92a889b0f 100644 --- a/src/__tests__/responseTemplateMutations.test.js +++ b/src/__tests__/responseTemplateMutations.test.js @@ -26,79 +26,80 @@ describe('Response template mutations', () => { await Users.remove({}); }); - test('Create response template', async () => { - const responseTemplateObj = await responseTemplateMutations.responseTemplateAdd( - {}, - { - name: _responseTemplate.name, - content: _responseTemplate.content, - brandId: _responseTemplate.brandId, - files: _responseTemplate.files, - }, - { user: _user }, - ); - expect(responseTemplateObj).toBeDefined(); - expect(responseTemplateObj.name).toBe(_responseTemplate.name); - expect(responseTemplateObj.content).toBe(_responseTemplate.content); - expect(responseTemplateObj.brandId).toBe(_responseTemplate.brandId); - expect(responseTemplateObj.files[0]).toBe(_responseTemplate.files[0]); - - // login required test - expect(() => + test('Response templates login required functions', async () => { + expect.assertions(3); + + // add response template + try { responseTemplateMutations.responseTemplateAdd( {}, { name: _responseTemplate.name, content: _responseTemplate.content }, {}, - ), - ).toThrowError('Login required'); + ); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + + // update response template + try { + await responseTemplateMutations.responseTemplateEdit({}, { _id: _responseTemplate.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + + // remove response template + try { + await responseTemplateMutations.responseTemplateRemove({}, { _id: _responseTemplate.id }, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }); + + test('Create response template', async () => { + ResponseTemplates.create = jest.fn(); + + const _doc = { + name: _responseTemplate.name, + content: _responseTemplate.content, + brandId: _responseTemplate.brandId, + files: _responseTemplate.files, + }; + + await responseTemplateMutations.responseTemplateAdd({}, _doc, { user: _user }); + expect(ResponseTemplates.create.mock.calls.length).toBe(1); + expect(ResponseTemplates.create).toBeCalledWith(_doc); }); test('Update response template', async () => { - const responseTemplateObj = await responseTemplateMutations.responseTemplateEdit( + ResponseTemplates.updateResponseTemplate = jest.fn(); + + const _doc = { + name: _responseTemplate.name, + content: _responseTemplate.content, + brandId: _responseTemplate.brandId, + files: _responseTemplate.files, + }; + + await responseTemplateMutations.responseTemplateEdit( {}, - { - _id: _responseTemplate.id, - name: _responseTemplate.name, - content: _responseTemplate.content, - brandId: _responseTemplate.brandId, - files: _responseTemplate.files, - }, + { _id: _responseTemplate.id, ..._doc }, { user: _user }, ); - expect(responseTemplateObj).toBeDefined(); - expect(responseTemplateObj.id).toBe(_responseTemplate.id); - expect(responseTemplateObj.name).toBe(_responseTemplate.name); - expect(responseTemplateObj.content).toBe(_responseTemplate.content); - expect(responseTemplateObj.brandId).toBe(_responseTemplate.brandId); - expect(responseTemplateObj.files[0]).toBe(_responseTemplate.files[0]); - }); - test('Update response template login required', async () => { - expect.assertions(1); - try { - await responseTemplateMutations.responseTemplateEdit({}, { _id: _responseTemplate.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + expect(ResponseTemplates.updateResponseTemplate.mock.calls.length).toBe(1); + expect(ResponseTemplates.updateResponseTemplate).toBeCalledWith(_responseTemplate.id, _doc); }); test('Delete response template', async () => { - const deletedObj = await responseTemplateMutations.responseTemplateRemove( + ResponseTemplates.removeResponseTemplate = jest.fn(); + + await responseTemplateMutations.responseTemplateRemove( {}, { _id: _responseTemplate.id }, { user: _user }, ); - expect(deletedObj.id).toBe(_responseTemplate.id); - const emailTemplateObj = await ResponseTemplates.findOne({ _id: _responseTemplate.id }); - expect(emailTemplateObj).toBeNull(); - }); - test('Delete response template login required', async () => { - expect.assertions(1); - try { - await responseTemplateMutations.responseTemplateRemove({}, { _id: _responseTemplate.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + expect(ResponseTemplates.removeResponseTemplate.mock.calls.length).toBe(1); + expect(ResponseTemplates.removeResponseTemplate).toBeCalledWith(_responseTemplate.id); }); }); diff --git a/src/data/resolvers/mutations/brands.js b/src/data/resolvers/mutations/brands.js index 9248db359..0da5bd328 100644 --- a/src/data/resolvers/mutations/brands.js +++ b/src/data/resolvers/mutations/brands.js @@ -3,49 +3,48 @@ import { Brands } from '../../../db/models'; export default { /** * Create new brand + * @param {Object} doc - brand fields * @return {Promise} brand object */ brandsAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); - if (!doc.code) throw new Error('Code is required field'); - return Brands.createBrand({ userId: user._id, ...doc }); }, /** * Update brand + * @param {String} _id - brand id + * @param {Object} fields - brand fields * @return {Promise} brand object */ - async brandsEdit(root, { _id, ...fields }, { user }) { + brandsEdit(root, { _id, ...fields }, { user }) { if (!user) throw new Error('Login required'); - await Brands.update({ _id }, { $set: { ...fields } }); - return Brands.findOne({ _id }); + return Brands.updateBrand(_id, fields); }, /** * Delete brand - * @return {Promise} + * @param {String} _id - brand id + * @return {String} */ - async brandsRemove(root, { _id }, { user }) { + brandsRemove(root, { _id }, { user }) { if (!user) throw new Error('Login required'); - const brandObj = await Brands.findOne({ _id }); - - if (!brandObj) throw new Error(`Brand not found with id ${_id}`); - - return brandObj.remove(); + Brands.removeBrand(_id); + return 'done'; }, /** * Update brands email config + * @param {String} _id - brand id + * @param {Object} emailConfig - brand email config fields * @return {Promise} brand object */ async brandsConfigEmail(root, { _id, emailConfig }, { user }) { if (!user) throw new Error('Login required'); - await Brands.update({ _id }, { emailConfig }); - return Brands.findOne({ _id }); + return Brands.updateEmailConfig(_id, emailConfig); }, }; diff --git a/src/data/resolvers/mutations/emailTemplates.js b/src/data/resolvers/mutations/emailTemplates.js index 0d5c2cde7..270b7e078 100644 --- a/src/data/resolvers/mutations/emailTemplates.js +++ b/src/data/resolvers/mutations/emailTemplates.js @@ -3,6 +3,7 @@ import { EmailTemplates } from '../../../db/models'; export default { /** * Create new email template + * @param {Object} doc - email templates fields * @return {Promise} email template object */ emailTemplateAdd(root, doc, { user }) { @@ -13,28 +14,24 @@ export default { /** * Update email template + * @param {String} _id - email templates id + * @param {Object} fields - email templates fields * @return {Promise} email template object */ - async emailTemplateEdit(root, { _id, ...fields }, { user }) { + emailTemplateEdit(root, { _id, ...fields }, { user }) { if (!user) throw new Error('Login required'); - await EmailTemplates.update({ _id }, { $set: { ...fields } }); - return EmailTemplates.findOne({ _id }); + return EmailTemplates.updateEmailTemplate(_id, fields); }, /** * Delete email template + * @param {String} doc - email templates fields * @return {Promise} */ - async emailTemplateRemove(root, { _id }, { user }) { + emailTemplateRemove(root, { _id }, { user }) { if (!user) throw new Error('Login required'); - const emailTemplateObj = await EmailTemplates.findOne({ _id }); - - if (!emailTemplateObj) { - throw new Error(`Email template not found with id ${_id}`); - } - - return emailTemplateObj.remove(); + return EmailTemplates.removeEmailTemplate(_id); }, }; diff --git a/src/data/resolvers/mutations/responseTemplates.js b/src/data/resolvers/mutations/responseTemplates.js index b18b4ddf4..107af03d3 100644 --- a/src/data/resolvers/mutations/responseTemplates.js +++ b/src/data/resolvers/mutations/responseTemplates.js @@ -3,6 +3,7 @@ import { ResponseTemplates } from '../../../db/models'; export default { /** * Create new response template + * @param {Object} fields - response template fields * @return {Promise} response template object */ responseTemplateAdd(root, doc, { user }) { @@ -13,28 +14,24 @@ export default { /** * Update response template + * @param {String} _id - response template id + * @param {Object} fields - response template fields * @return {Promise} response template object */ - async responseTemplateEdit(root, { _id, ...fields }, { user }) { + responseTemplateEdit(root, { _id, ...fields }, { user }) { if (!user) throw new Error('Login required'); - await ResponseTemplates.update({ _id }, { $set: { ...fields } }); - return ResponseTemplates.findOne({ _id }); + return ResponseTemplates.updateResponseTemplate(_id, fields); }, /** * Delete response template + * @param {String} _id - response template id * @return {Promise} */ - async responseTemplateRemove(root, { _id }, { user }) { + responseTemplateRemove(root, { _id }, { user }) { if (!user) throw new Error('Login required'); - const responseTemplateObj = await ResponseTemplates.findOne({ _id }); - - if (!responseTemplateObj) { - throw new Error(`Response template not found with id ${_id}`); - } - - return responseTemplateObj.remove(); + return ResponseTemplates.removeResponseTemplate(_id); }, }; diff --git a/src/data/schema/brand.js b/src/data/schema/brand.js index 2f67b7096..a47c94ddc 100644 --- a/src/data/schema/brand.js +++ b/src/data/schema/brand.js @@ -19,6 +19,6 @@ export const queries = ` export const mutations = ` brandsAdd(code: String!, name: String, description: String): Brand brandsEdit(_id: String!, code: String, name: String, description: String): Brand - brandsRemove(_id: String!): Brand + brandsRemove(_id: String!): String brandsConfigEmail(_id: String!, emailConfig: JSON): Brand `; diff --git a/src/db/models/Brands.js b/src/db/models/Brands.js index b1a49314d..4169f4d34 100644 --- a/src/db/models/Brands.js +++ b/src/db/models/Brands.js @@ -26,10 +26,12 @@ const BrandSchema = mongoose.Schema({ class Brand { /** * Create a brand - * @param {Object} brandObj object + * @param {Object} doc object * @return {Promise} Newly created brand object */ static createBrand(doc) { + if (!doc.code) throw new Error('Code is required field'); + return this.create({ ...doc, createdAt: new Date(), @@ -38,12 +40,37 @@ class Brand { /** * Update a brand - * @param {_id} brandObj object - * @param {fields} - * @return {Promise} Updated brand id + * @param {string} _id - brand id + * @param {fields} fields - brand fields + * @return {Promise} Updated brand object + */ + static async updateBrand(_id, fields) { + await Brands.update({ _id }, { $set: { ...fields } }); + return Brands.findOne({ _id }); + } + + /** + * Delete brand + * @param {string} _id - brand id + * @return {Promise} Updated brand object + */ + static async removeBrand(_id) { + const brandObj = await Brands.findOne({ _id }); + + if (!brandObj) throw new Error(`Brand not found with id ${_id}`); + + return brandObj.remove(); + } + + /** + * Update email config of brand + * @param {string} _id - brand id + * @return {Promise} Updated brand object */ - static updateBrand(_id, fields) { - return this.update({ _id }, { $set: { ...fields } }); + static async updateEmailConfig(_id, emailConfig) { + await Brands.update({ _id }, { $set: { emailConfig } }); + + return Brands.findOne({ _id }); } } diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index ec05fab17..6c2e0a19c 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -129,12 +129,12 @@ class Conversation { * @param {Object} conversationObj object * @return {Promise} Newly created conversation object */ - static createConversation(doc) { + static async createConversation(doc) { return this.create({ ...doc, status: CONVERSATION_STATUSES.NEW, createdAt: new Date(), - number: this.find().count() + 1, + number: (await this.find().count()) + 1, messageCount: 0, }); } @@ -149,7 +149,7 @@ class Conversation { await this.checkExistanceConversations(conversationIds); if (!await Users.findOne({ _id: assignedUserId })) { - throw new Error('User not found.'); + throw new Error(`User not found with id ${assignedUserId}`); } await this.update( @@ -263,12 +263,12 @@ class Conversation { static async markAsReadConversation(_id, userId) { const conversation = await Conversations.findOne({ _id }); - if (!conversation) return 'Conversation not found'; + if (!conversation) throw new Error(`Conversation not found with id ${_id}`); const readUserIds = conversation.readUserIds; // if current user is first one - if (!readUserIds) { + if (!readUserIds || readUserIds.length === 0) { return this.update({ _id }, { $set: { readUserIds: [userId] } }); } @@ -320,7 +320,7 @@ class Message { static async addMessage(doc, userId) { const conversation = await Conversations.findOne({ _id: doc.conversationId }); - if (!conversation) throw new Error('Conversation not found'); + if (!conversation) throw new Error(`Conversation not found with id ${doc.conversationId}`); // normalize content, attachments const content = doc.content || ''; diff --git a/src/db/models/EmailTemplates.js b/src/db/models/EmailTemplates.js index f6b1398cc..b4f18f208 100644 --- a/src/db/models/EmailTemplates.js +++ b/src/db/models/EmailTemplates.js @@ -11,6 +11,35 @@ const EmailTemplateSchema = mongoose.Schema({ content: String, }); +class EmailTemplate { + /** + * Update email template + * @param {String} _id - email template id + * @param {Object} fields - email template fields + * @return {Promise} Update email template object + */ + static async updateEmailTemplate(_id, fields) { + await EmailTemplates.update({ _id }, { $set: fields }); + return EmailTemplates.findOne({ _id }); + } + + /** + * Delete email template + * @param {String} _id - email template id + * @return {Promise} + */ + static async removeEmailTemplate(_id) { + const emailTemplateObj = await EmailTemplates.findOne({ _id }); + + if (!emailTemplateObj) { + throw new Error(`Email template not found with id ${_id}`); + } + + return emailTemplateObj.remove(); + } +} + +EmailTemplateSchema.loadClass(EmailTemplate); const EmailTemplates = mongoose.model('email_templates', EmailTemplateSchema); export default EmailTemplates; diff --git a/src/db/models/ResponseTemplates.js b/src/db/models/ResponseTemplates.js index a3628c761..22157ff17 100644 --- a/src/db/models/ResponseTemplates.js +++ b/src/db/models/ResponseTemplates.js @@ -15,6 +15,37 @@ const ResponseTemplateSchema = mongoose.Schema({ }, }); +class ResponseTemplate { + /** + * Update response template + * @param {String} _id - response template id + * @param {Object} fields - response template fields + * @return {Promise} Update response template object + */ + static async updateResponseTemplate(_id, fields) { + await ResponseTemplates.update({ _id }, { $set: { ...fields } }); + + return ResponseTemplates.findOne({ _id }); + } + + /** + * Delete response template + * @param {String} _id - response template id + * @return {Promise} + */ + static async removeResponseTemplate(_id) { + const responseTemplateObj = await ResponseTemplates.findOne({ _id }); + + if (!responseTemplateObj) { + throw new Error(`Response template not found with id ${_id}`); + } + + return responseTemplateObj.remove(); + } +} + +ResponseTemplateSchema.loadClass(ResponseTemplate); + const ResponseTemplates = mongoose.model('response_templates', ResponseTemplateSchema); export default ResponseTemplates; From 0419876adc7f6d4467759be174231218e6eb6a36 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Mon, 16 Oct 2017 14:52:07 +0800 Subject: [PATCH 080/318] #13 Send email --- src/__tests__/toolsSendEmail.test.js | 35 ---------------------------- src/data/utils.js | 8 +++---- 2 files changed, 3 insertions(+), 40 deletions(-) delete mode 100644 src/__tests__/toolsSendEmail.test.js diff --git a/src/__tests__/toolsSendEmail.test.js b/src/__tests__/toolsSendEmail.test.js deleted file mode 100644 index a539ae45b..000000000 --- a/src/__tests__/toolsSendEmail.test.js +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint-env jest */ -/* eslint-disable no-underscore-dangle */ - -import { connect, disconnect } from '../db/connection'; -import { sendEmail } from '../data/utils'; - -beforeAll(() => connect()); -afterAll(() => disconnect()); - -describe('data/tools/sendEmail', () => { - beforeEach(() => {}); - - afterEach(() => {}); - - test('check whether email is being sent successfully', async () => { - const doc = { - test: 'Nofiticaton test', - content: 'Notification content', - date: new Date(), - link: 'https://www.google.com', - }; - - await sendEmail({ - toEmails: ['javkhlan.sh@gmail.com'], - fromEmail: 'test@erxes.io', - title: 'Notification', - template: { - name: 'notification', - data: { - notification: doc, - }, - }, - }); - }); -}); diff --git a/src/data/utils.js b/src/data/utils.js index fd9bc5bee..b9e8f19f8 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -33,11 +33,9 @@ const applyTemplate = async (data, templateName) => { export const sendEmail = async ({ toEmails, fromEmail, title, template }) => { const { MAIL_SERVICE, MAIL_USER, MAIL_PASS, NODE_ENV } = process.env; // do not send email it is running in test mode - const isTest = NODE_ENV == 'test'; - - // if (isTest) { - // return; - // } + if (NODE_ENV == 'test') { + return; + } const transporter = nodemailer.createTransport({ service: MAIL_SERVICE, From 86036a4f07b26b524676e97e7acf1059f87d9c8d Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Mon, 16 Oct 2017 18:07:03 +0800 Subject: [PATCH 081/318] add individual function test in conversation mutation --- src/__tests__/brandDb.test.js | 2 +- ...ssageDB.test.js => conversationDb.test.js} | 34 +---- ....test.js => conversationMutations.test.js} | 142 +++++++++--------- src/__tests__/emailTemplateDb.test.js | 2 +- src/__tests__/responseTemplateDb.test.js | 2 +- src/data/resolvers/mutations/brands.js | 3 +- src/data/resolvers/mutations/conversations.js | 51 +++---- src/data/schema/conversation.js | 17 +-- src/data/utils.js | 7 + src/db/models/Conversations.js | 30 ++-- 10 files changed, 144 insertions(+), 146 deletions(-) rename src/__tests__/{conversationMessageDB.test.js => conversationDb.test.js} (84%) rename src/__tests__/{conversationMessageMutations.test.js => conversationMutations.test.js} (71%) diff --git a/src/__tests__/brandDb.test.js b/src/__tests__/brandDb.test.js index f66063626..69bff8241 100644 --- a/src/__tests__/brandDb.test.js +++ b/src/__tests__/brandDb.test.js @@ -9,7 +9,7 @@ beforeAll(() => connect()); afterAll(() => disconnect()); -describe('Brands mutations', () => { +describe('Brands db', () => { let _brand; let _user; diff --git a/src/__tests__/conversationMessageDB.test.js b/src/__tests__/conversationDb.test.js similarity index 84% rename from src/__tests__/conversationMessageDB.test.js rename to src/__tests__/conversationDb.test.js index 32876d590..05462146e 100644 --- a/src/__tests__/conversationMessageDB.test.js +++ b/src/__tests__/conversationDb.test.js @@ -4,14 +4,13 @@ import { connect, disconnect } from '../db/connection'; import { Conversations, ConversationMessages, Users } from '../db/models'; import { conversationFactory, conversationMessageFactory, userFactory } from '../db/factories'; -import conversationMutations from '../data/resolvers/mutations/conversations'; import { CONVERSATION_STATUSES } from '../data/constants'; beforeAll(() => connect()); afterAll(() => disconnect()); -describe('Conversation message db', () => { +describe('Conversation db', () => { let _conversation; let _conversationMessage; let _user; @@ -23,19 +22,8 @@ describe('Conversation message db', () => { _conversationMessage = await conversationMessageFactory(); _user = await userFactory(); - _doc = { - content: _conversationMessage.content, - attachments: _conversationMessage.attachments, - status: _conversationMessage.status, - mentionedUserIds: _conversationMessage.mentionedUserIds, - conversationId: _conversation._id, - internal: _conversationMessage.internal, - customerId: _conversationMessage.customerId, - isCustomerRead: _conversationMessage.isCustomerRead, - engageData: _conversationMessage.engageData, - formWidgetData: _conversationMessage.formWidgetData, - facebookData: _conversationMessage.facebookData, - }; + _doc = { ..._conversationMessage._doc, conversationId: _conversation._id }; + delete _doc['_id']; }); afterEach(async () => { @@ -147,13 +135,8 @@ describe('Conversation message db', () => { }); test('Conversation star', async () => { - await conversationMutations.conversationsStar( - {}, - { _ids: [_conversation._id] }, - { user: _user }, - ); + const user = await Conversations.starConversation([_conversation._id], _user._id); - const user = await Users.findOne({ _id: _user._id }); expect(user.details.starredConversationIds[0]).toBe(_conversation._id); }); @@ -171,9 +154,8 @@ describe('Conversation message db', () => { ); // unstar - await conversationMutations.conversationsUnstar({}, { _ids: ids }, { user: _user }); + const user = await Conversations.unstarConversation(ids, _user._id); - const user = await Users.findOne({ _id: _user._id }); expect(user.details.starredConversationIds.length).toBe(0); }); @@ -209,14 +191,14 @@ describe('Conversation message db', () => { expect(conversationObj.readUserIds[0]).toBe(_user._id); - const second_user = await userFactory(); + const secondUser = await userFactory(); // multiple users read conversation - await Conversations.markAsReadConversation(_conversation._id, second_user._id); + await Conversations.markAsReadConversation(_conversation._id, secondUser._id); try { // without conversation - await Conversations.markAsReadConversation('test', second_user._id); + await Conversations.markAsReadConversation('test', secondUser._id); } catch (e) { expect(e.message).toEqual('Conversation not found with id test'); } diff --git a/src/__tests__/conversationMessageMutations.test.js b/src/__tests__/conversationMutations.test.js similarity index 71% rename from src/__tests__/conversationMessageMutations.test.js rename to src/__tests__/conversationMutations.test.js index 77d541aed..f4977d2d0 100644 --- a/src/__tests__/conversationMessageMutations.test.js +++ b/src/__tests__/conversationMutations.test.js @@ -11,6 +11,7 @@ import { customerFactory, } from '../db/factories'; import conversationMutations from '../data/resolvers/mutations/conversations'; +import utils from '../data/utils'; beforeAll(() => connect()); @@ -37,19 +38,8 @@ describe('Conversation message mutations', () => { _conversation.assignedUserId = _user._id; await _conversation.save(); - _doc = { - content: _conversationMessage.content, - attachments: _conversationMessage.attachments, - status: _conversationMessage.status, - mentionedUserIds: _conversationMessage.mentionedUserIds, - conversationId: _conversation._id, - internal: _conversationMessage.internal, - customerId: _conversationMessage.customerId, - isCustomerRead: _conversationMessage.isCustomerRead, - engageData: _conversationMessage.engageData, - formWidgetData: _conversationMessage.formWidgetData, - facebookData: _conversationMessage.facebookData, - }; + _doc = { ..._conversationMessage._doc, conversationId: _conversation._id }; + delete _doc['_id']; }); afterEach(async () => { @@ -60,90 +50,92 @@ describe('Conversation message mutations', () => { }); test('Conversation login required functions', async () => { + const checkLogin = async (fn, args) => { + expect.assertions(8); + try { + await fn({}, args, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }; + + const _ids = { _ids: [_conversation._id] }; + expect.assertions(8); - try { - await conversationMutations.conversationMessageAdd({}, _doc, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + + // add message + checkLogin(conversationMutations.conversationMessageAdd, _doc); // assign - try { - await conversationMutations.conversationsAssign( - {}, - { conversationIds: [_conversation._id], assignedUserId: _user._id }, - {}, - ); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(conversationMutations.conversationsAssign, { + conversationIds: [_conversation._id], + assignedUserId: _user._id, + }); - // unassign - try { - await conversationMutations.conversationsUnassign( - {}, - { conversationIds: [_conversation._id], assignedUserId: _user._id }, - {}, - ); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + // assign + checkLogin(conversationMutations.conversationsUnassign, { + conversationIds: [_conversation._id], + assignedUserId: _user._id, + }); // change status - try { - await conversationMutations.conversationsChangeStatus({}, { _ids: [_conversation._id] }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(conversationMutations.conversationsChangeStatus, _ids); // conversation star - try { - await conversationMutations.conversationsStar({}, { _ids: [_conversation._id] }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(conversationMutations.conversationsStar, _ids); // conversation unstar - try { - await conversationMutations.conversationsUnstar({}, { _ids: [_conversation._id] }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(conversationMutations.conversationsUnstar, _ids); // add or remove participated users - try { - await conversationMutations.conversationsToggleParticipate( - {}, - { _ids: [_conversation._id] }, - {}, - ); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(conversationMutations.conversationsToggleParticipate, _ids); // mark conversation as read - try { - await conversationMutations.conversationMarkAsRead({}, { _ids: [_conversation._id] }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(conversationMutations.conversationMarkAsRead, _ids); }); test('Add conversation message', async () => { - expect.assertions(3); + expect.assertions(7); + ConversationMessages.addMessage = jest.fn(() => ({ _id: 'messageObject', })); + const spyNotification = jest.spyOn(utils, 'sendNotification'); + const spyEmail = jest.spyOn(utils, 'sendEmail'); + await conversationMutations.conversationMessageAdd({}, _doc, { user: _user }); expect(ConversationMessages.addMessage.mock.calls.length).toBe(1); expect(ConversationMessages.addMessage).toBeCalledWith(_doc, _user._id); + expect(spyNotification.mock.calls.length).toBe(1); + + // send notifincation + expect(spyNotification).toBeCalledWith({ + createdUser: _user._id, + notifType: 'conversationAddMessage', + title: 'You have a new message.', + content: _doc.content, + link: `/inbox/details/${_conversation._id}`, + receivers: [], + }); + // integration kind form test _doc['internal'] = false; await conversationMutations.conversationMessageAdd({}, _doc, { user: _user }); + expect(spyEmail.mock.calls.length).toBe(1); + + // send email + expect(spyEmail).toBeCalledWith({ + to: _customer.email, + title: 'Reply', + template: { + data: _doc.content, + }, + }); + try { // integration not found test _conversation.integrationId = 'test'; @@ -156,7 +148,9 @@ describe('Conversation message mutations', () => { // if user assigned to conversation test('Assign conversation to employee', async () => { - Conversations.assignUserConversation = jest.fn(); + Conversations.assignUserConversation = jest.fn(() => [_conversation]); + + const spyNotification = jest.spyOn(utils, 'sendNotification'); await conversationMutations.conversationsAssign( {}, @@ -166,6 +160,18 @@ describe('Conversation message mutations', () => { expect(Conversations.assignUserConversation.mock.calls.length).toBe(1); expect(Conversations.assignUserConversation).toBeCalledWith([_conversation._id], _user._id); + + const content = 'Assigned user has changed'; + + // send notifincation + expect(spyNotification).toBeCalledWith({ + createdUser: _user._id, + notifType: 'conversationAssigneeChange', + title: content, + content, + link: `/inbox/details/${_conversation._id}`, + receivers: [], + }); }); test('Unassign employee from conversation', async () => { diff --git a/src/__tests__/emailTemplateDb.test.js b/src/__tests__/emailTemplateDb.test.js index 563ec2037..28ce15ef6 100644 --- a/src/__tests__/emailTemplateDb.test.js +++ b/src/__tests__/emailTemplateDb.test.js @@ -9,7 +9,7 @@ beforeAll(() => connect()); afterAll(() => disconnect()); -describe('Email template mutations', () => { +describe('Email template db', () => { let _emailTemplate; beforeEach(async () => { diff --git a/src/__tests__/responseTemplateDb.test.js b/src/__tests__/responseTemplateDb.test.js index 01b5b9c30..7b72285ce 100644 --- a/src/__tests__/responseTemplateDb.test.js +++ b/src/__tests__/responseTemplateDb.test.js @@ -9,7 +9,7 @@ beforeAll(() => connect()); afterAll(() => disconnect()); -describe('Response template mutations', () => { +describe('Response template db', () => { let _responseTemplate; beforeEach(async () => { diff --git a/src/data/resolvers/mutations/brands.js b/src/data/resolvers/mutations/brands.js index 0da5bd328..476cf201e 100644 --- a/src/data/resolvers/mutations/brands.js +++ b/src/data/resolvers/mutations/brands.js @@ -32,8 +32,7 @@ export default { brandsRemove(root, { _id }, { user }) { if (!user) throw new Error('Login required'); - Brands.removeBrand(_id); - return 'done'; + return Brands.removeBrand(_id); }, /** diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 42b96bdef..d00d11350 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -1,7 +1,7 @@ import { Conversations, ConversationMessages, Integrations, Customers } from '../../../db/models'; import { pubsub } from '../subscriptions'; import { CONVERSATION_STATUSES, KIND_CHOICES } from '../../constants'; -import { sendEmail, sendNotification } from '../../utils'; +import utils from '../../utils'; import { _ } from 'underscore'; /** @@ -10,7 +10,7 @@ import { _ } from 'underscore'; * @param {String} currentUserId String * @return {list} userIds */ -const conversationNotifReceivers = (conversation, currentUserId) => { +export const conversationNotifReceivers = (conversation, currentUserId) => { let userIds = []; // assigned user can get notifications @@ -85,7 +85,7 @@ export default { const title = 'You have a new message.'; // send notification - sendNotification({ + utils.sendNotification({ createdUser: user._id, notifType: 'conversationAddMessage', title, @@ -119,7 +119,7 @@ export default { const email = customer ? customer.email : ''; if (kind === KIND_CHOICES.FORM && email) { - sendEmail({ + utils.sendEmail({ to: email, title: 'Reply', template: { @@ -148,28 +148,29 @@ export default { async conversationsAssign(root, { conversationIds, assignedUserId }, { user }) { if (!user) throw new Error('Login required'); - await Conversations.assignUserConversation(conversationIds, assignedUserId); + const updatedConversations = await Conversations.assignUserConversation( + conversationIds, + assignedUserId, + ); // notify graphl subscription await conversationsChanged(conversationIds, 'statusChanged'); - const updatedConversations = await Conversations.find({ _id: { $in: conversationIds } }); - for (let conversation of updatedConversations) { const content = 'Assigned user has changed'; // send notification - sendNotification({ + utils.sendNotification({ createdUser: user._id, notifType: 'conversationAssigneeChange', title: content, content, link: `/inbox/details/${conversation._id}`, - receivers: conversationNotifReceivers(conversation, this.userId), + receivers: conversationNotifReceivers(conversation, user._id), }); } - return 'done'; + return updatedConversations; }, /** @@ -180,12 +181,12 @@ export default { async conversationsUnassign(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); - await Conversations.unassignUserConversation(_ids); + const conversations = await Conversations.unassignUserConversation(_ids); // notify graphl subscription conversationsChanged(_ids, 'statusChanged'); - return 'done'; + return conversations; }, /** @@ -199,7 +200,7 @@ export default { const { conversations } = await Conversations.checkExistanceConversations(_ids); - await Conversations.changeStatusConversation(_ids, status); + const changesConversations = await Conversations.changeStatusConversation(_ids, status); // notify graphl subscription await conversationsChanged(_ids, 'statusChanged'); @@ -213,7 +214,7 @@ export default { if (notifyCustomer && customer.email) { // send email to customer - sendEmail({ + utils.sendEmail({ to: customer.email, subject: 'Conversation detail', templateArgs: { @@ -232,7 +233,7 @@ export default { const content = 'Conversation status has changed.'; - sendNotification({ + utils.sendNotification({ createdUser: user._id, notifType: 'conversationStateChange', title: content, @@ -242,7 +243,7 @@ export default { }); } - return 'done'; + return changesConversations; }, /** @@ -253,9 +254,7 @@ export default { async conversationsStar(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); - await Conversations.starConversation(_ids, user._id); - - return 'done'; + return Conversations.starConversation(_ids, user._id); }, /** @@ -266,9 +265,7 @@ export default { async conversationsUnstar(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); - await Conversations.unstarConversation(_ids, user._id); - - return 'done'; + return Conversations.unstarConversation(_ids, user._id); }, /** @@ -279,24 +276,22 @@ export default { async conversationsToggleParticipate(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); - await Conversations.toggleParticipatedUsers(_ids, user._id); + const conversations = await Conversations.toggleParticipatedUsers(_ids, user._id); // notify graphl subscription conversationsChanged(_ids, 'participatedStateChanged'); - return 'done'; + return conversations; }, /** * Conversation mark as read - * @param {list} _ids of conversation + * @param {String} _id of conversation * @return {Promise} String */ async conversationMarkAsRead(root, { _id }, { user }) { if (!user) throw new Error('Login required'); - await Conversations.markAsReadConversation(_id, user._id); - - return 'done'; + return Conversations.markAsReadConversation(_id, user._id); }, }; diff --git a/src/data/schema/conversation.js b/src/data/schema/conversation.js index f0307ab82..865f5f0ae 100644 --- a/src/data/schema/conversation.js +++ b/src/data/schema/conversation.js @@ -89,13 +89,12 @@ export const queries = ` `; export const mutations = ` - conversationMessageAdd(params: ConversationMessageParams): String - conversationsCheckExistance(_ids: [String]!): String - conversationsAssign(conversationIds: [String]!, assignedUserId: String): String - conversationsUnassign(_ids: [String]!): String - conversationsChangeStatus(_ids: [String]!): String - conversationsStar(_ids: [String]!): String - conversationsUnstar(_ids: [String]!): String - conversationsToggleParticipate(_ids: [String]!): String - conversationMarkAsRead(_id: String): String + conversationMessageAdd(params: ConversationMessageParams): ConversationMessage + conversationsAssign(conversationIds: [String]!, assignedUserId: String): [Conversation] + conversationsUnassign(_ids: [String]!): [Conversation] + conversationsChangeStatus(_ids: [String]!): [Conversation] + conversationsStar(_ids: [String]!): User + conversationsUnstar(_ids: [String]!): User + conversationsToggleParticipate(_ids: [String]!): Conversation + conversationMarkAsRead(_id: String): Conversation `; diff --git a/src/data/utils.js b/src/data/utils.js index 2a85cc4ec..596029f1e 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -12,6 +12,7 @@ import { Channels, Notifications, Users } from '../db/models'; * @param {Boolean} args.isCustom * @return {Promise} null */ + export const sendEmail = async ({ toEmails, fromEmail, title, templateArgs }) => { // TODO: test this method const { MAIL_SERVICE, MAIL_USER, MAIL_PASS, NODE_ENV } = process.env; @@ -119,3 +120,9 @@ export const sendNotification = async ({ createdUser, receivers, ...doc }) => { }, }); }; + +export default { + sendEmail, + sendNotification, + sendChannelNotifications, +}; diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index 6c2e0a19c..12be44de2 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -110,8 +110,7 @@ class Conversation { /** * Check conversations exists * @param {list} ids of conversations - * @return {object} selector - * @return {list} conversations object list + * @return {object, list} selector, conversations */ static async checkExistanceConversations(_ids) { const selector = { _id: { $in: _ids } }; @@ -158,7 +157,7 @@ class Conversation { { multi: true }, ); - return Conversations.find({ _id: { $in: conversationIds } }); + return this.find({ _id: { $in: conversationIds } }); } /** @@ -169,11 +168,13 @@ class Conversation { static async unassignUserConversation(conversationIds) { await this.checkExistanceConversations(conversationIds); - return this.update( + await this.update( { _id: { $in: conversationIds } }, { $unset: { assignedUserId: 1 } }, { multi: true }, ); + + return this.find({ _id: { $in: conversationIds } }); } /** @@ -189,7 +190,8 @@ class Conversation { /** * Star conversation * @param {list} _ids of conversations - * @param {string} userId + * @param {String} userId + * @return {Promise} user object */ static async starConversation(_ids, userId) { await this.checkExistanceConversations(_ids); @@ -202,12 +204,15 @@ class Conversation { }, }, ); + + return Users.findOne({ _id: userId }); } /** * Unstar conversation * @param {list} _ids of conversations * @param {string} userId + * @return {Promise} user object */ static async unstarConversation(_ids, userId) { // check conversations existance @@ -221,13 +226,15 @@ class Conversation { }, }, ); + + return Users.findOne({ _id: userId }); } /** * Add participated user to conversation - * @param {Object} selector + * @param {list} _ids * @param {String} userId - * @return {Promise} Updated conversation id + * @return {Promise} Updated conversation list */ static async toggleParticipatedUsers(_ids, userId) { const { selector } = await this.checkExistanceConversations(_ids); @@ -252,6 +259,7 @@ class Conversation { { multi: true }, ); } + return this.find({ _id: { $in: _ids } }); } /** @@ -261,7 +269,7 @@ class Conversation { * @return {Promise} Updated conversation id */ static async markAsReadConversation(_id, userId) { - const conversation = await Conversations.findOne({ _id }); + const conversation = await this.findOne({ _id }); if (!conversation) throw new Error(`Conversation not found with id ${_id}`); @@ -269,13 +277,15 @@ class Conversation { // if current user is first one if (!readUserIds || readUserIds.length === 0) { - return this.update({ _id }, { $set: { readUserIds: [userId] } }); + await this.update({ _id }, { $set: { readUserIds: [userId] } }); } // if current user is not in read users list then add it if (!readUserIds.includes(userId)) { - return this.update({ _id }, { $push: { readUserIds: userId } }); + await this.update({ _id }, { $push: { readUserIds: userId } }); } + + return this.findOne({ _id }); } } From 75be6be462237280704012f3e2fefb1e4f28ecc0 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Mon, 16 Oct 2017 18:07:11 +0800 Subject: [PATCH 082/318] #16 Add coverage --- src/__tests__/channelDb.test.js | 31 ++++++++ src/__tests__/channelMutations.test.js | 85 ++++++++++++---------- src/__tests__/integrationDb.test.js | 20 ++++- src/__tests__/integrationMutations.test.js | 6 +- src/__tests__/notificationDb.test.js | 25 +++++++ src/__tests__/notificationTools.test.js | 56 +++++++++++--- src/data/resolvers/mutations/channels.js | 8 +- src/data/utils/channel.js | 30 ++++++++ src/data/utils/index.js | 7 ++ src/data/{ => utils}/utils.js | 37 ++-------- src/db/factories.js | 42 ++++++++++- src/db/models/Channels.js | 11 +-- src/db/plugins/createdAtModifier.js | 2 +- 13 files changed, 263 insertions(+), 97 deletions(-) create mode 100644 src/data/utils/channel.js create mode 100644 src/data/utils/index.js rename src/data/{ => utils}/utils.js (74%) diff --git a/src/__tests__/channelDb.test.js b/src/__tests__/channelDb.test.js index bcc5c24f3..61e615890 100644 --- a/src/__tests__/channelDb.test.js +++ b/src/__tests__/channelDb.test.js @@ -4,6 +4,9 @@ import { connect, disconnect } from '../db/connection'; import { userFactory, integrationFactory } from '../db/factories'; import { Channels, Users, Integrations } from '../db/models'; +import toBeType from 'jest-tobetype'; + +expect.extend(toBeType); beforeAll(() => connect()); afterAll(() => disconnect()); @@ -153,3 +156,31 @@ describe('channel remove', () => { expect(channelCount).toBe(0); }); }); + +describe('test createdAtModifier', () => { + let _channel; + + beforeEach(async () => { + const user = await userFactory({}); + _channel = await Channels.createChannel( + { + name: 'Channel test', + }, + user._id, + ); + }); + + afterEach(async () => { + await Channels.remove({}); + }); + + test('test whether createdAtModifier is working properly', async () => { + expect(_channel.createdAt).toBeType('object'); + + const createdAt = _channel.createdAt; + + const updatedChannel = await Channels.updateChannel(_channel._id, { name: 'Channel test 2' }); + + expect(updatedChannel.createdAt).toEqual(createdAt); + }); +}); diff --git a/src/__tests__/channelMutations.test.js b/src/__tests__/channelMutations.test.js index 06b4dcd11..937a753bd 100644 --- a/src/__tests__/channelMutations.test.js +++ b/src/__tests__/channelMutations.test.js @@ -5,6 +5,7 @@ import { connect, disconnect } from '../db/connection'; import { userFactory } from '../db/factories'; import { Channels, Users } from '../db/models'; import channelMutations from '../data/resolvers/mutations/channels'; +import utils from '../data/utils'; beforeAll(() => connect()); afterAll(() => disconnect()); @@ -12,6 +13,7 @@ afterAll(() => disconnect()); describe('mutations', () => { const _channelId = 'fakeChannelId'; let _user; + let _user2Id = 'fakeuserId2'; beforeEach(async () => { _user = await userFactory({}); @@ -44,8 +46,6 @@ describe('mutations', () => { }); test('test mutations.channelsCreate', async () => { - expect.assertions(2); - let doc = { name: 'Channel test', description: 'test channel descripion', @@ -53,54 +53,63 @@ describe('mutations', () => { integrationIds: ['fakeIntegrationId'], }; - Channels.createChannel = jest.fn(); + let notifDoc = { + userId: _user._id, + memberIds: [_user._id, _user2Id], + _id: _channelId, + }; - try { - await channelMutations.channelsCreate(null, doc, { user: _user }); - } catch (e) { - /* this error is caused by Channels.createChannel mock function; - sendChannelNotifications method further in the workflow was using - the object returned by Channels.createChannel, since we mocked it, - returns null */ - if (e.message === `Cannot read property 'userId' of undefined`) { - expect(Channels.createChannel).toBeCalledWith(doc, _user); - expect(Channels.createChannel.mock.calls.length).toBe(1); - } - } + Channels.createChannel = jest.fn(() => notifDoc); + + jest.spyOn(utils, 'sendChannelNotifications').mockImplementation(() => ({})); + + await channelMutations.channelsCreate(null, doc, { user: _user }); + + expect(Channels.createChannel).toBeCalledWith(doc, _user); + expect(Channels.createChannel.mock.calls.length).toBe(1); + + expect(utils.sendChannelNotifications).toBeCalledWith({ + userId: _user._id, + memberIds: [_user._id, _user2Id], + channelId: _channelId, + }); + expect(utils.sendChannelNotifications.mock.calls.length).toBe(1); }); test('test mutations.channelsUpdate', async () => { const doc = { name: 'Channel test 1', description: 'Channel test description 1', - userId: 'fakeUserId1', memberIds: ['fakeUserId2'], integrationIds: ['integrationIds1'], }; - Channels.updateChannel = jest.fn(); + let notifDoc = { + userId: _user._id, + memberIds: [_user._id, _user2Id], + _id: _channelId, + }; - try { - await channelMutations.channelsEdit( - null, - { - ...doc, - _id: _channelId, - }, - { user: _user }, - ); - } catch (e) { - /* this error is caused by Channels.updateChannel mock function; - sendChannelNotifications method further in the workflow was using - the object returned by Channels.updateChannel, since we mocked it, - returns null */ - if (e.message === `Cannot read property '_id' of undefined`) { - expect(Channels.updateChannel).toBeCalledWith(_channelId, doc); - expect(Channels.updateChannel.mock.calls.length).toBe(1); - } else { - throw e; - } - } + Channels.updateChannel = jest.fn(() => notifDoc); + + await channelMutations.channelsEdit( + null, + { + ...doc, + _id: _channelId, + }, + { user: _user }, + ); + + expect(Channels.updateChannel).toBeCalledWith(_channelId, doc); + expect(Channels.updateChannel.mock.calls.length).toBe(1); + + expect(utils.sendChannelNotifications).toBeCalledWith({ + userId: _user._id, + memberIds: [_user._id, _user2Id], + channelId: _channelId, + }); + expect(utils.sendChannelNotifications.mock.calls.length).toBe(2); }); test('test mutations.channelsRemove', async () => { diff --git a/src/__tests__/integrationDb.test.js b/src/__tests__/integrationDb.test.js index 7c24f87e4..801581ef2 100644 --- a/src/__tests__/integrationDb.test.js +++ b/src/__tests__/integrationDb.test.js @@ -4,8 +4,15 @@ import faker from 'faker'; import { connect, disconnect } from '../db/connection'; import { KIND_CHOICES, FORM_LOAD_TYPES, MESSENGER_DATA_AVAILABILITY } from '../data/constants'; -import { brandFactory, integrationFactory, formFactory, userFactory } from '../db/factories'; -import { Integrations, Brands, Users, Forms } from '../db/models'; +import { + brandFactory, + integrationFactory, + formFactory, + userFactory, + messageFactory, + conversationFactory, +} from '../db/factories'; +import { Integrations, Brands, Users, Forms, ConversationMessages } from '../db/models'; beforeAll(() => connect()); afterAll(() => disconnect()); @@ -202,20 +209,28 @@ describe('edit form integration', () => { describe('remove integration model method test', () => { let _brand; let _integration; + let _conversation; beforeEach(async () => { _brand = await brandFactory({}); + _integration = await integrationFactory({ name: 'form integration test', brandId: _brand._id, kind: 'form', }); + + _conversation = await conversationFactory({ integrationId: _integration._id }); + + await messageFactory({ conversationId: _conversation._id }); + await messageFactory({ conversationId: _conversation._id }); }); afterEach(async () => { await Brands.remove({}); await Integrations.remove({}); await Users.remove({}); + await ConversationMessages.remove({}); }); test('test if remove form integration model method is working successfully', async () => { @@ -224,6 +239,7 @@ describe('remove integration model method test', () => { const integrationCount = await Integrations.find({}).count(); expect(integrationCount).toEqual(0); + expect(await ConversationMessages.find({}).count()).toBe(0); }); }); diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js index a6c6e0d46..fbe3795b8 100644 --- a/src/__tests__/integrationMutations.test.js +++ b/src/__tests__/integrationMutations.test.js @@ -26,7 +26,7 @@ describe('mutations', () => { }); test('test if `logging required` error is working as intended', () => { - expect.assertions(6); + expect.assertions(7); // Login required ================== expect(() => @@ -37,6 +37,10 @@ describe('mutations', () => { integrationMutations.integrationsEditMessengerIntegration(null, {}, {}), ).toThrowError('Login required'); + expect(() => integrationMutations.integrationsSaveMessengerConfigs(null, {}, {})).toThrowError( + 'Login required', + ); + expect(() => integrationMutations.integrationsSaveMessengerAppearanceData(null, {}, {}), ).toThrowError('Login required'); diff --git a/src/__tests__/notificationDb.test.js b/src/__tests__/notificationDb.test.js index 1563e96af..ed45129d3 100644 --- a/src/__tests__/notificationDb.test.js +++ b/src/__tests__/notificationDb.test.js @@ -24,6 +24,16 @@ describe('Notification model tests', () => { Users.remove({}); }); + test(`check whether Error('createdUser must be supplied') is being thrown as intended`, async () => { + expect.assertions(1); + + try { + await Notifications.createNotification({}); + } catch (e) { + expect(e.message).toBe('createdUser must be supplied'); + } + }); + test('check for error in model creation', async () => { expect.assertions(1); @@ -105,6 +115,21 @@ describe('Notification model tests', () => { }); describe('NotificationConfiguration model tests', async () => { + test(`check whether Error('user must be supplied') is being thrown as intended`, async () => { + expect.assertions(1); + + const doc = { + notifType: MODULES.CONVERSATION_ADD_MESSAGE, + isAllowed: true, + }; + + try { + await NotificationConfigurations.createOrUpdateConfiguration(doc); + } catch (e) { + expect(e.message).toBe('user must be supplied'); + } + }); + test('test if model methods are working correctly', async () => { // creating new notification configuration ========== const user = await userFactory({}); diff --git a/src/__tests__/notificationTools.test.js b/src/__tests__/notificationTools.test.js index f8765e538..2bd6a5724 100644 --- a/src/__tests__/notificationTools.test.js +++ b/src/__tests__/notificationTools.test.js @@ -2,20 +2,31 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { userFactory, notificationConfigurationFactory } from '../db/factories'; +import { userFactory, notificationConfigurationFactory, channelFactory } from '../db/factories'; import { MODULES } from '../data/constants'; -import { sendNotification } from '../data/utils'; -import { Notifications, NotificationConfigurations } from '../db/models'; +import utils from '../data/utils'; +import utils2 from '../data/utils/utils'; +import { Notifications, NotificationConfigurations, Users } from '../db/models'; beforeAll(() => connect()); afterAll(() => disconnect()); describe('testings helper methods', () => { - test('testing tools.sendNotification method', async () => { - const _user = await userFactory({}); - const _user2 = await userFactory({}); - const _user3 = await userFactory({}); + let _user; + let _user2; + let _user3; + + beforeEach(async () => { + _user = await userFactory({}); + _user2 = await userFactory({}); + _user3 = await userFactory({}); + }); + afterEach(async () => { + await Users.remove({}); + }); + + test('testing tools.sendNotification method', async () => { // Try to send notifications when there is config not allowing it ========= await notificationConfigurationFactory({ notifType: MODULES.CHANNEL_MEMBERS_CHANGE, @@ -44,7 +55,7 @@ describe('testings helper methods', () => { receivers: [_user._id, _user2._id, _user3._id], }; - await sendNotification(doc); + await utils.sendNotification(doc); let notifications = await Notifications.find({}); expect(notifications.length).toEqual(0); @@ -52,7 +63,7 @@ describe('testings helper methods', () => { // Send notifications when there is config allowing it ==================== await NotificationConfigurations.update({}, { isAllowed: true }, { multi: true }); - await sendNotification(doc); + await utils.sendNotification(doc); notifications = await Notifications.find({}); @@ -69,4 +80,31 @@ describe('testings helper methods', () => { expect(notifications[2].receiver).toEqual(_user3._id); }); + + test('test tools.sendChannelNotifications', async () => { + const channel = await channelFactory({}); + + const doc = { + channelId: channel._id, + memberIds: [_user._id, _user2._id, _user3._id], + userId: _user._id, + }; + + const content = `You have invited to '${channel.name}' channel.`; + + jest.spyOn(utils2, 'sendNotification').mockImplementation(() => ({})); + + await utils.sendChannelNotifications(doc); + + expect(utils2.sendNotification).toBeCalledWith({ + createdUser: doc.userId, + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + title: content, + content, + link: `/inbox/${channel._id}`, + receivers: doc.memberIds.filter(id => id !== doc.userId), + }); + + expect(utils2.sendNotification.mock.calls.length).toBe(1); + }); }); diff --git a/src/data/resolvers/mutations/channels.js b/src/data/resolvers/mutations/channels.js index 1f77edfec..a9227422d 100644 --- a/src/data/resolvers/mutations/channels.js +++ b/src/data/resolvers/mutations/channels.js @@ -1,5 +1,5 @@ import { Channels } from '../../../db/models'; -import { sendChannelNotifications } from '../../utils'; +import utils from '../../utils'; export default { /** @@ -21,7 +21,7 @@ export default { const channel = await Channels.createChannel(doc, user); - await sendChannelNotifications({ + await utils.sendChannelNotifications({ userId: channel.userId, memberIds: channel.memberIds, channelId: channel._id, @@ -51,10 +51,10 @@ export default { const channel = Channels.updateChannel(_id, doc); - await sendChannelNotifications({ + await utils.sendChannelNotifications({ channelId: channel._id, memberIds: channel.memberIds, - userId: user, + userId: channel.userId, }); return channel; diff --git a/src/data/utils/channel.js b/src/data/utils/channel.js new file mode 100644 index 000000000..2053bb5bc --- /dev/null +++ b/src/data/utils/channel.js @@ -0,0 +1,30 @@ +import { MODULES } from '../constants'; +import { Channels } from '../../db/models'; +import utils from './utils'; + +/** + * Send notification to all members of this channel except the sender + * @param {Object} object - Object + * @param {string} object.channelId - Channel id + * @param {Array} object.memberIds - Members of the channel + * @param {string} object.userId - Sender of the notification + * @return {Promise} + */ +export default async ({ channelId, memberIds, userId }) => { + memberIds = memberIds || []; + + const channel = await Channels.findOne({ _id: channelId }); + + const content = `You have invited to '${channel.name}' channel.`; + + return utils.sendNotification({ + createdUser: userId, + notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + title: content, + content, + link: `/inbox/${channel._id}`, + + // exclude current user + receivers: memberIds.filter(id => id !== userId), + }); +}; diff --git a/src/data/utils/index.js b/src/data/utils/index.js new file mode 100644 index 000000000..a73f53434 --- /dev/null +++ b/src/data/utils/index.js @@ -0,0 +1,7 @@ +import utils from './utils'; +import sendChannelNotifications from './channel'; + +export default { + ...utils, + sendChannelNotifications, +}; diff --git a/src/data/utils.js b/src/data/utils/utils.js similarity index 74% rename from src/data/utils.js rename to src/data/utils/utils.js index b9e8f19f8..463e52097 100644 --- a/src/data/utils.js +++ b/src/data/utils/utils.js @@ -1,8 +1,7 @@ import nodemailer from 'nodemailer'; import Handlebars from 'handlebars'; import readFile from 'fs-readfile-promise'; -import { MODULES } from './constants'; -import { Channels, Notifications, Users } from '../db/models'; +import { Notifications, Users } from '../../db/models'; /** * SendEmail template helper @@ -30,7 +29,7 @@ const applyTemplate = async (data, templateName) => { * @param {Boolean} args.isCustom * @return {Promise} */ -export const sendEmail = async ({ toEmails, fromEmail, title, template }) => { +const sendEmail = async ({ toEmails, fromEmail, title, template }) => { const { MAIL_SERVICE, MAIL_USER, MAIL_PASS, NODE_ENV } = process.env; // do not send email it is running in test mode if (NODE_ENV == 'test') { @@ -69,32 +68,6 @@ export const sendEmail = async ({ toEmails, fromEmail, title, template }) => { }); }; -/** - * Send notification to all members of this channel except the sender - * @param {String} channelId - * @param {Array} memberIds - * @param {String} userId - * @return {Promise} - */ -export const sendChannelNotifications = async ({ channelId, memberIds, userId }) => { - memberIds = memberIds || []; - - const channel = await Channels.findOne({ _id: channelId }); - - const content = `You have invited to '${channel.name}' channel.`; - - return sendNotification({ - createdUser: userId, - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, - title: content, - content, - link: `/inbox/${channel._id}`, - - // exclude current user - receivers: memberIds.filter(id => id !== userId), - }); -}; - /** * Send a notification * @param {String} doc.notifType @@ -105,7 +78,7 @@ export const sendChannelNotifications = async ({ channelId, memberIds, userId }) * @param {Array} doc.receivers Array of user ids * @return {Promise} */ -export const sendNotification = async ({ createdUser, receivers, ...doc }) => { +const sendNotification = async ({ createdUser, receivers, ...doc }) => { // collecting emails const recipients = await Users.find({ _id: { $in: receivers } }); @@ -141,3 +114,7 @@ export const sendNotification = async ({ createdUser, receivers, ...doc }) => { }, }); }; + +export default { + sendNotification, +}; diff --git a/src/db/factories.js b/src/db/factories.js index 138d67a30..3d552acf2 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -13,6 +13,9 @@ import { FormFields, NotificationConfigurations, Notifications, + ConversationMessages, + Conversations, + Channels, } from './models'; export const userFactory = (params = {}) => { @@ -148,7 +151,42 @@ export function messageFactory(params = {}) { }, params, ); - const message = new Messages(obj); + return ConversationMessages.create(obj); +} + +export function conversationFactory(params = {}) { + const obj = Object.assign( + { + createdAt: faker.date.past(), + content: faker.lorem.sentence, + customerId: Random.id(), + integrationId: Random.id(), + number: 1, + messageCount: 0, + status: faker.random.word, + }, + params, + ); + + return Conversations.create(obj); +} + +export async function channelFactory(params = {}) { + const user = await userFactory({}); + + const obj = Object.assign( + { + name: faker.random.word(), + description: faker.lorem.sentence, + integrationIds: [], + memberIds: [user._id], + userId: user._id, + conversationCount: 0, + openConversationCount: 0, + createdAt: new Date(), + }, + params, + ); - return message.save(); + return Channels.create(obj); } diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index 3ce30d7ca..eefa1a896 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -4,10 +4,7 @@ import { createdAtModifier } from '../plugins'; // schema for Channels const ChannelSchema = mongoose.Schema({ - _id: { - type: String, - default: () => Random.id(), - }, + _id: { type: String, unique: true, default: () => Random.id() }, name: String, description: { type: String, @@ -78,12 +75,6 @@ class Channel { * @return {Promise} returns Promise resolving updated channel document */ static async updateChannel(_id, doc) { - const { userId } = doc; - - if (userId && userId._id) { - doc.userId = doc.userId._id; - } - this.preSave(doc); await this.update({ _id }, { $set: doc }, { runValidators: true }); diff --git a/src/db/plugins/createdAtModifier.js b/src/db/plugins/createdAtModifier.js index 0120d6049..0152aca6e 100644 --- a/src/db/plugins/createdAtModifier.js +++ b/src/db/plugins/createdAtModifier.js @@ -4,7 +4,7 @@ export const createdAtModifier = schema => { }); schema.pre('save', function(next) { - if (this._id == undefined) { + if (this.createdAt == undefined) { this.createdAt = new Date(); } next(); From badc5b893064273a217d08dfb3b9a1026fab7a5c Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Mon, 16 Oct 2017 18:13:31 +0800 Subject: [PATCH 083/318] Tag, Engage test coverage --- src/__tests__/engageMessageMutations.test.js | 68 +++++++++------- src/__tests__/tagDb.test.js | 36 ++++++++- src/__tests__/tagMutations.test.js | 81 ++++++++++++++------ src/data/resolvers/mutations/engages.js | 10 +-- src/data/resolvers/mutations/tags.js | 6 +- src/data/schema/engage.js | 2 +- src/data/schema/tag.js | 2 +- src/db/models/Engages.js | 23 +++++- src/db/models/Tags.js | 59 ++++++++------ 9 files changed, 201 insertions(+), 86 deletions(-) diff --git a/src/__tests__/engageMessageMutations.test.js b/src/__tests__/engageMessageMutations.test.js index 08c4a991f..67e68d0c2 100644 --- a/src/__tests__/engageMessageMutations.test.js +++ b/src/__tests__/engageMessageMutations.test.js @@ -35,40 +35,61 @@ describe('mutations', () => { await EngageMessages.remove({}); }); - test('messages create', async () => { - EngageMessages.createEngageMessage = jest.fn(); - await mutations.engageMessageAdd(null, _doc, { user: _user }); + test('Check login required', async () => { + expect.assertions(6); + + const check = async fn => { + try { + await fn({}, {}, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }; - expect(EngageMessages.createEngageMessage).toBeCalledWith(_doc); - expect(EngageMessages.createEngageMessage.mock.calls.length).toBe(1); + // add + check(mutations.engageMessageAdd); + + // edit + check(mutations.engageMessageEdit); + + // remove + check(mutations.engageMessageRemove); + + // set live + check(mutations.engageMessageSetLive); + + // set pause + check(mutations.engageMessageSetPause); + + // set live manual + check(mutations.engageMessageSetLiveManual); }); - test('Create message login required', async () => { + test('Engage message remove not found', async () => { expect.assertions(1); try { - await mutations.engageMessageAdd({}, _doc, {}); + await mutations.engageMessageRemove({}, `${_message._id}-`, { user: _user }); } catch (e) { - expect(e.message).toEqual('Login required'); + expect(e.message).toEqual(`Engage message not found with id ${_message._id}-`); } }); + test('messages create', async () => { + EngageMessages.createEngageMessage = jest.fn(); + await mutations.engageMessageAdd(null, _doc, { user: _user }); + + expect(EngageMessages.createEngageMessage).toBeCalledWith(_doc); + expect(EngageMessages.createEngageMessage.mock.calls.length).toBe(1); + }); + test('messages update', async () => { EngageMessages.updateEngageMessage = jest.fn(); - await mutations.engageMessageUpdate(null, { _id: _message._id, ..._doc }, { user: _user }); + await mutations.engageMessageEdit(null, { _id: _message._id, ..._doc }, { user: _user }); expect(EngageMessages.updateEngageMessage).toBeCalledWith(_message._id, _doc); expect(EngageMessages.updateEngageMessage.mock.calls.length).toBe(1); }); - test('Update message login required', async () => { - expect.assertions(1); - try { - await mutations.engageMessageUpdate({}, { _id: _message._id, ..._doc }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } - }); - test('messages remove', async () => { EngageMessages.removeEngageMessage = jest.fn(); await mutations.engageMessageRemove(null, _message._id, { user: _user }); @@ -76,15 +97,6 @@ describe('mutations', () => { expect(EngageMessages.removeEngageMessage.mock.calls.length).toBe(1); }); - test('Remove message login required', async () => { - expect.assertions(1); - try { - await mutations.engageMessageRemove({}, _message._id, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } - }); - test('set live', async () => { EngageMessages.engageMessageSetLive = jest.fn(); await mutations.engageMessageSetLive(null, _message._id, { user: _user }); @@ -103,7 +115,7 @@ describe('mutations', () => { test('set live manual', async () => { EngageMessages.engageMessageSetLive = jest.fn(); - await mutations.engageMessageSetLive(null, _message._id, { user: _user }); + await mutations.engageMessageSetLiveManual(null, _message._id, { user: _user }); expect(EngageMessages.engageMessageSetLive).toBeCalledWith(_message._id); expect(EngageMessages.engageMessageSetLive.mock.calls.length).toBe(1); diff --git a/src/__tests__/tagDb.test.js b/src/__tests__/tagDb.test.js index 7c96cf5ea..77d26cc1e 100644 --- a/src/__tests__/tagDb.test.js +++ b/src/__tests__/tagDb.test.js @@ -2,7 +2,8 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { Tags, Users, EngageMessages } from '../db/models'; +import { Tags, EngageMessages } from '../db/models'; +import { validateUniqueness, tagObject } from '../db/models/Tags'; import { tagsFactory, engageMessageFactory } from '../db/factories'; beforeAll(() => connect()); @@ -11,11 +12,13 @@ afterAll(() => disconnect()); describe('Tags model', () => { let _tag; + let _tag2; let _message; beforeEach(async () => { // Creating test data _tag = await tagsFactory(); + _tag2 = await tagsFactory(); _message = await engageMessageFactory({}); }); @@ -25,6 +28,37 @@ describe('Tags model', () => { await EngageMessages.remove({}); }); + test('Validate unique tag', async () => { + const empty = await validateUniqueness({}, {}); + const selectTag = await validateUniqueness( + { type: _tag2.type }, + { name: 'new tag', type: _tag2.type }, + ); + const existing = await validateUniqueness({}, _tag); + + expect(empty).toEqual(true); + expect(selectTag).toEqual(false); + expect(existing).toEqual(false); + }); + + test('Tag not found', async () => { + expect.assertions(1); + try { + await tagObject({ + tagIds: [_tag._id], + objectIds: [], + EngageMessages, + tagType: 'customer', + }); + } catch (e) { + expect(e.message).toEqual('Tag not found.'); + } + }); + + test('Attach tag type', async () => { + Tags.tagsTag('customer', [], []); + }); + test('Create tag', async () => { const tagObj = await Tags.createTag({ name: `${_tag.name}1`, diff --git a/src/__tests__/tagMutations.test.js b/src/__tests__/tagMutations.test.js index f86447722..fa0c4b5ef 100644 --- a/src/__tests__/tagMutations.test.js +++ b/src/__tests__/tagMutations.test.js @@ -12,6 +12,7 @@ afterAll(() => disconnect()); describe('Tags mutations', () => { let _tag; + let _tag2; let _user; let _message; let doc; @@ -19,6 +20,7 @@ describe('Tags mutations', () => { beforeEach(async () => { // Creating test data _tag = await tagsFactory(); + _tag2 = await tagsFactory(); _user = await userFactory(); _message = await engageMessageFactory({}); @@ -36,6 +38,47 @@ describe('Tags mutations', () => { await EngageMessages.remove({}); }); + test('Check login required', async () => { + expect.assertions(4); + + const check = async fn => { + try { + await fn({}, {}, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }; + + // add + check(tagsMutations.tagsAdd); + + // edit + check(tagsMutations.tagsEdit); + + // remove + check(tagsMutations.tagsRemove); + + // tags tag + check(tagsMutations.tagsTag); + }); + + test('Check tag duplicated', async () => { + expect.assertions(2); + const check = async (mutation, doc) => { + try { + await mutation({}, doc, { user: _user }); + } catch (e) { + expect(e.message).toEqual('Tag duplicated'); + } + }; + + // add + await check(tagsMutations.tagsAdd, _tag2); + + // edit + await check(tagsMutations.tagsEdit, { _id: _tag2._id, name: _tag.name, type: _tag.type }); + }); + test('Create tag', async () => { Tags.createTag = jest.fn(); await tagsMutations.tagsAdd({}, doc, { user: _user }); @@ -44,29 +87,30 @@ describe('Tags mutations', () => { expect(Tags.createTag.mock.calls.length).toBe(1); }); - test('Create tag login required', async () => { - expect.assertions(1); - try { - await tagsMutations.tagsAdd({}, { type: _tag.type, name: _tag.name }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } - }); - test('Update tag', async () => { Tags.updateTag = jest.fn(); - await tagsMutations.tagsUpdate(null, { _id: _tag._id, ...doc }, { user: _user }); + await tagsMutations.tagsEdit(null, { _id: _tag._id, ...doc }, { user: _user }); expect(Tags.updateTag).toBeCalledWith(_tag._id, doc); expect(Tags.updateTag.mock.calls.length).toBe(1); }); - test('Update tag login required', async () => { + test('Remove tag not found', async () => { + expect.assertions(1); + try { + await tagsMutations.tagsRemove({}, { ids: [_message._id] }, { user: _user }); + } catch (e) { + expect(e.message).toEqual('Tag not found'); + } + }); + + test("Can't remove a tag", async () => { expect.assertions(1); try { - await tagsMutations.tagsUpdate({}, { _id: _tag.id, name: _tag.name }, {}); + await EngageMessages.update({ _id: _message._id }, { $set: { tagIds: [_tag._id] } }); + await tagsMutations.tagsRemove({}, { ids: [_tag._id] }, { user: _user }); } catch (e) { - expect(e.message).toEqual('Login required'); + expect(e.message).toEqual("Can't remove a tag with tagged object(s)"); } }); @@ -77,23 +121,14 @@ describe('Tags mutations', () => { expect(Tags.removeTag.mock.calls.length).toBe(1); }); - test('Remove tag login required', async () => { - expect.assertions(1); - try { - await tagsMutations.tagsRemove({}, { ids: [_tag.id] }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } - }); - test('Tags tag', async () => { + Tags.tagsTag = jest.fn(); const tagObj = { type: 'engageMessage', targetIds: [_message._id], tagIds: [_tag._id], }; - Tags.tagsTag = jest.fn(); await tagsMutations.tagsTag({}, tagObj, { user: _user }); expect(Tags.tagsTag.mock.calls.length).toBe(1); diff --git a/src/data/resolvers/mutations/engages.js b/src/data/resolvers/mutations/engages.js index 77db0bb2e..1cf375e12 100644 --- a/src/data/resolvers/mutations/engages.js +++ b/src/data/resolvers/mutations/engages.js @@ -23,7 +23,7 @@ export default { }, /** - * Update message + * Edit message * @param {String} doc.title * @param {String} doc.fromUserId * @param {String} doc.kind @@ -37,7 +37,7 @@ export default { * @param {[String]} doc.tagIds * @return {Promise} message object */ - engageMessageUpdate(root, { _id, ...doc }, { user }) { + engageMessageEdit(root, { _id, ...doc }, { user }) { if (!user) throw new Error('Login required'); return EngageMessages.updateEngageMessage(_id, doc); @@ -55,7 +55,7 @@ export default { }, /** - * Update message + * Engage message set live * @param {String} id * @return {Promise} message object */ @@ -66,7 +66,7 @@ export default { }, /** - * Update message + * Engage message set pause * @param {String} id * @return {Promise} message object */ @@ -77,7 +77,7 @@ export default { }, /** - * Update message + * Engage message set live manual * @param {String} id * @return {Promise} message object */ diff --git a/src/data/resolvers/mutations/tags.js b/src/data/resolvers/mutations/tags.js index e333e1677..39990861e 100644 --- a/src/data/resolvers/mutations/tags.js +++ b/src/data/resolvers/mutations/tags.js @@ -15,20 +15,20 @@ export default { }, /** - * Update tag + * Edit tag * @param {String} doc.name * @param {String} doc.type * @param {String} doc.colorCode * @return {Promise} tag object */ - tagsUpdate(root, { _id, ...doc }, { user }) { + tagsEdit(root, { _id, ...doc }, { user }) { if (!user) throw new Error('Login required'); return Tags.updateTag(_id, doc); }, /** - * Delete tag + * Remove tag * @param {[String]} ids * @return {Promise} */ diff --git a/src/data/schema/engage.js b/src/data/schema/engage.js index 76e2d7f41..4b543c645 100644 --- a/src/data/schema/engage.js +++ b/src/data/schema/engage.js @@ -33,7 +33,7 @@ export const queries = ` export const mutations = ` engageMessageAdd(title: String!, kind: String!, segmentId: String!, method: String!, fromUserId: String!): EngageMessage - engageMessageUpdate(_id: String!, title: String!, kind: String!, + engageMessageEdit(_id: String!, title: String!, kind: String!, segmentId: String!, method: String!, fromUserId: String!): EngageMessage engageMessageRemove(ids: [String!]!): EngageMessage engageMessageSetLive(_id: String!): EngageMessage diff --git a/src/data/schema/tag.js b/src/data/schema/tag.js index e5245ed13..23a451247 100644 --- a/src/data/schema/tag.js +++ b/src/data/schema/tag.js @@ -15,7 +15,7 @@ export const queries = ` export const mutations = ` tagsAdd(name: String!, type: String!, colorCode: String): Tag - tagsUpdate(_id: String!, name: String!, type: String!, colorCode: String): Tag + tagsEdit(_id: String!, name: String!, type: String!, colorCode: String): Tag tagsRemove(ids: [String!]!): Tag tagsTag(type: String!, targetIds: [String!]!, tagIds: [String!]!): Tag `; diff --git a/src/db/models/Engages.js b/src/db/models/Engages.js index 0c92b593a..07466025d 100644 --- a/src/db/models/Engages.js +++ b/src/db/models/Engages.js @@ -60,7 +60,7 @@ const EngageMessageSchema = mongoose.Schema({ class Message { /** * Create engage message - * @param {Object} doc object + * @param {Object} doc object * @return {Promise} Newly created message object */ static createEngageMessage(doc) { @@ -72,24 +72,45 @@ class Message { }); } + /** + * Update engage message + * @param {String} _id + * @param {Object} doc object + * @return {Promise} updated message object + */ static async updateEngageMessage(_id, doc) { await this.update({ _id }, { $set: doc }); return this.findOne({ _id }); } + /** + * Engage message set live + * @param {String} _id + * @return {Promise} updated message object + */ static async engageMessageSetLive(_id) { await this.update({ _id }, { $set: { isLive: true, isDraft: false } }); return this.findOne({ _id }); } + /** + * Engage message set pause + * @param {String} _id + * @return {Promise} updated message object + */ static async engageMessageSetPause(_id) { await this.update({ _id }, { $set: { isLive: false } }); return this.findOne({ _id }); } + /** + * Remove engage message + * @param {String} _id + * @return {Promise} deleted message object + */ static async removeEngageMessage(_id) { const messageObj = await this.findOne({ _id }); diff --git a/src/db/models/Tags.js b/src/db/models/Tags.js index d83293c4d..c60e28d44 100644 --- a/src/db/models/Tags.js +++ b/src/db/models/Tags.js @@ -4,7 +4,23 @@ import Random from 'meteor-random'; import { TAG_TYPES } from '../../data/constants'; import { Customers, Conversations, EngageMessages } from '.'; -const validateUniqueness = async (selector, data) => { +const TagSchema = mongoose.Schema({ + _id: { + type: String, + unique: true, + default: () => Random.id(), + }, + name: String, + type: { + type: String, + enum: TAG_TYPES.ALL_LIST, + }, + colorCode: String, + createdAt: Date, + objectCount: Number, +}); + +export const validateUniqueness = async (selector, data) => { const { name, type } = data; const filter = { name, type }; @@ -34,7 +50,7 @@ const validateUniqueness = async (selector, data) => { return true; }; -const tagObject = async ({ tagIds, objectIds, collection, tagType }) => { +export const tagObject = async ({ tagIds, objectIds, collection, tagType }) => { if ((await Tags.find({ _id: { $in: tagIds }, type: tagType }).count()) !== tagIds.length) { throw new Error('Tag not found.'); } @@ -56,33 +72,13 @@ const tagObject = async ({ tagIds, objectIds, collection, tagType }) => { await Tags.update({ _id: { $in: tagIds } }, { $inc: { objectCount: 1 } }, { multi: true }); }; -const TagSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - name: String, - type: { - type: String, - enum: TAG_TYPES.ALL_LIST, - }, - colorCode: String, - createdAt: Date, - objectCount: Number, -}); - class Tag { /** * Create a tag - * @param {Object} tagObj object + * @param {Object} doc * @return {Promise} Newly created tag object */ static async createTag(doc) { - if (!doc.name) throw new Error('Name is required field'); - - if (!doc.type) throw new Error('Type is required field'); - const isUnique = await validateUniqueness(null, doc); if (!isUnique) throw new Error('Tag duplicated'); @@ -93,6 +89,11 @@ class Tag { }); } + /** + * Update Tag + * @param {Object} doc + * @return {Promise} updated tag object + */ static async updateTag(_id, doc) { const isUnique = await validateUniqueness({ _id }, doc); @@ -103,6 +104,11 @@ class Tag { return this.findOne({ _id }); } + /** + * Remove Tag + * @param {[String]} ids + * @return {Promise} removed tag object + */ static async removeTag(ids) { const tagCount = await Tags.find({ _id: { $in: ids } }).count(); @@ -119,6 +125,13 @@ class Tag { return await Tags.remove({ _id: { $in: ids } }); } + /** + * Attach a tag + * @param {String} type + * @param {[String]} targetIds + * @param {[String]} tagIds + * @return {Promise} removed tag object + */ static async tagsTag(type, targetIds, tagIds) { let collection = Conversations; From 85d2f737fda468cf72b7c91cb28af907a62d5da0 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Tue, 17 Oct 2017 11:50:23 +0800 Subject: [PATCH 084/318] Delete engage utils --- src/__tests__/engageMessageDb.test.js | 9 ++ src/__tests__/engageMessageMutations.test.js | 11 +- src/__tests__/tagDb.test.js | 39 +++++- src/__tests__/tagMutations.test.js | 40 +----- src/data/resolvers/mutations/engageUtils.js | 121 ------------------- src/db/models/Tags.js | 8 +- 6 files changed, 53 insertions(+), 175 deletions(-) delete mode 100644 src/data/resolvers/mutations/engageUtils.js diff --git a/src/__tests__/engageMessageDb.test.js b/src/__tests__/engageMessageDb.test.js index 2d946b3b2..a869dfbdb 100644 --- a/src/__tests__/engageMessageDb.test.js +++ b/src/__tests__/engageMessageDb.test.js @@ -76,4 +76,13 @@ describe('engage messages model', () => { expect(message.isLive).toEqual(false); }); + + test('Engage message remove not found', async () => { + expect.assertions(1); + try { + await EngageMessages.removeEngageMessage(_segment._id); + } catch (e) { + expect(e.message).toEqual(`Engage message not found with id ${_segment._id}`); + } + }); }); diff --git a/src/__tests__/engageMessageMutations.test.js b/src/__tests__/engageMessageMutations.test.js index 67e68d0c2..c4851b1fa 100644 --- a/src/__tests__/engageMessageMutations.test.js +++ b/src/__tests__/engageMessageMutations.test.js @@ -9,7 +9,7 @@ import mutations from '../data/resolvers/mutations'; beforeAll(() => connect()); afterAll(() => disconnect()); -describe('mutations', () => { +describe('engage message mutations', () => { let _user; let _segment; let _message; @@ -65,15 +65,6 @@ describe('mutations', () => { check(mutations.engageMessageSetLiveManual); }); - test('Engage message remove not found', async () => { - expect.assertions(1); - try { - await mutations.engageMessageRemove({}, `${_message._id}-`, { user: _user }); - } catch (e) { - expect(e.message).toEqual(`Engage message not found with id ${_message._id}-`); - } - }); - test('messages create', async () => { EngageMessages.createEngageMessage = jest.fn(); await mutations.engageMessageAdd(null, _doc, { user: _user }); diff --git a/src/__tests__/tagDb.test.js b/src/__tests__/tagDb.test.js index 77d26cc1e..065ceab48 100644 --- a/src/__tests__/tagDb.test.js +++ b/src/__tests__/tagDb.test.js @@ -10,7 +10,7 @@ beforeAll(() => connect()); afterAll(() => disconnect()); -describe('Tags model', () => { +describe('Test tags model', () => { let _tag; let _tag2; let _message; @@ -59,6 +59,24 @@ describe('Tags model', () => { Tags.tagsTag('customer', [], []); }); + test('Create tag check duplicated', async () => { + expect.assertions(1); + try { + await Tags.createTag(_tag2); + } catch (e) { + expect(e.message).toEqual('Tag duplicated'); + } + }); + + test('Update tag check duplicated', async () => { + expect.assertions(1); + try { + await Tags.updateTag(_tag2._id, { name: _tag.name, type: _tag.type }); + } catch (e) { + expect(e.message).toEqual('Tag duplicated'); + } + }); + test('Create tag', async () => { const tagObj = await Tags.createTag({ name: `${_tag.name}1`, @@ -103,4 +121,23 @@ describe('Tags model', () => { expect(tagObj.objectCount).toBe(1); expect(messageObj.tagIds[0]).toEqual(_tag.id); }); + + test('Remove tag not found', async () => { + expect.assertions(1); + try { + await Tags.removeTag([_message._id]); + } catch (e) { + expect(e.message).toEqual('Tag not found'); + } + }); + + test("Can't remove a tag", async () => { + expect.assertions(1); + try { + await EngageMessages.update({ _id: _message._id }, { $set: { tagIds: [_tag._id] } }); + await Tags.removeTag([_tag._id]); + } catch (e) { + expect(e.message).toEqual("Can't remove a tag with tagged object(s)"); + } + }); }); diff --git a/src/__tests__/tagMutations.test.js b/src/__tests__/tagMutations.test.js index fa0c4b5ef..57e317b1f 100644 --- a/src/__tests__/tagMutations.test.js +++ b/src/__tests__/tagMutations.test.js @@ -10,9 +10,8 @@ beforeAll(() => connect()); afterAll(() => disconnect()); -describe('Tags mutations', () => { +describe('Test tags mutations', () => { let _tag; - let _tag2; let _user; let _message; let doc; @@ -20,7 +19,6 @@ describe('Tags mutations', () => { beforeEach(async () => { // Creating test data _tag = await tagsFactory(); - _tag2 = await tagsFactory(); _user = await userFactory(); _message = await engageMessageFactory({}); @@ -62,23 +60,6 @@ describe('Tags mutations', () => { check(tagsMutations.tagsTag); }); - test('Check tag duplicated', async () => { - expect.assertions(2); - const check = async (mutation, doc) => { - try { - await mutation({}, doc, { user: _user }); - } catch (e) { - expect(e.message).toEqual('Tag duplicated'); - } - }; - - // add - await check(tagsMutations.tagsAdd, _tag2); - - // edit - await check(tagsMutations.tagsEdit, { _id: _tag2._id, name: _tag.name, type: _tag.type }); - }); - test('Create tag', async () => { Tags.createTag = jest.fn(); await tagsMutations.tagsAdd({}, doc, { user: _user }); @@ -95,25 +76,6 @@ describe('Tags mutations', () => { expect(Tags.updateTag.mock.calls.length).toBe(1); }); - test('Remove tag not found', async () => { - expect.assertions(1); - try { - await tagsMutations.tagsRemove({}, { ids: [_message._id] }, { user: _user }); - } catch (e) { - expect(e.message).toEqual('Tag not found'); - } - }); - - test("Can't remove a tag", async () => { - expect.assertions(1); - try { - await EngageMessages.update({ _id: _message._id }, { $set: { tagIds: [_tag._id] } }); - await tagsMutations.tagsRemove({}, { ids: [_tag._id] }, { user: _user }); - } catch (e) { - expect(e.message).toEqual("Can't remove a tag with tagged object(s)"); - } - }); - test('Remove tag', async () => { Tags.removeTag = jest.fn(); await tagsMutations.tagsRemove({}, { ids: [_tag.id] }, { user: _user }); diff --git a/src/data/resolvers/mutations/engageUtils.js b/src/data/resolvers/mutations/engageUtils.js deleted file mode 100644 index 3e1226616..000000000 --- a/src/data/resolvers/mutations/engageUtils.js +++ /dev/null @@ -1,121 +0,0 @@ -import { EngageMessages, Customers, Users, EmailTemplates, Integrations } from '../../../db/models'; -import { - EMAIL_CONTENT_PLACEHOLDER, - METHODS, - MESSAGE_KINDS, - INTEGRATION_KIND_CHOICES, -} from '../../constants'; -import Random from 'meteor-random'; - -export const replaceKeys = ({ content, customer, user }) => { - let result = content; - - // replace customer fields - result = result.replace(/{{\s?customer.name\s?}}/gi, customer.name); - result = result.replace(/{{\s?customer.email\s?}}/gi, customer.email); - - // replace user fields - result = result.replace(/{{\s?user.fullName\s?}}/gi, user.fullName); - result = result.replace(/{{\s?user.position\s?}}/gi, user.position); - result = result.replace(/{{\s?user.email\s?}}/gi, user.email); - - return result; -}; - -const findCustomers = ({ customerIds, segmentId }) => { - // find matched customers - let customerQuery = { _id: { $in: customerIds || [] } }; - - // TODO - - return Customers.find(customerQuery).fetch(); -}; - -const saveMatchedCustomerIds = (messageId, customers) => - EngageMessages.update( - { _id: messageId }, - { $set: { customerIds: customers.map(customer => customer._id) } }, - ); - -const sendViaEmail = message => { - const { fromUserId, segmentId, customerIds } = message; - const { templateId, subject, content } = message.email; - - const user = Users.findOne(fromUserId); - const userEmail = user.emails.pop(); - const template = EmailTemplates.findOne(templateId); - - // find matched customers - const customers = findCustomers({ customerIds, segmentId }); - - // save matched customer ids - saveMatchedCustomerIds(message._id, customers); - - customers.forEach(customer => { - // replace keys in subject - const replacedSubject = replaceKeys({ content: subject, customer, user }); - - // replace keys such as {{ customer.name }} in content - let replacedContent = replaceKeys({ content, customer, user }); - - // if sender choosed some template then use it - if (template) { - replacedContent = template.content.replace(EMAIL_CONTENT_PLACEHOLDER, replacedContent); - } - - const mailMessageId = Random.id(); - - // add new delivery report - EngageMessages.update( - { _id: message._id }, - { - $set: { - [`deliveryReports.${mailMessageId}`]: { - customerId: customer._id, - status: 'pending', - }, - }, - }, - ); - - // TODO send email - }); -}; - -const sendViaMessenger = message => { - const { fromUserId, segmentId, customerIds } = message; - const { brandId, content } = message.messenger; - - const user = Users.findOne(fromUserId); - - // find integration - const integration = Integrations.findOne({ - brandId, - kind: INTEGRATION_KIND_CHOICES.MESSENGER, - }); - - if (!integration) { - return 'Integration not found'; - } - - // find matched customers - const customers = findCustomers({ customerIds, segmentId }); - - // save matched customer ids - saveMatchedCustomerIds(message._id, customers); - - // TODO -}; - -export const send = message => { - const { method, kind } = message; - - if (method === METHODS.EMAIL) { - return sendViaEmail(message); - } - - // when kind is visitor auto, do not do anything - if (method === METHODS.MESSENGER && kind !== MESSAGE_KINDS.VISITOR_AUTO) { - return sendViaMessenger(message); - } -}; diff --git a/src/db/models/Tags.js b/src/db/models/Tags.js index c60e28d44..cc0c93edf 100644 --- a/src/db/models/Tags.js +++ b/src/db/models/Tags.js @@ -60,7 +60,7 @@ export const tagObject = async ({ tagIds, objectIds, collection, tagType }) => { let removeIds = []; objects.forEach(obj => { - removeIds.push(obj.tagIds || []); + removeIds.push(obj.tagIds); }); removeIds = _.uniq(_.flatten(removeIds)); @@ -107,7 +107,7 @@ class Tag { /** * Remove Tag * @param {[String]} ids - * @return {Promise} removed tag object + * @return {Promise} */ static async removeTag(ids) { const tagCount = await Tags.find({ _id: { $in: ids } }).count(); @@ -122,7 +122,7 @@ class Tag { if (count > 0) throw new Error("Can't remove a tag with tagged object(s)"); - return await Tags.remove({ _id: { $in: ids } }); + return Tags.remove({ _id: { $in: ids } }); } /** @@ -130,7 +130,7 @@ class Tag { * @param {String} type * @param {[String]} targetIds * @param {[String]} tagIds - * @return {Promise} removed tag object + * @return {Promise} attach tag object */ static async tagsTag(type, targetIds, tagIds) { let collection = Conversations; From 27f41efe5a311a502a2e047e7ee9b12e924cea2d Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Tue, 17 Oct 2017 11:55:28 +0800 Subject: [PATCH 085/318] Update comments meaning in conversation mutation --- src/__tests__/brandMutations.test.js | 32 +++++++----------- src/__tests__/conversationMutations.test.js | 1 - src/__tests__/emailTemplateMutations.test.js | 33 ++++++++----------- .../responseTemplateMutations.test.js | 33 ++++++++----------- src/data/resolvers/mutations/conversations.js | 20 +++++------ 5 files changed, 50 insertions(+), 69 deletions(-) diff --git a/src/__tests__/brandMutations.test.js b/src/__tests__/brandMutations.test.js index e7cf3aecb..dd3438676 100644 --- a/src/__tests__/brandMutations.test.js +++ b/src/__tests__/brandMutations.test.js @@ -27,35 +27,27 @@ describe('Brands mutations', () => { }); test('Check login required mutations', async () => { + const checkLogin = async (fn, args) => { + try { + await fn({}, args, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }; + expect.assertions(4); // brands add - try { - await brandMutations.brandsAdd({}, { code: _brand.code, name: _brand.name }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(brandMutations.brandsAdd, { code: _brand.code, name: _brand.name }); // brands edit - try { - await brandMutations.brandsEdit({}, { _id: _brand.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(brandMutations.brandsEdit, { _id: _brand.id }); // brands remove - try { - await brandMutations.brandsRemove({}, { _id: _brand.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(brandMutations.brandsRemove, { _id: _brand.id }); // brands update email config - try { - await brandMutations.brandsConfigEmail({}, { _id: _brand.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(brandMutations.brandsConfigEmail, { _id: _brand.id }); }); test('Create brand', async () => { diff --git a/src/__tests__/conversationMutations.test.js b/src/__tests__/conversationMutations.test.js index f4977d2d0..6d219438a 100644 --- a/src/__tests__/conversationMutations.test.js +++ b/src/__tests__/conversationMutations.test.js @@ -51,7 +51,6 @@ describe('Conversation message mutations', () => { test('Conversation login required functions', async () => { const checkLogin = async (fn, args) => { - expect.assertions(8); try { await fn({}, args, {}); } catch (e) { diff --git a/src/__tests__/emailTemplateMutations.test.js b/src/__tests__/emailTemplateMutations.test.js index 9eb6ef6e2..d12b8805c 100644 --- a/src/__tests__/emailTemplateMutations.test.js +++ b/src/__tests__/emailTemplateMutations.test.js @@ -27,32 +27,27 @@ describe('Email template mutations', () => { }); test('Email templates login required functions', async () => { + const checkLogin = async (fn, args) => { + try { + await fn({}, args, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }; + expect.assertions(3); // add email template - try { - emailTemplateMutations.emailTemplateAdd( - {}, - { name: _emailTemplate.name, content: _emailTemplate.content }, - {}, - ); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(emailTemplateMutations.emailTemplateAdd, { + name: _emailTemplate.name, + content: _emailTemplate.content, + }); // update email template - try { - await emailTemplateMutations.emailTemplateEdit({}, { _id: _emailTemplate.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(emailTemplateMutations.emailTemplateEdit, { _id: _emailTemplate.id }); // remove email template - try { - await emailTemplateMutations.emailTemplateRemove({}, { _id: _emailTemplate.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(emailTemplateMutations.emailTemplateRemove, { _id: _emailTemplate.id }); }); test('Create email template', async () => { diff --git a/src/__tests__/responseTemplateMutations.test.js b/src/__tests__/responseTemplateMutations.test.js index 92a889b0f..357d75970 100644 --- a/src/__tests__/responseTemplateMutations.test.js +++ b/src/__tests__/responseTemplateMutations.test.js @@ -27,32 +27,27 @@ describe('Response template mutations', () => { }); test('Response templates login required functions', async () => { + const checkLogin = async (fn, args) => { + try { + await fn({}, args, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }; + expect.assertions(3); // add response template - try { - responseTemplateMutations.responseTemplateAdd( - {}, - { name: _responseTemplate.name, content: _responseTemplate.content }, - {}, - ); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(responseTemplateMutations.responseTemplateAdd, { + name: _responseTemplate.name, + content: _responseTemplate.content, + }); // update response template - try { - await responseTemplateMutations.responseTemplateEdit({}, { _id: _responseTemplate.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(responseTemplateMutations.responseTemplateAdd, { _id: _responseTemplate.id }); // remove response template - try { - await responseTemplateMutations.responseTemplateRemove({}, { _id: _responseTemplate.id }, {}); - } catch (e) { - expect(e.message).toEqual('Login required'); - } + checkLogin(responseTemplateMutations.responseTemplateRemove, { _id: _responseTemplate.id }); }); test('Create response template', async () => { diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index d00d11350..7e9564a80 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -74,7 +74,7 @@ export default { /** * Create new message in conversation * @param {Object} doc contains conversation message inputs - * @return {Promise} messageId + * @return {Promise} newly created message object */ async conversationMessageAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); @@ -143,7 +143,7 @@ export default { * Assign employee to conversation * @param {list} conversationIds * @param {String} assignedUserId - * @return {Promise} String + * @return {Promise} object list of assigned conversation */ async conversationsAssign(root, { conversationIds, assignedUserId }, { user }) { if (!user) throw new Error('Login required'); @@ -176,7 +176,7 @@ export default { /** * Unassign employee from conversation * @param {list} _ids of conversation - * @return {Promise} String + * @return {Promise} unassigned conversation object list */ async conversationsUnassign(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); @@ -193,14 +193,14 @@ export default { * Change conversation status * @param {list} _ids of conversation * @param {String} status - * @return {Promise} String + * @return {Promise} object list of updated conversations */ async conversationsChangeStatus(root, { _ids, status }, { user }) { if (!user) throw new Error('Login required'); const { conversations } = await Conversations.checkExistanceConversations(_ids); - const changesConversations = await Conversations.changeStatusConversation(_ids, status); + const changedConversations = await Conversations.changeStatusConversation(_ids, status); // notify graphl subscription await conversationsChanged(_ids, 'statusChanged'); @@ -243,13 +243,13 @@ export default { }); } - return changesConversations; + return changedConversations; }, /** * Star conversation * @param {list} _ids of conversation - * @return {Promise} String + * @return {Promise} user object of starred conversations */ async conversationsStar(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); @@ -260,7 +260,7 @@ export default { /** * Unstar conversation * @param {list} _ids of conversation - * @return {Promise} String + * @return {Promise} user object from unstarred conversations */ async conversationsUnstar(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); @@ -271,7 +271,7 @@ export default { /** * Add or remove participed users in conversation * @param {list} _ids of conversation - * @return {Promise} String + * @return {Promise} Conversation object */ async conversationsToggleParticipate(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); @@ -287,7 +287,7 @@ export default { /** * Conversation mark as read * @param {String} _id of conversation - * @return {Promise} String + * @return {Promise} Conversation object with mark as read */ async conversationMarkAsRead(root, { _id }, { user }) { if (!user) throw new Error('Login required'); From 22ea33c11bfd221fd22a86eb9323761bae85ab45 Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 17 Oct 2017 11:57:54 +0800 Subject: [PATCH 086/318] Add some comments --- src/__tests__/engageMessageDb.test.js | 2 +- src/data/resolvers/mutations/engages.js | 8 ++++---- src/data/resolvers/mutations/tags.js | 5 ++--- src/db/models/Engages.js | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/__tests__/engageMessageDb.test.js b/src/__tests__/engageMessageDb.test.js index a869dfbdb..347b101bc 100644 --- a/src/__tests__/engageMessageDb.test.js +++ b/src/__tests__/engageMessageDb.test.js @@ -8,7 +8,7 @@ import { EngageMessages, Users, Segments } from '../db/models'; beforeAll(() => connect()); afterAll(() => disconnect()); -describe('engage messages model', () => { +describe('engage messages model tests', () => { let _user; let _segment; let _message; diff --git a/src/data/resolvers/mutations/engages.js b/src/data/resolvers/mutations/engages.js index 1cf375e12..b5275825a 100644 --- a/src/data/resolvers/mutations/engages.js +++ b/src/data/resolvers/mutations/engages.js @@ -46,7 +46,7 @@ export default { /** * Remove message * @param {String} id - * @return {Promise} null + * @return {Promise} */ engageMessageRemove(root, _id, { user }) { if (!user) throw new Error('Login required'); @@ -57,7 +57,7 @@ export default { /** * Engage message set live * @param {String} id - * @return {Promise} message object + * @return {Promise} updated message object */ engageMessageSetLive(root, _id, { user }) { if (!user) throw new Error('Login required'); @@ -68,7 +68,7 @@ export default { /** * Engage message set pause * @param {String} id - * @return {Promise} message object + * @return {Promise} updated message object */ engageMessageSetPause(root, _id, { user }) { if (!user) throw new Error('Login required'); @@ -79,7 +79,7 @@ export default { /** * Engage message set live manual * @param {String} id - * @return {Promise} message object + * @return {Promise} updated message object */ engageMessageSetLiveManual(root, _id, { user }) { if (!user) throw new Error('Login required'); diff --git a/src/data/resolvers/mutations/tags.js b/src/data/resolvers/mutations/tags.js index 39990861e..27c688be5 100644 --- a/src/data/resolvers/mutations/tags.js +++ b/src/data/resolvers/mutations/tags.js @@ -6,7 +6,7 @@ export default { * @param {String} doc.name * @param {String} doc.type * @param {String} doc.colorCode - * @return {Promise} tag object + * @return {Promise} newly created tag object */ tagsAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); @@ -19,7 +19,7 @@ export default { * @param {String} doc.name * @param {String} doc.type * @param {String} doc.colorCode - * @return {Promise} tag object + * @return {Promise} updated tag object */ tagsEdit(root, { _id, ...doc }, { user }) { if (!user) throw new Error('Login required'); @@ -43,7 +43,6 @@ export default { * @param {String} type * @param {[String]} targetIds * @param {[String]} tagIds - * @return {Promise} */ tagsTag(root, { type, targetIds, tagIds }, { user }) { if (!user) throw new Error('Login required'); diff --git a/src/db/models/Engages.js b/src/db/models/Engages.js index 07466025d..a05e66abe 100644 --- a/src/db/models/Engages.js +++ b/src/db/models/Engages.js @@ -109,7 +109,7 @@ class Message { /** * Remove engage message * @param {String} _id - * @return {Promise} deleted message object + * @return {Promise} */ static async removeEngageMessage(_id) { const messageObj = await this.findOne({ _id }); From ce01d46245764edb0fef4a09f421134604d09fe0 Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 17 Oct 2017 12:12:54 +0800 Subject: [PATCH 087/318] Refactor tag validateUniqueness --- src/__tests__/engageMessageMutations.test.js | 2 +- src/__tests__/tagDb.test.js | 11 +++---- src/db/models/Tags.js | 32 +++++++++++++++----- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/__tests__/engageMessageMutations.test.js b/src/__tests__/engageMessageMutations.test.js index c4851b1fa..b706ec64f 100644 --- a/src/__tests__/engageMessageMutations.test.js +++ b/src/__tests__/engageMessageMutations.test.js @@ -9,7 +9,7 @@ import mutations from '../data/resolvers/mutations'; beforeAll(() => connect()); afterAll(() => disconnect()); -describe('engage message mutations', () => { +describe('engage message mutation tests', () => { let _user; let _segment; let _message; diff --git a/src/__tests__/tagDb.test.js b/src/__tests__/tagDb.test.js index 065ceab48..995cf3154 100644 --- a/src/__tests__/tagDb.test.js +++ b/src/__tests__/tagDb.test.js @@ -29,12 +29,11 @@ describe('Test tags model', () => { }); test('Validate unique tag', async () => { - const empty = await validateUniqueness({}, {}); - const selectTag = await validateUniqueness( - { type: _tag2.type }, - { name: 'new tag', type: _tag2.type }, - ); - const existing = await validateUniqueness({}, _tag); + const empty = await validateUniqueness({}); + + const selectTag = await validateUniqueness({ type: _tag2.type }, 'new tag', _tag2.type); + + const existing = await validateUniqueness({}, _tag.name, _tag.type); expect(empty).toEqual(true); expect(selectTag).toEqual(false); diff --git a/src/db/models/Tags.js b/src/db/models/Tags.js index cc0c93edf..6f8326c83 100644 --- a/src/db/models/Tags.js +++ b/src/db/models/Tags.js @@ -20,10 +20,14 @@ const TagSchema = mongoose.Schema({ objectCount: Number, }); -export const validateUniqueness = async (selector, data) => { - const { name, type } = data; - const filter = { name, type }; - +/* + * Validates tag uniquness + * @param {Object} selector - mongoose selector object + * @param {String} data.name - tag name + * @param {String} data.type - tag type + */ +export const validateUniqueness = async (selector, name, type) => { + // required name and type if (!name || !type) { return true; } @@ -37,6 +41,8 @@ export const validateUniqueness = async (selector, data) => { const obj = selector && (await Tags.findOne(selector)); + const filter = { name, type }; + if (obj) { filter._id = { $ne: obj._id }; } @@ -50,8 +56,20 @@ export const validateUniqueness = async (selector, data) => { return true; }; +/* + * Common helper for taggable objects like conversation, engage, customer etc ... + * @param {[String]} tagIds - Tag ids + * @param {[String]} objectIds - conversation, engage or customer's ids + * @param {MongooseCollection} collection - conversation, engage or customer's collections + * @param {String} tagType - one of conversation, engageMessage, customer + */ export const tagObject = async ({ tagIds, objectIds, collection, tagType }) => { - if ((await Tags.find({ _id: { $in: tagIds }, type: tagType }).count()) !== tagIds.length) { + const prevTagsCount = await Tags.find({ + _id: { $in: tagIds }, + type: tagType, + }).count(); + + if (prevTagsCount !== tagIds.length) { throw new Error('Tag not found.'); } @@ -79,7 +97,7 @@ class Tag { * @return {Promise} Newly created tag object */ static async createTag(doc) { - const isUnique = await validateUniqueness(null, doc); + const isUnique = await validateUniqueness(null, doc.name, doc.type); if (!isUnique) throw new Error('Tag duplicated'); @@ -95,7 +113,7 @@ class Tag { * @return {Promise} updated tag object */ static async updateTag(_id, doc) { - const isUnique = await validateUniqueness({ _id }, doc); + const isUnique = await validateUniqueness({ _id }, doc.name, doc.type); if (!isUnique) throw new Error('Tag duplicated'); From a88ae4be6b5804e294e858c23f13b2281809e004 Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 17 Oct 2017 12:37:43 +0800 Subject: [PATCH 088/318] Add some comments --- src/__tests__/conversationDb.test.js | 2 +- src/data/resolvers/mutations/brands.js | 4 ++-- src/data/resolvers/mutations/conversations.js | 19 ++++++++++--------- .../resolvers/mutations/emailTemplates.js | 4 ++-- .../resolvers/mutations/responseTemplates.js | 4 ++-- src/db/models/Conversations.js | 10 +++++----- 6 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/__tests__/conversationDb.test.js b/src/__tests__/conversationDb.test.js index 05462146e..93ccf2b34 100644 --- a/src/__tests__/conversationDb.test.js +++ b/src/__tests__/conversationDb.test.js @@ -123,7 +123,7 @@ describe('Conversation db', () => { const conversationObj = await Conversations.findOne({ _id: _conversation._id }); - expect(conversationObj.assignedUserId).toBe(undefined); + expect(conversationObj.assignedUserId).toBeUndefined(); }); test('Change conversation status', async () => { diff --git a/src/data/resolvers/mutations/brands.js b/src/data/resolvers/mutations/brands.js index 476cf201e..4a14ec763 100644 --- a/src/data/resolvers/mutations/brands.js +++ b/src/data/resolvers/mutations/brands.js @@ -27,7 +27,7 @@ export default { /** * Delete brand * @param {String} _id - brand id - * @return {String} + * @return {Promise} */ brandsRemove(root, { _id }, { user }) { if (!user) throw new Error('Login required'); @@ -39,7 +39,7 @@ export default { * Update brands email config * @param {String} _id - brand id * @param {Object} emailConfig - brand email config fields - * @return {Promise} brand object + * @return {Promise} updated brand object */ async brandsConfigEmail(root, { _id, emailConfig }, { user }) { if (!user) throw new Error('Login required'); diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 7e9564a80..57245b3a2 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -8,7 +8,7 @@ import { _ } from 'underscore'; * conversation notrification receiver ids * @param {object} conversation object * @param {String} currentUserId String - * @return {list} userIds + * @return {[String]} userIds */ export const conversationNotifReceivers = (conversation, currentUserId) => { let userIds = []; @@ -31,7 +31,7 @@ export const conversationNotifReceivers = (conversation, currentUserId) => { /** * Publish updated conversation - * @param {list} _ids of conversations + * @param {[String]} _ids of conversations * @param {String} type of status */ const conversationsChanged = async (_ids, type) => { @@ -134,16 +134,17 @@ export default { // TODO: facebookReply(conversation, strip(content), messageId); } + // notify subscription await conversationMessageCreated(message, doc.conversationId); - return message._id; + return message; }, /** * Assign employee to conversation - * @param {list} conversationIds + * @param {[String]} conversationIds * @param {String} assignedUserId - * @return {Promise} object list of assigned conversation + * @return {Promise} object list of assigned conversations */ async conversationsAssign(root, { conversationIds, assignedUserId }, { user }) { if (!user) throw new Error('Login required'); @@ -175,8 +176,8 @@ export default { /** * Unassign employee from conversation - * @param {list} _ids of conversation - * @return {Promise} unassigned conversation object list + * @param {[String]} _ids of conversation + * @return {Promise} unassigned conversations */ async conversationsUnassign(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); @@ -191,7 +192,7 @@ export default { /** * Change conversation status - * @param {list} _ids of conversation + * @param {[String]} _ids of conversation * @param {String} status * @return {Promise} object list of updated conversations */ @@ -271,7 +272,7 @@ export default { /** * Add or remove participed users in conversation * @param {list} _ids of conversation - * @return {Promise} Conversation object + * @return {Promise} updated conversations */ async conversationsToggleParticipate(root, { _ids }, { user }) { if (!user) throw new Error('Login required'); diff --git a/src/data/resolvers/mutations/emailTemplates.js b/src/data/resolvers/mutations/emailTemplates.js index 270b7e078..57bb671e0 100644 --- a/src/data/resolvers/mutations/emailTemplates.js +++ b/src/data/resolvers/mutations/emailTemplates.js @@ -4,7 +4,7 @@ export default { /** * Create new email template * @param {Object} doc - email templates fields - * @return {Promise} email template object + * @return {Promise} newly created email template object */ emailTemplateAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); @@ -16,7 +16,7 @@ export default { * Update email template * @param {String} _id - email templates id * @param {Object} fields - email templates fields - * @return {Promise} email template object + * @return {Promise} updated email template object */ emailTemplateEdit(root, { _id, ...fields }, { user }) { if (!user) throw new Error('Login required'); diff --git a/src/data/resolvers/mutations/responseTemplates.js b/src/data/resolvers/mutations/responseTemplates.js index 107af03d3..702764d06 100644 --- a/src/data/resolvers/mutations/responseTemplates.js +++ b/src/data/resolvers/mutations/responseTemplates.js @@ -4,7 +4,7 @@ export default { /** * Create new response template * @param {Object} fields - response template fields - * @return {Promise} response template object + * @return {Promise} newly created response template object */ responseTemplateAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); @@ -16,7 +16,7 @@ export default { * Update response template * @param {String} _id - response template id * @param {Object} fields - response template fields - * @return {Promise} response template object + * @return {Promise} updated response template object */ responseTemplateEdit(root, { _id, ...fields }, { user }) { if (!user) throw new Error('Login required'); diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index 12be44de2..15a3ac96e 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -142,7 +142,7 @@ class Conversation { * Assign user to conversation * @param {list} conversationIds * @param {String} assignedUserId - * @return {Promise} Updated conversation id + * @return {Promise} Updated conversation objects */ static async assignUserConversation(conversationIds, assignedUserId) { await this.checkExistanceConversations(conversationIds); @@ -163,7 +163,7 @@ class Conversation { /** * Unassign user from conversation * @param {list} conversationIds - * @return {Promise} Updated conversation id + * @return {Promise} Updated conversation objects */ static async unassignUserConversation(conversationIds) { await this.checkExistanceConversations(conversationIds); @@ -191,7 +191,7 @@ class Conversation { * Star conversation * @param {list} _ids of conversations * @param {String} userId - * @return {Promise} user object + * @return {Promise} updated user object */ static async starConversation(_ids, userId) { await this.checkExistanceConversations(_ids); @@ -212,7 +212,7 @@ class Conversation { * Unstar conversation * @param {list} _ids of conversations * @param {string} userId - * @return {Promise} user object + * @return {Promise} updated user object */ static async unstarConversation(_ids, userId) { // check conversations existance @@ -266,7 +266,7 @@ class Conversation { * Mark as read conversation * @param {String} _id of conversation * @param {String} userId - * @return {Promise} Updated conversation id + * @return {Promise} Updated conversation object */ static async markAsReadConversation(_id, userId) { const conversation = await this.findOne({ _id }); From 18b0e2759924685641b2100d0a6f3c20284980f4 Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 17 Oct 2017 12:39:55 +0800 Subject: [PATCH 089/318] Add responseTemplate edit mutation login required test --- src/__tests__/responseTemplateMutations.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/responseTemplateMutations.test.js b/src/__tests__/responseTemplateMutations.test.js index 357d75970..44fca6f0f 100644 --- a/src/__tests__/responseTemplateMutations.test.js +++ b/src/__tests__/responseTemplateMutations.test.js @@ -44,7 +44,7 @@ describe('Response template mutations', () => { }); // update response template - checkLogin(responseTemplateMutations.responseTemplateAdd, { _id: _responseTemplate.id }); + checkLogin(responseTemplateMutations.responseTemplateEdit, { _id: _responseTemplate.id }); // remove response template checkLogin(responseTemplateMutations.responseTemplateRemove, { _id: _responseTemplate.id }); From 1a21e104b36de206c563399a75d546d7720b94af Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 17 Oct 2017 20:19:28 +0800 Subject: [PATCH 090/318] Filter by contentType in customer, company segments --- src/constants.js | 18 ------------------ src/data/constants.js | 19 +++++++++++++++++++ src/data/resolvers/queries/companies.js | 5 ++++- src/data/resolvers/queries/customers.js | 6 ++++-- src/data/resolvers/queries/fields.js | 2 +- src/db/models/Fields.js | 2 +- src/db/models/InternalNotes.js | 2 +- src/db/models/Segments.js | 2 +- 8 files changed, 31 insertions(+), 25 deletions(-) delete mode 100644 src/constants.js diff --git a/src/constants.js b/src/constants.js deleted file mode 100644 index 696787e82..000000000 --- a/src/constants.js +++ /dev/null @@ -1,18 +0,0 @@ -export const FIELD_CONTENT_TYPES = { - FORM: 'form', - CUSTOMER: 'customer', - COMPANY: 'company', - ALL_LIST: ['form', 'customer', 'company'], -}; - -export const INTERNAL_NOTE_CONTENT_TYPES = { - CUSTOMER: 'customer', - COMPANY: 'company', - ALL_LIST: ['customer', 'company'], -}; - -export const SEGMENT_CONTENT_TYPES = { - CUSTOMER: 'customer', - COMPANY: 'company', - ALL_LIST: ['customer', 'company'], -}; diff --git a/src/data/constants.js b/src/data/constants.js index 2efdbcda4..66f785af9 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -131,3 +131,22 @@ export const MESSENGER_DATA_AVAILABILITY = { AUTO: 'auto', ALL: ['manual', 'auto'], }; + +export const FIELD_CONTENT_TYPES = { + FORM: 'form', + CUSTOMER: 'customer', + COMPANY: 'company', + ALL_LIST: ['form', 'customer', 'company'], +}; + +export const INTERNAL_NOTE_CONTENT_TYPES = { + CUSTOMER: 'customer', + COMPANY: 'company', + ALL_LIST: ['customer', 'company'], +}; + +export const SEGMENT_CONTENT_TYPES = { + CUSTOMER: 'customer', + COMPANY: 'company', + ALL_LIST: ['customer', 'company'], +}; diff --git a/src/data/resolvers/queries/companies.js b/src/data/resolvers/queries/companies.js index 9ccb3d181..2bf5c9947 100644 --- a/src/data/resolvers/queries/companies.js +++ b/src/data/resolvers/queries/companies.js @@ -1,5 +1,6 @@ import { Companies, Segments } from '../../../db/models'; import QueryBuilder from './segmentQueryBuilder.js'; +import { SEGMENT_CONTENT_TYPES } from '../../constants'; const listQuery = async params => { const selector = {}; @@ -50,7 +51,9 @@ export default { counts.all = await count(selector); // Count companies by segments - const segments = await Segments.find(); + const segments = await Segments.find({ + contentType: SEGMENT_CONTENT_TYPES.COMPANY, + }); for (let s of segments) { counts.bySegment[s._id] = await count(QueryBuilder.segments(s)); diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index 6f872dc85..e28efc174 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -1,6 +1,6 @@ import _ from 'underscore'; import { Brands, Tags, Integrations, Customers, Segments } from '../../../db/models'; -import { TAG_TYPES, INTEGRATION_KIND_CHOICES } from '../../constants'; +import { TAG_TYPES, INTEGRATION_KIND_CHOICES, SEGMENT_CONTENT_TYPES } from '../../constants'; import QueryBuilder from './segmentQueryBuilder.js'; const listQuery = async params => { @@ -80,7 +80,9 @@ export default { counts.all = await count(selector); // Count customers by segments - const segments = await Segments.find(); + const segments = await Segments.find({ + contentType: SEGMENT_CONTENT_TYPES.CUSTOMER, + }); for (let s of segments) { counts.bySegment[s._id] = await count(QueryBuilder.segments(s)); diff --git a/src/data/resolvers/queries/fields.js b/src/data/resolvers/queries/fields.js index 8963d7bb4..0d2eab4f7 100644 --- a/src/data/resolvers/queries/fields.js +++ b/src/data/resolvers/queries/fields.js @@ -1,5 +1,5 @@ import { Companies, Customers, Fields } from '../../../db/models'; -import { FIELD_CONTENT_TYPES } from '../../../constants'; +import { FIELD_CONTENT_TYPES } from '../../../data/constants'; export default { /** diff --git a/src/db/models/Fields.js b/src/db/models/Fields.js index 9cc7a3d9b..cf71b3dd5 100644 --- a/src/db/models/Fields.js +++ b/src/db/models/Fields.js @@ -5,7 +5,7 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; import validator from 'validator'; -import { FIELD_CONTENT_TYPES } from '../../constants'; +import { FIELD_CONTENT_TYPES } from '../../data/constants'; import { Forms } from './'; const FieldSchema = mongoose.Schema({ diff --git a/src/db/models/InternalNotes.js b/src/db/models/InternalNotes.js index 5807a956a..13f14e2cf 100644 --- a/src/db/models/InternalNotes.js +++ b/src/db/models/InternalNotes.js @@ -1,6 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -import { INTERNAL_NOTE_CONTENT_TYPES } from '../../constants'; +import { INTERNAL_NOTE_CONTENT_TYPES } from '../../data/constants'; /* * internal note schema diff --git a/src/db/models/Segments.js b/src/db/models/Segments.js index 73aed2219..ba00ba1c6 100644 --- a/src/db/models/Segments.js +++ b/src/db/models/Segments.js @@ -1,6 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -import { SEGMENT_CONTENT_TYPES } from '../../constants'; +import { SEGMENT_CONTENT_TYPES } from '../../data/constants'; const ConditionSchema = mongoose.Schema( { From 3d58abd269de9b481ca398a44c8e8b0e6d483632 Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 17 Oct 2017 21:07:45 +0800 Subject: [PATCH 091/318] Refactor channelMutation tests --- src/__tests__/channelMutations.test.js | 28 ++++++++++---------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/__tests__/channelMutations.test.js b/src/__tests__/channelMutations.test.js index 8b27d5378..d6b30a604 100644 --- a/src/__tests__/channelMutations.test.js +++ b/src/__tests__/channelMutations.test.js @@ -26,23 +26,17 @@ describe('mutations', () => { test(`test if Error('Login required') error is working as intended`, async () => { expect.assertions(3); - try { - await channelMutations.channelsCreate(null, {}, {}); - } catch (e) { - expect(e.message).toBe('Login required'); - } - - try { - await channelMutations.channelsEdit(null, {}, {}); - } catch (e) { - expect(e.message).toBe('Login required'); - } - - try { - await channelMutations.channelsRemove(null, {}, {}); - } catch (e) { - expect(e.message).toBe('Login required'); - } + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(channelMutations.channelsCreate); + expectError(channelMutations.channelsEdit); + expectError(channelMutations.channelsRemove); }); test('test mutations.channelsCreate', async () => { From 6b3e8583fb3225d14a570f9d6ce545a0b6e80a4f Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Tue, 17 Oct 2017 21:12:22 +0800 Subject: [PATCH 092/318] #16 Refactor tests --- src/__tests__/utils.test.js | 11 +++++++++++ src/data/utils.js | 7 ++++--- {private => src/private}/emailTemplates/base.html | 0 .../private}/emailTemplates/conversationCron.html | 0 .../private}/emailTemplates/conversationDetail.html | 0 .../private}/emailTemplates/invitation.html | 0 .../private}/emailTemplates/notification.html | 0 7 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/utils.test.js rename {private => src/private}/emailTemplates/base.html (100%) rename {private => src/private}/emailTemplates/conversationCron.html (100%) rename {private => src/private}/emailTemplates/conversationDetail.html (100%) rename {private => src/private}/emailTemplates/invitation.html (100%) rename {private => src/private}/emailTemplates/notification.html (100%) diff --git a/src/__tests__/utils.test.js b/src/__tests__/utils.test.js new file mode 100644 index 000000000..84124c726 --- /dev/null +++ b/src/__tests__/utils.test.js @@ -0,0 +1,11 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import utils from '../data/utils'; + +describe('test utils', () => { + test('test readFile', async () => { + const data = await utils.readFile('notification'); + expect(data).toBeDefined(); + }); +}); diff --git a/src/data/utils.js b/src/data/utils.js index 0e74f632f..12bf9fa77 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -8,8 +8,8 @@ import { Notifications, Users } from '../db/models'; * @param {string} filename - relative file path * @return {Promise} returns promise resolving file contents */ -const readFilePro = filename => { - const filePath = `${__dirname}/../../private/emailTemplates/${filename}.html`; +export const readFile = filename => { + const filePath = `${__dirname}/../private/emailTemplates/${filename}.html`; return new Promise((resolve, reject) => { fs.readFile(filePath, 'utf8', (err, data) => { @@ -28,7 +28,7 @@ const readFilePro = filename => { * @return email with template as text */ const applyTemplate = async (data, templateName) => { - let template = await readFilePro(templateName); + let template = await readFile(templateName); template = Handlebars.compile(template.toString()); @@ -134,4 +134,5 @@ export const sendNotification = async ({ createdUser, receivers, ...doc }) => { export default { sendEmail, sendNotification, + readFile, }; diff --git a/private/emailTemplates/base.html b/src/private/emailTemplates/base.html similarity index 100% rename from private/emailTemplates/base.html rename to src/private/emailTemplates/base.html diff --git a/private/emailTemplates/conversationCron.html b/src/private/emailTemplates/conversationCron.html similarity index 100% rename from private/emailTemplates/conversationCron.html rename to src/private/emailTemplates/conversationCron.html diff --git a/private/emailTemplates/conversationDetail.html b/src/private/emailTemplates/conversationDetail.html similarity index 100% rename from private/emailTemplates/conversationDetail.html rename to src/private/emailTemplates/conversationDetail.html diff --git a/private/emailTemplates/invitation.html b/src/private/emailTemplates/invitation.html similarity index 100% rename from private/emailTemplates/invitation.html rename to src/private/emailTemplates/invitation.html diff --git a/private/emailTemplates/notification.html b/src/private/emailTemplates/notification.html similarity index 100% rename from private/emailTemplates/notification.html rename to src/private/emailTemplates/notification.html From 7ae9fda44b1e1c9fdeab4f05045701b31f8c9951 Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 17 Oct 2017 21:37:39 +0800 Subject: [PATCH 093/318] Exclude some files from jest coverage --- package.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 331fda247..02a5e22c5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "lint": "eslint src", "format": "prettier --write --print-width 100 --single-quote --trailing-comma all 'src/**/*.js'", "precommit": "lint-staged", - "test": "jest --runInBand" + "test": "jest --runInBand --coverage" }, "lint-staged": { "*.js": [ @@ -28,6 +28,18 @@ "git add" ] }, + "jest": { + "collectCoverageFrom": [ + "src/**/*.{js}", + "!src/index.js", + "!src/db/factories.js", + "!src/db/connection.js", + "!src/data/schema/**", + "!src/data/resolvers/subscriptions/**", + "!src/data/index.js", + "!src/data/utils.js" + ] + }, "dependencies": { "body-parser": "^1.17.1", "cors": "^2.8.1", From 50961897339d4869018dbe04134323e119f08685 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Wed, 18 Oct 2017 16:37:01 +0800 Subject: [PATCH 094/318] Add engage utils --- src/__tests__/engageMessageDb.test.js | 70 ++++++++- src/__tests__/engageMessageMutations.test.js | 56 ++++++- src/data/resolvers/mutations/engageUtils.js | 156 +++++++++++++++++++ src/data/resolvers/mutations/engages.js | 20 ++- src/data/utils.js | 31 +++- src/db/factories.js | 11 ++ src/db/models/Engages.js | 62 +++++++- src/db/models/Users.js | 7 +- 8 files changed, 392 insertions(+), 21 deletions(-) create mode 100644 src/data/resolvers/mutations/engageUtils.js diff --git a/src/__tests__/engageMessageDb.test.js b/src/__tests__/engageMessageDb.test.js index 347b101bc..468121566 100644 --- a/src/__tests__/engageMessageDb.test.js +++ b/src/__tests__/engageMessageDb.test.js @@ -1,9 +1,16 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ +import Random from 'meteor-random'; import { connect, disconnect } from '../db/connection'; -import { userFactory, segmentsFactory, engageMessageFactory } from '../db/factories'; -import { EngageMessages, Users, Segments } from '../db/models'; +import { EngageMessages, Users, Segments, Customers } from '../db/models'; +import { send } from '../data/resolvers/mutations/engageUtils'; +import { + userFactory, + segmentsFactory, + engageMessageFactory, + customerFactory, +} from '../db/factories'; beforeAll(() => connect()); afterAll(() => disconnect()); @@ -12,17 +19,22 @@ describe('engage messages model tests', () => { let _user; let _segment; let _message; + let _customer; + let _customer2; beforeEach(async () => { _user = await userFactory({}); _segment = await segmentsFactory({}); _message = await engageMessageFactory({}); + _customer = await customerFactory({}); + _customer2 = await customerFactory({}); }); afterEach(async () => { await Users.remove({}); await Segments.remove({}); await EngageMessages.remove({}); + await Customers.remove({}); }); test('create messages', async () => { @@ -85,4 +97,58 @@ describe('engage messages model tests', () => { expect(e.message).toEqual(`Engage message not found with id ${_segment._id}`); } }); + + test('Engage utils send via messenger', async () => { + expect.assertions(1); + try { + await send({ + _id: _message._id, + method: 'messenger', + title: 'Send via messenger', + fromUserId: _user._id, + segmentId: _segment._id, + isLive: true, + messenger: { + brandId: 'brandId', + content: 'content', + }, + }); + } catch (e) { + expect(e.message).toEqual('Integration not found'); + } + }); + + test('save matched customer ids', async () => { + const message = await EngageMessages.saveMatchedCustomerIds(_message._id, [ + _customer, + _customer2, + ]); + + expect(message.customerIds).toContain(_customer._id); + expect(message.customerIds).toContain(_customer2._id); + expect(message.customerIds.length).toEqual(2); + }); + + test('add new delivery report', async () => { + const mailMessageId = Random.id(); + const message = await EngageMessages.addNewDeliveryReport( + _message._id, + mailMessageId, + _customer._id, + ); + + expect(message.deliveryReports[`${mailMessageId}`].status).toEqual('pending'); + expect(message.deliveryReports[`${mailMessageId}`].customerId).toEqual(_customer._id); + }); + + test('change delivery report status', async () => { + const mailMessageId = Random.id(); + const message = await EngageMessages.changeDeliveryReportStatus( + _message._id, + mailMessageId, + 'sent', + ); + + expect(message.deliveryReports[`${mailMessageId}`].status).toEqual('sent'); + }); }); diff --git a/src/__tests__/engageMessageMutations.test.js b/src/__tests__/engageMessageMutations.test.js index b706ec64f..37e659c81 100644 --- a/src/__tests__/engageMessageMutations.test.js +++ b/src/__tests__/engageMessageMutations.test.js @@ -2,29 +2,55 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { userFactory, segmentsFactory, engageMessageFactory } from '../db/factories'; -import { EngageMessages, Users, Segments } from '../db/models'; import mutations from '../data/resolvers/mutations'; +import { + EngageMessages, + Users, + Segments, + Customers, + EmailTemplates, + Integrations, +} from '../db/models'; +import { + engageMessageFactory, + userFactory, + segmentsFactory, + emailTemplateFactory, + customerFactory, + integrationFactory, +} from '../db/factories'; beforeAll(() => connect()); afterAll(() => disconnect()); describe('engage message mutation tests', () => { + let _message; let _user; let _segment; - let _message; + let _customer; + let _emailTemplate; let _doc = null; beforeEach(async () => { _user = await userFactory({}); _segment = await segmentsFactory({}); _message = await engageMessageFactory({}); + _emailTemplate = await emailTemplateFactory({}); + _customer = await customerFactory({}); + await integrationFactory({ brandId: 'brandId' }); _doc = { kind: 'manual', method: 'email', title: 'Message test', fromUserId: _user._id, segmentId: _segment._id, + isLive: true, + customerIds: [_customer._id], + email: { + templateId: _emailTemplate._id, + subject: 'String', + content: 'String asd', + }, }; }); @@ -33,6 +59,9 @@ describe('engage message mutation tests', () => { await Users.remove({}); await Segments.remove({}); await EngageMessages.remove({}); + await EmailTemplates.remove({}); + await Customers.remove({}); + await Integrations.remove({}); }); test('Check login required', async () => { @@ -66,7 +95,11 @@ describe('engage message mutation tests', () => { }); test('messages create', async () => { - EngageMessages.createEngageMessage = jest.fn(); + EngageMessages.createEngageMessage = jest.fn(() => ({ + _id: 'ghghghgh', + ..._doc, + })); + await mutations.engageMessageAdd(null, _doc, { user: _user }); expect(EngageMessages.createEngageMessage).toBeCalledWith(_doc); @@ -105,7 +138,20 @@ describe('engage message mutation tests', () => { }); test('set live manual', async () => { - EngageMessages.engageMessageSetLive = jest.fn(); + EngageMessages.engageMessageSetLive = jest.fn(() => ({ + _id: _message._id, + method: 'messenger', + title: 'Send via messenger', + fromUserId: _user._id, + segmentId: _segment._id, + isLive: true, + customerIds: [_customer._id], + messenger: { + brandId: 'brandId', + content: 'content', + }, + })); + await mutations.engageMessageSetLiveManual(null, _message._id, { user: _user }); expect(EngageMessages.engageMessageSetLive).toBeCalledWith(_message._id); diff --git a/src/data/resolvers/mutations/engageUtils.js b/src/data/resolvers/mutations/engageUtils.js new file mode 100644 index 000000000..71dbec6ad --- /dev/null +++ b/src/data/resolvers/mutations/engageUtils.js @@ -0,0 +1,156 @@ +import { + EngageMessages, + Customers, + Users, + EmailTemplates, + Integrations, + Segments, + Conversations, + ConversationMessages, +} from '../../../db/models'; +import { + EMAIL_CONTENT_PLACEHOLDER, + METHODS, + MESSAGE_KINDS, + INTEGRATION_KIND_CHOICES, +} from '../../constants'; +import Random from 'meteor-random'; +import QueryBuilder from '../queries/segmentQueryBuilder'; +import { createTransporter } from '../../utils'; + +export const replaceKeys = ({ content, customer, user }) => { + let result = content; + + // replace customer fields + result = result.replace(/{{\s?customer.name\s?}}/gi, customer.name); + result = result.replace(/{{\s?customer.email\s?}}/gi, customer.email); + + // replace user fields + result = result.replace(/{{\s?user.fullName\s?}}/gi, user.fullName); + result = result.replace(/{{\s?user.position\s?}}/gi, user.position); + result = result.replace(/{{\s?user.email\s?}}/gi, user.email); + + return result; +}; + +const findCustomers = async ({ customerIds, segmentId }) => { + // find matched customers + let customerQuery = { _id: { $in: customerIds || [] } }; + + if (segmentId) { + const segment = await Segments.findOne({ _id: segmentId }); + customerQuery = QueryBuilder.segments(segment); + } + + return await Customers.find(customerQuery); +}; + +const sendViaEmail = async message => { + const { fromUserId, segmentId, customerIds } = message; + const { templateId, subject, content } = message.email; + + const user = await Users.findOne({ _id: fromUserId }); + const userEmail = user.emails.pop(); + const template = await EmailTemplates.findOne({ _id: templateId }); + + // find matched customers + const customers = await findCustomers({ customerIds, segmentId }); + + // save matched customer ids + EngageMessages.saveMatchedCustomerIds(message._id, customers); + + for (let customer of customers) { + // replace keys in subject + const replacedSubject = replaceKeys({ content: subject, customer, user }); + + // replace keys such as {{ customer.name }} in content + let replacedContent = replaceKeys({ content, customer, user }); + + // if sender choosed some template then use it + if (template) { + replacedContent = template.content.replace(EMAIL_CONTENT_PLACEHOLDER, replacedContent); + } + + const mailMessageId = Random.id(); + + // add new delivery report + EngageMessages.addNewDeliveryReport(message._id, mailMessageId, customer._id); + + // send email + const transporter = await createTransporter(); + transporter.sendMail( + { + from: userEmail.address, + to: customer.email, + subject: replacedSubject, + html: replacedContent, + }, + error => { + // set new status + const status = error ? 'failed' : 'sent'; + + // update status + EngageMessages.changeDeliveryReportStatus(message._id, mailMessageId, status); + }, + ); + } +}; + +const sendViaMessenger = async message => { + const { fromUserId, segmentId, customerIds } = message; + const { brandId, content } = message.messenger; + + const user = Users.findOne({ _id: fromUserId }); + + // find integration + const integration = await Integrations.findOne({ + brandId, + kind: INTEGRATION_KIND_CHOICES.MESSENGER, + }); + + if (integration === null) throw new Error('Integration not found'); + + // find matched customers + const customers = await findCustomers({ customerIds, segmentId }); + // save matched customer ids + EngageMessages.saveMatchedCustomerIds(message._id, customers); + + for (let customer of customers) { + // replace keys in content + const replacedContent = replaceKeys({ content, customer, user }); + + // create conversation + const conversationId = await Conversations.createConversation({ + userId: fromUserId, + customerId: customer._id, + integrationId: integration._id, + content: replacedContent, + }); + + // create message + ConversationMessages.createMessage({ + engageData: { + messageId: message._id, + fromUserId, + ...message.messenger, + }, + conversationId, + userId: fromUserId, + customerId: customer._id, + content: replacedContent, + }); + } +}; + +export const send = message => { + const { method, kind } = message; + + if (method === METHODS.EMAIL) { + return sendViaEmail(message); + } + + // when kind is visitor auto, do not do anything + if (method === METHODS.MESSENGER && kind !== MESSAGE_KINDS.VISITOR_AUTO) { + return sendViaMessenger(message); + } +}; diff --git a/src/data/resolvers/mutations/engages.js b/src/data/resolvers/mutations/engages.js index b5275825a..6763a80eb 100644 --- a/src/data/resolvers/mutations/engages.js +++ b/src/data/resolvers/mutations/engages.js @@ -1,4 +1,6 @@ import { EngageMessages } from '../../../db/models'; +import { MESSAGE_KINDS } from '../../constants'; +import { send } from './engageUtils'; export default { /** @@ -16,10 +18,17 @@ export default { * @param {[String]} doc.tagIds * @return {Promise} message object */ - engageMessageAdd(root, doc, { user }) { + async engageMessageAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); - return EngageMessages.createEngageMessage(doc); + const engageMessage = EngageMessages.createEngageMessage(doc); + + // if manual and live then send immediately + if (doc.kind === MESSAGE_KINDS.MANUAL && doc.isLive) { + await send(engageMessage); + } + + return engageMessage; }, /** @@ -81,9 +90,12 @@ export default { * @param {String} id * @return {Promise} updated message object */ - engageMessageSetLiveManual(root, _id, { user }) { + async engageMessageSetLiveManual(root, _id, { user }) { if (!user) throw new Error('Login required'); - return EngageMessages.engageMessageSetLive(_id); + const engageMessage = EngageMessages.engageMessageSetLive(_id); + await send(engageMessage); + + return engageMessage; }, }; diff --git a/src/data/utils.js b/src/data/utils.js index 12bf9fa77..54c8e5695 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -45,20 +45,36 @@ const applyTemplate = async (data, templateName) => { * @param {Boolean} args.isCustom * @return {Promise} */ -export const sendEmail = async ({ toEmails, fromEmail, title, template }) => { - const { MAIL_SERVICE, MAIL_USER, MAIL_PASS, NODE_ENV } = process.env; - // do not send email it is running in test mode - if (NODE_ENV == 'test') { - return; - } +export const createTransporter = async () => { + const { MAIL_SERVICE, MAIL_USER, MAIL_PASS } = process.env; - const transporter = nodemailer.createTransport({ + return nodemailer.createTransport({ service: MAIL_SERVICE, auth: { user: MAIL_USER, pass: MAIL_PASS, }, }); +}; + +/** + * Send email + * @param {Array} args.toEmails + * @param {String} args.fromEmail + * @param {String} args.title + * @param {String} args.templateArgs.name + * @param {Object} args.templateArgs.data + * @param {Boolean} args.isCustom + * @return {Promise} +*/ +export const sendEmail = async ({ toEmails, fromEmail, title, template }) => { + const { NODE_ENV } = process.env; + // do not send email it is running in test mode + if (NODE_ENV == 'test') { + return; + } + + const transporter = createTransporter(); const { isCustom, data, name } = template; @@ -135,4 +151,5 @@ export default { sendEmail, sendNotification, readFile, + createTransporter, }; diff --git a/src/db/factories.js b/src/db/factories.js index 146f8250c..724fd6014 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -30,6 +30,12 @@ export const userFactory = (params = {}) => { details: { fullName: params.fullName || faker.random.word(), }, + emails: [ + { + address: params.email || faker.internet.email(), + verified: true, + }, + ], }); return user.save(); @@ -49,11 +55,16 @@ export const tagsFactory = (params = { type: 'engageMessage' }) => { export const engageMessageFactory = (params = {}) => { const engageMessage = new EngageMessages({ kind: 'manual', + method: 'messenger', title: faker.random.word(), fromUserId: params.userId || faker.random.word(), segmentId: params.segmentId || faker.random.word(), isLive: true, isDraft: false, + messenger: { + brandId: faker.random.word(), + content: faker.random.word(), + }, }); return engageMessage.save(); diff --git a/src/db/models/Engages.js b/src/db/models/Engages.js index a05e66abe..be496dd74 100644 --- a/src/db/models/Engages.js +++ b/src/db/models/Engages.js @@ -1,6 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -import { MESSENGER_KINDS, SENT_AS_CHOICES } from '../../data/constants'; +import { MESSENGER_KINDS, SENT_AS_CHOICES, METHODS } from '../../data/constants'; const EmailSchema = mongoose.Schema({ templateId: String, @@ -44,7 +44,10 @@ const EngageMessageSchema = mongoose.Schema({ customerIds: [String], title: String, fromUserId: String, - method: String, + method: { + type: String, + enum: METHODS.ALL_LIST, + }, isDraft: Boolean, isLive: Boolean, stopDate: Date, @@ -118,6 +121,61 @@ class Message { return messageObj.remove(); } + + /** + * Save matched customer ids + * @param {String} _id + * @param {[Object]} customers + * @return {Promise} updated message object + */ + static async saveMatchedCustomerIds(_id, customers) { + await this.update({ _id }, { $set: { customerIds: customers.map(customer => customer._id) } }); + + return this.findOne({ _id }); + } + + /** + * Add new delivery report + * @param {String} _id + * @param {String} mailMessageId + * @param {String} customerId + * @return {Promise} updated message object + */ + static async addNewDeliveryReport(_id, mailMessageId, customerId) { + await this.update( + { _id }, + { + $set: { + [`deliveryReports.${mailMessageId}`]: { + customerId, + status: 'pending', + }, + }, + }, + ); + + return this.findOne({ _id }); + } + + /** + * Change delivery report status + * @param {String} _id + * @param {String} mailMessageId + * @param {String} status + * @return {Promise} updated message object + */ + static async changeDeliveryReportStatus(_id, mailMessageId, status) { + await this.update( + { _id }, + { + $set: { + [`deliveryReports.${mailMessageId}.status`]: status, + }, + }, + ); + + return this.findOne({ _id }); + } } EngageMessageSchema.loadClass(Message); diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 5c6427955..5bcd99ba9 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -1,6 +1,11 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +const EmailSchema = mongoose.Schema({ + address: String, + verified: Boolean, +}); + const UserSchema = mongoose.Schema({ _id: { type: String, @@ -9,7 +14,7 @@ const UserSchema = mongoose.Schema({ }, username: String, details: Object, - emails: Object, + emails: [EmailSchema], }); const Users = mongoose.model('users', UserSchema); From 3b56c32d4bdb60dbfcd63e7f2bfd2ec5b9523097 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Thu, 19 Oct 2017 14:56:26 +0800 Subject: [PATCH 095/318] Refactor engage tests --- src/__tests__/engageMessageDb.test.js | 26 +-------------- src/__tests__/engageMessageMutations.test.js | 33 +++++++++++++++++++- src/data/resolvers/mutations/engageUtils.js | 31 ++++++++++++++++-- src/data/resolvers/mutations/engages.js | 9 +++--- src/data/utils.js | 13 +++----- src/db/models/Engages.js | 26 +++++++-------- 6 files changed, 84 insertions(+), 54 deletions(-) diff --git a/src/__tests__/engageMessageDb.test.js b/src/__tests__/engageMessageDb.test.js index 468121566..96728a192 100644 --- a/src/__tests__/engageMessageDb.test.js +++ b/src/__tests__/engageMessageDb.test.js @@ -4,7 +4,6 @@ import Random from 'meteor-random'; import { connect, disconnect } from '../db/connection'; import { EngageMessages, Users, Segments, Customers } from '../db/models'; -import { send } from '../data/resolvers/mutations/engageUtils'; import { userFactory, segmentsFactory, @@ -98,31 +97,8 @@ describe('engage messages model tests', () => { } }); - test('Engage utils send via messenger', async () => { - expect.assertions(1); - try { - await send({ - _id: _message._id, - method: 'messenger', - title: 'Send via messenger', - fromUserId: _user._id, - segmentId: _segment._id, - isLive: true, - messenger: { - brandId: 'brandId', - content: 'content', - }, - }); - } catch (e) { - expect(e.message).toEqual('Integration not found'); - } - }); - test('save matched customer ids', async () => { - const message = await EngageMessages.saveMatchedCustomerIds(_message._id, [ - _customer, - _customer2, - ]); + const message = await EngageMessages.setCustomerIds(_message._id, [_customer, _customer2]); expect(message.customerIds).toContain(_customer._id); expect(message.customerIds).toContain(_customer2._id); diff --git a/src/__tests__/engageMessageMutations.test.js b/src/__tests__/engageMessageMutations.test.js index 37e659c81..210f4d4b4 100644 --- a/src/__tests__/engageMessageMutations.test.js +++ b/src/__tests__/engageMessageMutations.test.js @@ -3,6 +3,7 @@ import { connect, disconnect } from '../db/connection'; import mutations from '../data/resolvers/mutations'; +import { send } from '../data/resolvers/mutations/engageUtils'; import { EngageMessages, Users, @@ -10,6 +11,8 @@ import { Customers, EmailTemplates, Integrations, + Conversations, + ConversationMessages, } from '../db/models'; import { engageMessageFactory, @@ -94,16 +97,39 @@ describe('engage message mutation tests', () => { check(mutations.engageMessageSetLiveManual); }); + test('Engage utils send via messenger', async () => { + expect.assertions(1); + + try { + await send({ + _id: _message._id, + method: 'messenger', + title: 'Send via messenger', + fromUserId: _user._id, + segmentId: _segment._id, + isLive: true, + messenger: { + brandId: '', + content: 'content', + }, + }); + } catch (e) { + expect(e.message).toEqual('Integration not found'); + } + }); + test('messages create', async () => { EngageMessages.createEngageMessage = jest.fn(() => ({ _id: 'ghghghgh', ..._doc, })); + EngageMessages.addNewDeliveryReport = jest.fn(); await mutations.engageMessageAdd(null, _doc, { user: _user }); expect(EngageMessages.createEngageMessage).toBeCalledWith(_doc); expect(EngageMessages.createEngageMessage.mock.calls.length).toBe(1); + expect(EngageMessages.addNewDeliveryReport.mock.calls.length).toBe(1); }); test('messages update', async () => { @@ -148,13 +174,18 @@ describe('engage message mutation tests', () => { customerIds: [_customer._id], messenger: { brandId: 'brandId', - content: 'content', + content: 'messenger content {{ customer.name }}', }, })); + Conversations.createConversation = jest.fn(); + ConversationMessages.createMessage = jest.fn(); + await mutations.engageMessageSetLiveManual(null, _message._id, { user: _user }); expect(EngageMessages.engageMessageSetLive).toBeCalledWith(_message._id); expect(EngageMessages.engageMessageSetLive.mock.calls.length).toBe(1); + expect(Conversations.createConversation.mock.calls.length).toBe(1); + expect(ConversationMessages.createMessage.mock.calls.length).toBe(1); }); }); diff --git a/src/data/resolvers/mutations/engageUtils.js b/src/data/resolvers/mutations/engageUtils.js index 71dbec6ad..5647e94ee 100644 --- a/src/data/resolvers/mutations/engageUtils.js +++ b/src/data/resolvers/mutations/engageUtils.js @@ -18,6 +18,18 @@ import Random from 'meteor-random'; import QueryBuilder from '../queries/segmentQueryBuilder'; import { createTransporter } from '../../utils'; +/** + * Dynamic content tags + * @param {String} content + * @param {Object} customer + * @param {String} customer.name + * @param {String} customer.email + * @param {Object} user + * @param {String} user.fullName + * @param {String} user.position + * @param {String} user.email + * @return replaced content text + */ export const replaceKeys = ({ content, customer, user }) => { let result = content; @@ -33,6 +45,12 @@ export const replaceKeys = ({ content, customer, user }) => { return result; }; +/** + * Find customers + * @param {[String]} customerIds + * @param {String} segmentId + * @return {Promise} customers + */ const findCustomers = async ({ customerIds, segmentId }) => { // find matched customers let customerQuery = { _id: { $in: customerIds || [] } }; @@ -45,6 +63,10 @@ const findCustomers = async ({ customerIds, segmentId }) => { return await Customers.find(customerQuery); }; +/** + * Send via email + * @param {Object} engage message object + */ const sendViaEmail = async message => { const { fromUserId, segmentId, customerIds } = message; const { templateId, subject, content } = message.email; @@ -57,7 +79,7 @@ const sendViaEmail = async message => { const customers = await findCustomers({ customerIds, segmentId }); // save matched customer ids - EngageMessages.saveMatchedCustomerIds(message._id, customers); + EngageMessages.setCustomerIds(message._id, customers); for (let customer of customers) { // replace keys in subject @@ -96,6 +118,10 @@ const sendViaEmail = async message => { } }; +/** + * Send via messenger + * @param {Object} engage message object + */ const sendViaMessenger = async message => { const { fromUserId, segmentId, customerIds } = message; const { brandId, content } = message.messenger; @@ -112,8 +138,9 @@ const sendViaMessenger = async message => { // find matched customers const customers = await findCustomers({ customerIds, segmentId }); + // save matched customer ids - EngageMessages.saveMatchedCustomerIds(message._id, customers); + EngageMessages.setCustomerIds(message._id, customers); for (let customer of customers) { // replace keys in content diff --git a/src/data/resolvers/mutations/engages.js b/src/data/resolvers/mutations/engages.js index 6763a80eb..5d1c82bb2 100644 --- a/src/data/resolvers/mutations/engages.js +++ b/src/data/resolvers/mutations/engages.js @@ -54,7 +54,7 @@ export default { /** * Remove message - * @param {String} id + * @param {String} _id - Engage message id * @return {Promise} */ engageMessageRemove(root, _id, { user }) { @@ -65,7 +65,7 @@ export default { /** * Engage message set live - * @param {String} id + * @param {String} _id - Engage message id * @return {Promise} updated message object */ engageMessageSetLive(root, _id, { user }) { @@ -76,7 +76,7 @@ export default { /** * Engage message set pause - * @param {String} id + * @param {String} _id - Engage message id * @return {Promise} updated message object */ engageMessageSetPause(root, _id, { user }) { @@ -87,13 +87,14 @@ export default { /** * Engage message set live manual - * @param {String} id + * @param {String} _id - Engage message id * @return {Promise} updated message object */ async engageMessageSetLiveManual(root, _id, { user }) { if (!user) throw new Error('Login required'); const engageMessage = EngageMessages.engageMessageSetLive(_id); + await send(engageMessage); return engageMessage; diff --git a/src/data/utils.js b/src/data/utils.js index 54c8e5695..cdf10f3d7 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -36,14 +36,8 @@ const applyTemplate = async (data, templateName) => { }; /** - * Send email - * @param {Array} args.toEmails - * @param {String} args.fromEmail - * @param {String} args.title - * @param {String} args.templateArgs.name - * @param {Object} args.templateArgs.data - * @param {Boolean} args.isCustom - * @return {Promise} + * Create transporter + * @return nodemailer transporter */ export const createTransporter = async () => { const { MAIL_SERVICE, MAIL_USER, MAIL_PASS } = process.env; @@ -64,11 +58,12 @@ export const createTransporter = async () => { * @param {String} args.title * @param {String} args.templateArgs.name * @param {Object} args.templateArgs.data - * @param {Boolean} args.isCustom + * @param {Boolean} args.templateArgs.isCustom * @return {Promise} */ export const sendEmail = async ({ toEmails, fromEmail, title, template }) => { const { NODE_ENV } = process.env; + // do not send email it is running in test mode if (NODE_ENV == 'test') { return; diff --git a/src/db/models/Engages.js b/src/db/models/Engages.js index be496dd74..a78fce4ed 100644 --- a/src/db/models/Engages.js +++ b/src/db/models/Engages.js @@ -77,7 +77,7 @@ class Message { /** * Update engage message - * @param {String} _id + * @param {String} _id - Engage message id * @param {Object} doc object * @return {Promise} updated message object */ @@ -89,7 +89,7 @@ class Message { /** * Engage message set live - * @param {String} _id + * @param {String} _id - Engage message id * @return {Promise} updated message object */ static async engageMessageSetLive(_id) { @@ -100,7 +100,7 @@ class Message { /** * Engage message set pause - * @param {String} _id + * @param {String} _id - Engage message id * @return {Promise} updated message object */ static async engageMessageSetPause(_id) { @@ -111,7 +111,7 @@ class Message { /** * Remove engage message - * @param {String} _id + * @param {String} _id - Engage message id * @return {Promise} */ static async removeEngageMessage(_id) { @@ -124,11 +124,11 @@ class Message { /** * Save matched customer ids - * @param {String} _id - * @param {[Object]} customers + * @param {String} _id - Engage message id + * @param {[Object]} customers - Customers object * @return {Promise} updated message object */ - static async saveMatchedCustomerIds(_id, customers) { + static async setCustomerIds(_id, customers) { await this.update({ _id }, { $set: { customerIds: customers.map(customer => customer._id) } }); return this.findOne({ _id }); @@ -136,9 +136,9 @@ class Message { /** * Add new delivery report - * @param {String} _id - * @param {String} mailMessageId - * @param {String} customerId + * @param {String} _id - Engage message id + * @param {String} mailMessageId - Random mail message id + * @param {String} customerId - Customer id * @return {Promise} updated message object */ static async addNewDeliveryReport(_id, mailMessageId, customerId) { @@ -159,9 +159,9 @@ class Message { /** * Change delivery report status - * @param {String} _id - * @param {String} mailMessageId - * @param {String} status + * @param {String} _id - Engage message id + * @param {String} mailMessageId - Random mail message id + * @param {String} status - pending, send, failed etc... * @return {Promise} updated message object */ static async changeDeliveryReportStatus(_id, mailMessageId, status) { From 08f780fba77cfe121a7abb77476ab70c6203b6f2 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Thu, 19 Oct 2017 15:35:42 +0800 Subject: [PATCH 096/318] Engage test call with data --- src/__tests__/engageMessageMutations.test.js | 29 ++++++++++++++++++-- src/data/resolvers/mutations/engageUtils.js | 18 ++++++------ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/__tests__/engageMessageMutations.test.js b/src/__tests__/engageMessageMutations.test.js index 210f4d4b4..ed1b46772 100644 --- a/src/__tests__/engageMessageMutations.test.js +++ b/src/__tests__/engageMessageMutations.test.js @@ -31,6 +31,7 @@ describe('engage message mutation tests', () => { let _user; let _segment; let _customer; + let _integration; let _emailTemplate; let _doc = null; @@ -40,7 +41,7 @@ describe('engage message mutation tests', () => { _message = await engageMessageFactory({}); _emailTemplate = await emailTemplateFactory({}); _customer = await customerFactory({}); - await integrationFactory({ brandId: 'brandId' }); + _integration = await integrationFactory({ brandId: 'brandId' }); _doc = { kind: 'manual', method: 'email', @@ -174,18 +175,40 @@ describe('engage message mutation tests', () => { customerIds: [_customer._id], messenger: { brandId: 'brandId', - content: 'messenger content {{ customer.name }}', + content: 'messenger content', }, })); - Conversations.createConversation = jest.fn(); + const conversationObj = { + userId: _user._id, + customerId: _customer._id, + integrationId: _integration._id, + content: 'messenger content', + }; + + const conversationMessageObj = { + engageData: { + messageId: _message._id, + fromUserId: _user._id, + brandId: 'brandId', + content: 'messenger content', + }, + conversationId: 'convId', + userId: _user._id, + customerId: _customer._id, + content: 'messenger content', + }; + + Conversations.createConversation = jest.fn(() => ({ _id: 'convId' })); ConversationMessages.createMessage = jest.fn(); await mutations.engageMessageSetLiveManual(null, _message._id, { user: _user }); expect(EngageMessages.engageMessageSetLive).toBeCalledWith(_message._id); expect(EngageMessages.engageMessageSetLive.mock.calls.length).toBe(1); + expect(Conversations.createConversation).toBeCalledWith(conversationObj); expect(Conversations.createConversation.mock.calls.length).toBe(1); + expect(ConversationMessages.createMessage).toBeCalledWith(conversationMessageObj); expect(ConversationMessages.createMessage.mock.calls.length).toBe(1); }); }); diff --git a/src/data/resolvers/mutations/engageUtils.js b/src/data/resolvers/mutations/engageUtils.js index 5647e94ee..fe8b3e449 100644 --- a/src/data/resolvers/mutations/engageUtils.js +++ b/src/data/resolvers/mutations/engageUtils.js @@ -22,12 +22,12 @@ import { createTransporter } from '../../utils'; * Dynamic content tags * @param {String} content * @param {Object} customer - * @param {String} customer.name - * @param {String} customer.email + * @param {String} customer.name - Customer name + * @param {String} customer.email - Customer email * @param {Object} user - * @param {String} user.fullName - * @param {String} user.position - * @param {String} user.email + * @param {String} user.fullName - User full name + * @param {String} user.position - User position + * @param {String} user.email - User email * @return replaced content text */ export const replaceKeys = ({ content, customer, user }) => { @@ -47,8 +47,8 @@ export const replaceKeys = ({ content, customer, user }) => { /** * Find customers - * @param {[String]} customerIds - * @param {String} segmentId + * @param {[String]} customerIds - Customer ids + * @param {String} segmentId - Segment id * @return {Promise} customers */ const findCustomers = async ({ customerIds, segmentId }) => { @@ -147,7 +147,7 @@ const sendViaMessenger = async message => { const replacedContent = replaceKeys({ content, customer, user }); // create conversation - const conversationId = await Conversations.createConversation({ + const conversation = await Conversations.createConversation({ userId: fromUserId, customerId: customer._id, integrationId: integration._id, @@ -161,7 +161,7 @@ const sendViaMessenger = async message => { fromUserId, ...message.messenger, }, - conversationId, + conversationId: conversation._id, userId: fromUserId, customerId: customer._id, content: replacedContent, From d91fdd335c1173afc21a2d89d3863670049ca044 Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Thu, 19 Oct 2017 17:24:15 +0800 Subject: [PATCH 097/318] Update conversation message count after message add or remove --- src/__tests__/conversationDb.test.js | 43 ++++++++++++++++++++-- src/db/factories.js | 8 +--- src/db/models/Conversations.js | 55 +++++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 11 deletions(-) diff --git a/src/__tests__/conversationDb.test.js b/src/__tests__/conversationDb.test.js index 93ccf2b34..7f8a14dc9 100644 --- a/src/__tests__/conversationDb.test.js +++ b/src/__tests__/conversationDb.test.js @@ -19,7 +19,7 @@ describe('Conversation db', () => { beforeEach(async () => { // Creating test data _conversation = await conversationFactory(); - _conversationMessage = await conversationMessageFactory(); + _conversationMessage = await conversationMessageFactory({ conversationId: _conversation._id }); _user = await userFactory(); _doc = { ..._conversationMessage._doc, conversationId: _conversation._id }; @@ -65,6 +65,9 @@ describe('Conversation db', () => { } }); test('Create conversation message', async () => { + // get messageCount before add message + const prevConversationObj = await Conversations.findOne({ _id: _doc.conversationId }); + const messageObj = await ConversationMessages.addMessage(_doc, _user); expect(messageObj.content).toBe(_conversationMessage.content); @@ -96,6 +99,20 @@ describe('Conversation db', () => { } catch (e) { expect(e.message).toEqual('Conversation not found with id test'); } + + // get messageCount after add message + const afterConversationObj = await Conversations.findOne({ _id: messageObj.conversationId }); + + // check mendtioned users + for (let mentionedUser of messageObj.mentionedUserIds) { + expect(afterConversationObj.participatedUserIds).toContain(mentionedUser); + } + + // check participated users + expect(afterConversationObj.participatedUserIds).toContain(messageObj.userId); + + // check if message count increase + expect(afterConversationObj.messageCount).toBe(prevConversationObj.messageCount + 1); }); // if user assigned to conversation @@ -168,7 +185,7 @@ describe('Conversation db', () => { const conversationObj = await Conversations.findOne({ _id: _conversation.id }); - expect(conversationObj.participatedUserIds[0]).toBe(_user._id); + expect(conversationObj.participatedUserIds).toContain(_user._id); // remove user from conversation await Conversations.toggleParticipatedUsers([_conversation._id], _user.id); @@ -177,7 +194,7 @@ describe('Conversation db', () => { _id: _conversation.id, }); - expect(conversationObjWithParticipatedUser.participatedUserIds.length).toBe(0); + expect(conversationObjWithParticipatedUser.participatedUserIds.indexOf(_user.id)).toBe(-1); }); test('Conversation mark as read', async () => { @@ -203,4 +220,24 @@ describe('Conversation db', () => { expect(e.message).toEqual('Conversation not found with id test'); } }); + + test('Conversation message remove', async () => { + // get conversation message count before message delete + await ConversationMessages.addMessage(_doc, _user); + + const beforeConversation = await Conversations.findOne({ + _id: _conversationMessage.conversationId, + }); + + await ConversationMessages.removeMessage({ _id: _conversationMessage._id }); + + expect(await ConversationMessages.find({ _id: _conversationMessage._id }).count()).toBe(0); + + const afterConversation = await Conversations.findOne({ + _id: _conversationMessage.conversationId, + }); + + // Conversation message count subtracted + expect(beforeConversation.messageCount).toBe(afterConversation.messageCount + 1); + }); }); diff --git a/src/db/factories.js b/src/db/factories.js index 146f8250c..e96e61ce1 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -172,18 +172,16 @@ export const fieldFactory = (params = {}) => { }; export const conversationFactory = (params = {}) => { - const conversation = new Conversations({ + return Conversations.createConversation({ content: params.content || faker.lorem.sentence(), customerId: params.customerId || Random.id(), integrationId: params.integrationId || Random.id(), status: CONVERSATION_STATUSES.NEW, }); - - return conversation.save(); }; export const conversationMessageFactory = (params = {}) => { - const conversationMessage = new ConversationMessages({ + return ConversationMessages.createMessage({ content: params.content || faker.random.word(), attachments: {}, mentionedUserIds: params.mentionedUserIds || [Random.id()], @@ -197,8 +195,6 @@ export const conversationMessageFactory = (params = {}) => { formWidgetData: params.formWidgetData || {}, facebookData: params.facebookData || {}, }); - - return conversationMessage.save(); }; export const integrationFactory = (params = {}) => { diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index 15a3ac96e..36cbcb26f 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -314,11 +314,42 @@ class Message { * @param {Object} messageObj object * @return {Promise} Newly created message object */ - static createMessage(doc) { - return this.create({ + static async createMessage(doc) { + // const message = Object.assign({ createdAt: new Date() }, doc); + const message = await this.create({ ...doc, createdAt: new Date(), }); + + const messageCount = await this.find({ + conversationId: message.conversationId, + }).count(); + + await Conversations.update({ _id: message.conversationId }, { $set: { messageCount } }); + + // add created user to participators + if (message.conversationId && message.userId) { + await Conversations.update( + { _id: message.conversationId }, + { + $addToSet: { participatedUserIds: message.userId }, + }, + ); + } + + // add mentioned users to participators + for (let userId of message.mentionedUserIds) { + if (message.conversationId && userId) { + await Conversations.update( + { _id: message.conversationId }, + { + $addToSet: { participatedUserIds: userId }, + }, + ); + } + } + + return message; } /** @@ -347,6 +378,26 @@ class Message { return this.createMessage({ ...doc, userId }); } + + /** + * Remove a message + * @param {Object} selector + * @return {Promise} Deleted message object + */ + static async removeMessage(selector) { + const messages = await this.find(selector); + const result = await this.remove(selector); + + for (let message of messages) { + const messageCount = await Messages.find({ + conversationId: message.conversationId, + }).count(); + + await Conversations.update({ _id: message.conversationId }, { $set: { messageCount } }); + } + + return result; + } } MessageSchema.loadClass(Message); From b86004ed2f1c73e48804cb9431d2c0dd4c8a93a9 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 20 Oct 2017 19:08:49 +0800 Subject: [PATCH 098/318] Rename channelsCreate to channelsAdd --- src/__tests__/channelMutations.test.js | 6 +++--- src/data/resolvers/mutations/channels.js | 2 +- src/data/schema/channel.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/__tests__/channelMutations.test.js b/src/__tests__/channelMutations.test.js index d6b30a604..fd5b5bd4e 100644 --- a/src/__tests__/channelMutations.test.js +++ b/src/__tests__/channelMutations.test.js @@ -34,12 +34,12 @@ describe('mutations', () => { } }; - expectError(channelMutations.channelsCreate); + expectError(channelMutations.channelsAdd); expectError(channelMutations.channelsEdit); expectError(channelMutations.channelsRemove); }); - test('test mutations.channelsCreate', async () => { + test('test mutations.channelsAdd', async () => { let doc = { name: 'Channel test', description: 'test channel descripion', @@ -71,7 +71,7 @@ describe('mutations', () => { jest.spyOn(utils, 'sendNotification').mockImplementation(() => ({})); - await channelMutations.channelsCreate(null, doc, { user: _user }); + await channelMutations.channelsAdd(null, doc, { user: _user }); expect(Channels.createChannel).toBeCalledWith(doc, _user); expect(Channels.createChannel.mock.calls.length).toBe(1); diff --git a/src/data/resolvers/mutations/channels.js b/src/data/resolvers/mutations/channels.js index 9b1d7bf79..46a869317 100644 --- a/src/data/resolvers/mutations/channels.js +++ b/src/data/resolvers/mutations/channels.js @@ -38,7 +38,7 @@ export default { * @return {Promise} return Promise resolving created Channel document * @throws {Error} throws Error('Login required') if user is not logged in */ - async channelsCreate(root, doc, { user }) { + async channelsAdd(root, doc, { user }) { if (!user) { throw new Error('Login required'); } diff --git a/src/data/schema/channel.js b/src/data/schema/channel.js index 0315bacaf..679df4320 100644 --- a/src/data/schema/channel.js +++ b/src/data/schema/channel.js @@ -18,7 +18,7 @@ export const queries = ` `; export const mutations = ` - channelsCreate( + channelsAdd( name: String!, description: String, memberIds: [String], From 3a437661be50a640f4113d829f8774d92118e04c Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 21 Oct 2017 01:15:05 +0800 Subject: [PATCH 099/318] Generate code when brand insert --- src/__tests__/brandDb.test.js | 19 +++++++----- src/data/resolvers/mutations/channels.js | 6 ++-- src/data/schema/brand.js | 4 +-- src/data/schema/channel.js | 4 +-- src/db/models/Brands.js | 38 ++++++++++++++++++++---- 5 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/__tests__/brandDb.test.js b/src/__tests__/brandDb.test.js index 69bff8241..2a2cbd446 100644 --- a/src/__tests__/brandDb.test.js +++ b/src/__tests__/brandDb.test.js @@ -25,22 +25,27 @@ describe('Brands db', () => { await Users.remove({}); }); + test('Generate code', async () => { + // try using exisiting one + let code = await Brands.generateCode(_brand.code); + expect(code).not.toBe(_brand.code); + expect(code).toBeDefined(); + + // try using not existing one + code = await Brands.generateCode('DFAFFADFSF'); + expect(code).toBeDefined(); + }); + test('Create brand', async () => { const brandObj = await Brands.createBrand({ - code: _brand.code, name: _brand.name, description: _brand.description, userId: _user.id, }); expect(brandObj).toBeDefined(); - expect(brandObj.code).toBe(_brand.code); + expect(brandObj.code).toBeDefined(); expect(brandObj.name).toBe(_brand.name); expect(brandObj.userId).toBe(_user._id); - - // invalid data - expect(() => { - Brands.createBrand({ code: '', name: _brand.name, userId: _user.id }); - }).toThrowError('Code is required field'); }); test('Update brand', async () => { diff --git a/src/data/resolvers/mutations/channels.js b/src/data/resolvers/mutations/channels.js index 46a869317..cbe82b1b1 100644 --- a/src/data/resolvers/mutations/channels.js +++ b/src/data/resolvers/mutations/channels.js @@ -86,11 +86,13 @@ export default { * @return {Promise} * @throws {Error} throws Error('Login required') if user is not logged in */ - channelsRemove(root, { _id }, { user }) { + async channelsRemove(root, { _id }, { user }) { if (!user) { throw new Error('Login required'); } - return Channels.removeChannel(_id); + await Channels.removeChannel(_id); + + return _id; }, }; diff --git a/src/data/schema/brand.js b/src/data/schema/brand.js index a47c94ddc..16953c8f1 100644 --- a/src/data/schema/brand.js +++ b/src/data/schema/brand.js @@ -17,8 +17,8 @@ export const queries = ` `; export const mutations = ` - brandsAdd(code: String!, name: String, description: String): Brand - brandsEdit(_id: String!, code: String, name: String, description: String): Brand + brandsAdd(name: String, description: String): Brand + brandsEdit(_id: String!, name: String, description: String): Brand brandsRemove(_id: String!): String brandsConfigEmail(_id: String!, emailConfig: JSON): Brand `; diff --git a/src/data/schema/channel.js b/src/data/schema/channel.js index 679df4320..dde75269d 100644 --- a/src/data/schema/channel.js +++ b/src/data/schema/channel.js @@ -29,7 +29,7 @@ export const mutations = ` name: String!, description: String, memberIds: [String], - integrationIds: [String]): Boolean + integrationIds: [String]): Channel - channelsRemove(_id: String!): Boolean + channelsRemove(_id: String!): String `; diff --git a/src/db/models/Brands.js b/src/db/models/Brands.js index 4169f4d34..cdc84bd3c 100644 --- a/src/db/models/Brands.js +++ b/src/db/models/Brands.js @@ -24,18 +24,44 @@ const BrandSchema = mongoose.Schema({ }); class Brand { + /* + * Generates new brand code + * @param {String} code - initial code + * @return {String} generatedCode - generated code + */ + static async generateCode(code) { + let generatedCode = code || Random.id().substr(0, 6); + + let prevBrand = await this.findOne({ code: generatedCode }); + + // search until not existing one found + while (prevBrand) { + generatedCode = Random.id().substr(0, 6); + + if (code) { + // eslint-disable-next-line no-console + console.log('User defined brand code already exists. New code is generated.'); + } + + prevBrand = await this.findOne({ code: generatedCode }); + } + + return generatedCode; + } + /** * Create a brand * @param {Object} doc object * @return {Promise} Newly created brand object */ - static createBrand(doc) { - if (!doc.code) throw new Error('Code is required field'); + static async createBrand(doc) { + // generate code automatically + // if there is no brand code defined + doc.code = await this.generateCode(doc.code); + doc.createdAt = new Date(); + doc.emailConfig = { type: 'simple' }; - return this.create({ - ...doc, - createdAt: new Date(), - }); + return this.create(doc); } /** From 1b29634b928fa1ec2525aab2289dad4460936147 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 21 Oct 2017 03:08:00 +0800 Subject: [PATCH 100/318] Fix coding standard in email and response templates --- src/__tests__/emailTemplateMutations.test.js | 12 ++++----- .../responseTemplateMutations.test.js | 12 ++++----- .../resolvers/mutations/emailTemplates.js | 6 ++--- .../resolvers/mutations/responseTemplates.js | 6 ++--- src/data/schema/emailTemplate.js | 8 +++--- src/data/schema/responseTemplate.js | 26 ++++++++++++++----- 6 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/__tests__/emailTemplateMutations.test.js b/src/__tests__/emailTemplateMutations.test.js index d12b8805c..e1cf66f67 100644 --- a/src/__tests__/emailTemplateMutations.test.js +++ b/src/__tests__/emailTemplateMutations.test.js @@ -38,16 +38,16 @@ describe('Email template mutations', () => { expect.assertions(3); // add email template - checkLogin(emailTemplateMutations.emailTemplateAdd, { + checkLogin(emailTemplateMutations.emailTemplatesAdd, { name: _emailTemplate.name, content: _emailTemplate.content, }); // update email template - checkLogin(emailTemplateMutations.emailTemplateEdit, { _id: _emailTemplate.id }); + checkLogin(emailTemplateMutations.emailTemplatesEdit, { _id: _emailTemplate.id }); // remove email template - checkLogin(emailTemplateMutations.emailTemplateRemove, { _id: _emailTemplate.id }); + checkLogin(emailTemplateMutations.emailTemplatesRemove, { _id: _emailTemplate.id }); }); test('Create email template', async () => { @@ -55,7 +55,7 @@ describe('Email template mutations', () => { const _doc = { name: _emailTemplate.name, content: _emailTemplate.content }; - await emailTemplateMutations.emailTemplateAdd({}, _doc, { user: _user }); + await emailTemplateMutations.emailTemplatesAdd({}, _doc, { user: _user }); expect(EmailTemplates.create.mock.calls.length).toBe(1); expect(EmailTemplates.create).toBeCalledWith(_doc); @@ -66,7 +66,7 @@ describe('Email template mutations', () => { const _doc = { name: _emailTemplate.name, content: _emailTemplate.content }; - await emailTemplateMutations.emailTemplateEdit( + await emailTemplateMutations.emailTemplatesEdit( {}, { _id: _emailTemplate.id, ..._doc }, { user: _user }, @@ -79,7 +79,7 @@ describe('Email template mutations', () => { test('Delete email template', async () => { EmailTemplates.removeEmailTemplate = jest.fn(); - await emailTemplateMutations.emailTemplateRemove( + await emailTemplateMutations.emailTemplatesRemove( {}, { _id: _emailTemplate.id }, { user: _user }, diff --git a/src/__tests__/responseTemplateMutations.test.js b/src/__tests__/responseTemplateMutations.test.js index 44fca6f0f..3d4fa4e95 100644 --- a/src/__tests__/responseTemplateMutations.test.js +++ b/src/__tests__/responseTemplateMutations.test.js @@ -38,16 +38,16 @@ describe('Response template mutations', () => { expect.assertions(3); // add response template - checkLogin(responseTemplateMutations.responseTemplateAdd, { + checkLogin(responseTemplateMutations.responseTemplatesAdd, { name: _responseTemplate.name, content: _responseTemplate.content, }); // update response template - checkLogin(responseTemplateMutations.responseTemplateEdit, { _id: _responseTemplate.id }); + checkLogin(responseTemplateMutations.responseTemplatesEdit, { _id: _responseTemplate.id }); // remove response template - checkLogin(responseTemplateMutations.responseTemplateRemove, { _id: _responseTemplate.id }); + checkLogin(responseTemplateMutations.responseTemplatesRemove, { _id: _responseTemplate.id }); }); test('Create response template', async () => { @@ -60,7 +60,7 @@ describe('Response template mutations', () => { files: _responseTemplate.files, }; - await responseTemplateMutations.responseTemplateAdd({}, _doc, { user: _user }); + await responseTemplateMutations.responseTemplatesAdd({}, _doc, { user: _user }); expect(ResponseTemplates.create.mock.calls.length).toBe(1); expect(ResponseTemplates.create).toBeCalledWith(_doc); }); @@ -75,7 +75,7 @@ describe('Response template mutations', () => { files: _responseTemplate.files, }; - await responseTemplateMutations.responseTemplateEdit( + await responseTemplateMutations.responseTemplatesEdit( {}, { _id: _responseTemplate.id, ..._doc }, { user: _user }, @@ -88,7 +88,7 @@ describe('Response template mutations', () => { test('Delete response template', async () => { ResponseTemplates.removeResponseTemplate = jest.fn(); - await responseTemplateMutations.responseTemplateRemove( + await responseTemplateMutations.responseTemplatesRemove( {}, { _id: _responseTemplate.id }, { user: _user }, diff --git a/src/data/resolvers/mutations/emailTemplates.js b/src/data/resolvers/mutations/emailTemplates.js index 57bb671e0..84e09c32a 100644 --- a/src/data/resolvers/mutations/emailTemplates.js +++ b/src/data/resolvers/mutations/emailTemplates.js @@ -6,7 +6,7 @@ export default { * @param {Object} doc - email templates fields * @return {Promise} newly created email template object */ - emailTemplateAdd(root, doc, { user }) { + emailTemplatesAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); return EmailTemplates.create(doc); @@ -18,7 +18,7 @@ export default { * @param {Object} fields - email templates fields * @return {Promise} updated email template object */ - emailTemplateEdit(root, { _id, ...fields }, { user }) { + emailTemplatesEdit(root, { _id, ...fields }, { user }) { if (!user) throw new Error('Login required'); return EmailTemplates.updateEmailTemplate(_id, fields); @@ -29,7 +29,7 @@ export default { * @param {String} doc - email templates fields * @return {Promise} */ - emailTemplateRemove(root, { _id }, { user }) { + emailTemplatesRemove(root, { _id }, { user }) { if (!user) throw new Error('Login required'); return EmailTemplates.removeEmailTemplate(_id); diff --git a/src/data/resolvers/mutations/responseTemplates.js b/src/data/resolvers/mutations/responseTemplates.js index 702764d06..b96686648 100644 --- a/src/data/resolvers/mutations/responseTemplates.js +++ b/src/data/resolvers/mutations/responseTemplates.js @@ -6,7 +6,7 @@ export default { * @param {Object} fields - response template fields * @return {Promise} newly created response template object */ - responseTemplateAdd(root, doc, { user }) { + responseTemplatesAdd(root, doc, { user }) { if (!user) throw new Error('Login required'); return ResponseTemplates.create(doc); @@ -18,7 +18,7 @@ export default { * @param {Object} fields - response template fields * @return {Promise} updated response template object */ - responseTemplateEdit(root, { _id, ...fields }, { user }) { + responseTemplatesEdit(root, { _id, ...fields }, { user }) { if (!user) throw new Error('Login required'); return ResponseTemplates.updateResponseTemplate(_id, fields); @@ -29,7 +29,7 @@ export default { * @param {String} _id - response template id * @return {Promise} */ - responseTemplateRemove(root, { _id }, { user }) { + responseTemplatesRemove(root, { _id }, { user }) { if (!user) throw new Error('Login required'); return ResponseTemplates.removeResponseTemplate(_id); diff --git a/src/data/schema/emailTemplate.js b/src/data/schema/emailTemplate.js index 89f1482d1..206a116ee 100644 --- a/src/data/schema/emailTemplate.js +++ b/src/data/schema/emailTemplate.js @@ -1,7 +1,7 @@ export const types = ` type EmailTemplate { _id: String! - name: String + name: String! content: String } `; @@ -12,7 +12,7 @@ export const queries = ` `; export const mutations = ` - emailTemplateAdd(name: String, content: String): EmailTemplate - emailTemplateEdit(_id: String!, name: String, content: String): EmailTemplate - emailTemplateRemove(_id: String!): EmailTemplate + emailTemplatesAdd(name: String!, content: String): EmailTemplate + emailTemplatesEdit(_id: String!, name: String!, content: String): EmailTemplate + emailTemplatesRemove(_id: String!): String `; diff --git a/src/data/schema/responseTemplate.js b/src/data/schema/responseTemplate.js index cdfe1b2e9..eb16ce7d0 100644 --- a/src/data/schema/responseTemplate.js +++ b/src/data/schema/responseTemplate.js @@ -1,9 +1,10 @@ export const types = ` type ResponseTemplate { _id: String! - name: String + name: String! + brandId: String! content: String - brandId: String + brand: Brand, files: JSON } @@ -15,9 +16,20 @@ export const queries = ` `; export const mutations = ` - responseTemplateAdd(name: String, content: String, brandId: String, files: JSON): - ResponseTemplate - responseTemplateEdit(_id: String!, name: String, content: String, brandId: String, files: JSON): - ResponseTemplate - responseTemplateRemove(_id: String!): ResponseTemplate + responseTemplatesAdd( + brandId: String!, + name: String!, + content: String, + files: JSON + ): ResponseTemplate + + responseTemplatesEdit( + _id: String!, + brandId: String!, + name: String!, + content: String, + files: JSON + ): ResponseTemplate + + responseTemplatesRemove(_id: String!): String `; From 24f6dd78979ed5351f948f6fd64a6d912a3d2145 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 22 Oct 2017 11:17:58 +0800 Subject: [PATCH 101/318] Little schema changes --- src/data/schema/integration.js | 16 +++++++++----- src/data/schema/segment.js | 2 +- src/db/models/Fields.js | 15 ++++++++++--- src/db/models/Integrations.js | 40 +++++++++++++++++++++++++++------- 4 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/data/schema/integration.js b/src/data/schema/integration.js index 0287df973..9a8812f6a 100644 --- a/src/data/schema/integration.js +++ b/src/data/schema/integration.js @@ -72,22 +72,26 @@ export const mutations = ` name: String!, brandId: String!): Integration - integrationsSaveMessengerAppearanceData(_id: String!, uiOptions: MessengerUiOptions): Boolean + integrationsSaveMessengerAppearanceData( + _id: String!, + uiOptions: MessengerUiOptions): Integration - integrationsSaveMessengerConfigs(_id: String!, messengerData: IntegrationMessengerData): Boolean + integrationsSaveMessengerConfigs( + _id: String!, + messengerData: IntegrationMessengerData): Integration integrationsCreateFormIntegration( name: String!, brandId: String!, - formId: String, + formId: String!, formData: IntegrationFormData!): Integration integrationsEditFormIntegration( _id: String! name: String!, brandId: String!, - formId: String, - formData: IntegrationFormData!): Boolean + formId: String!, + formData: IntegrationFormData!): Integration - integrationsRemove(id: String!): Boolean + integrationsRemove(_id: String!): String `; diff --git a/src/data/schema/segment.js b/src/data/schema/segment.js index 9a49a7fa9..e7989a76f 100644 --- a/src/data/schema/segment.js +++ b/src/data/schema/segment.js @@ -40,5 +40,5 @@ const commonFields = ` export const mutations = ` segmentsAdd(contentType: String!, ${commonFields}): Segment segmentsEdit(_id: String!, ${commonFields}): Segment - segmentsRemove(_id: String!): Segment + segmentsRemove(_id: String!): String `; diff --git a/src/db/models/Fields.js b/src/db/models/Fields.js index cf71b3dd5..8242533d8 100644 --- a/src/db/models/Fields.js +++ b/src/db/models/Fields.js @@ -22,10 +22,19 @@ const FieldSchema = mongoose.Schema({ contentTypeId: String, type: String, - validation: String, + validation: { + type: String, + optional: true, + }, text: String, - description: String, - options: [String], + description: { + type: String, + optional: true, + }, + options: { + type: [String], + optional: true, + }, isRequired: Boolean, order: Number, }); diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index be23827ba..e0029b744 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -51,14 +51,38 @@ const FormDataSchema = mongoose.Schema( type: String, enum: FORM_SUCCESS_ACTIONS.ALL_LIST, }, - fromEmail: mongoose.SchemaTypes.Email, - userEmailTitle: String, - userEmailContent: String, - adminEmails: [mongoose.SchemaTypes.Email], - adminEmailTitle: String, - adminEmailContent: String, - thankContent: String, - redirectUrl: String, + fromEmail: { + type: String, + optional: true, + }, + userEmailTitle: { + type: String, + optional: true, + }, + userEmailContent: { + type: String, + optional: true, + }, + adminEmails: { + type: [String], + optional: true, + }, + adminEmailTitle: { + type: String, + optional: true, + }, + adminEmailContent: { + type: String, + optional: true, + }, + thankContent: { + type: String, + optional: true, + }, + redirectUrl: { + type: String, + optional: true, + }, }, { _id: false }, ); From 715e0f0f8b28f1b25656c2684be99ff6ee43f1b9 Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Sun, 22 Oct 2017 11:46:25 +0800 Subject: [PATCH 102/318] add cronjob to sendEmail --- src/__tests__/conversationDb.test.js | 23 +++++++ src/cronJobs.js | 89 ++++++++++++++++++++++++++++ src/db/models/Conversations.js | 55 +++++++++++++++++ src/index.js | 1 + 4 files changed, 168 insertions(+) create mode 100644 src/cronJobs.js diff --git a/src/__tests__/conversationDb.test.js b/src/__tests__/conversationDb.test.js index 7f8a14dc9..728c4f322 100644 --- a/src/__tests__/conversationDb.test.js +++ b/src/__tests__/conversationDb.test.js @@ -240,4 +240,27 @@ describe('Conversation db', () => { // Conversation message count subtracted expect(beforeConversation.messageCount).toBe(afterConversation.messageCount + 1); }); + + test('Conversation message', async () => { + expect(await ConversationMessages.getNonAsnweredMessage(_conversation._id).count()).toBe(1); + // expect(question) + + await ConversationMessages.update( + { conversationId: _conversation._id }, + { $set: { isCustomerRead: false, internal: false } }, + ); + + expect(await ConversationMessages.getAdminMessages(_conversation._id).count()).toBe(1); + + const msarm = await ConversationMessages.markSentAsReadMessages(_conversation._id); + console.log(msarm); + + const messagesMarkAsRead = await ConversationMessages.find({ _id: _conversation._id }); + + for (let message in messagesMarkAsRead) { + expect(message.isCustomerRead).toBeTruthy(); + } + + expect(await Conversations.newOrOpenConversation().count()).toBe(1); + }); }); diff --git a/src/cronJobs.js b/src/cronJobs.js new file mode 100644 index 000000000..1c76730fa --- /dev/null +++ b/src/cronJobs.js @@ -0,0 +1,89 @@ +import schedule from 'node-schedule'; +import { _ } from 'underscore'; +import moment from 'moment'; +import { sendEmail } from './data/utils'; +import { Conversations, Brands, Customers, Users, Messages } from './db/models'; + +export const sendMessageEmail = async () => { + // new or open conversations + const conversations = await Conversations.newOrOpenConversation(); + + for (let conversation of conversations) { + const customer = await Customers.findOne({ _id: conversation.customerId }); + const brand = await Brands.findOne({ _id: conversation.brandId }); + + if (!customer || !customer.email) { + return; + } + if (!brand) { + return; + } + + // user's last non answered question + const question = (await Messages.getNonAsnweredMessage(conversation._id)) || {}; + + question.createdAt = moment(question.createdAt).format('DD MMM YY, HH:mm'); + + // generate admin unread answers + const answers = []; + + const adminMessages = await Messages.getAdminMessages(conversation._id); + + for (let message of adminMessages) { + const answer = message; + + // add user object to answer + answer.user = await Users.findOne({ _id: message.userId }); + answer.createdAt = moment(answer.createdAt).format('DD MMM YY, HH:mm'); + answers.push(answer); + } + + if (answers.length < 1) { + return; + } + + // template data + const data = { customer, question, answers, brand }; + + // add user's signature + const user = await Users.findOne({ _id: answers[0].userId }); + + if (user && user.emailSignatures) { + const signature = await _.find(user.emailSignatures, s => brand._id === s.brandId); + + if (signature) { + data.signature = signature.signature; + } + } + + // send email + sendEmail({ + to: customer.email, + subject: `Reply from "${brand.name}"`, + template: { + name: 'conversationCron', + isCustom: true, + data, + }, + }); + + // mark sent messages as read + Messages.markSentAsReadMessages(conversation._id); + } +}; + +/** +* * * * * * * +* ┬ ┬ ┬ ┬ ┬ ┬ +* │ │ │ │ │ | +* │ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) +* │ │ │ │ └───── month (1 - 12) +* │ │ │ └────────── day of month (1 - 31) +* │ │ └─────────────── hour (0 - 23) +* │ └──────────────────── minute (0 - 59) +* └───────────────────────── second (0 - 59, OPTIONAL) +*/ +// every 10 minutes +schedule.scheduleJob('*/10 * * * * *', function() { + sendMessageEmail(); +}); diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index 36cbcb26f..87ea23f4d 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -287,6 +287,16 @@ class Conversation { return this.findOne({ _id }); } + + /** + * Get new or open conversation + * @return {Promise} conversations + */ + static newOrOpenConversation() { + return this.find({ + status: { $in: [CONVERSATION_STATUSES.NEW, CONVERSATION_STATUSES.OPEN] }, + }); + } } ConversationSchema.loadClass(Conversation); @@ -398,6 +408,51 @@ class Message { return result; } + + /** + * user's last non answered question + * @param {String} conversationId + * @return {Promise} message object + */ + static getNonAsnweredMessage(conversationId) { + return this.findOne({ + conversationId: conversationId, + customerId: { $exists: true }, + }).sort({ createdAt: -1 }); + } + + /** + * get admin messages + * @param {String} conversationId + * @return {Promise} messages + */ + static getAdminMessages(conversationId) { + return this.find({ + conversationId: conversationId, + userId: { $exists: true }, + isCustomerRead: false, + + // exclude internal notes + internal: false, + }).sort({ createdAt: 1 }); + } + + /** + * mark sent messages as read + * @param {String} conversationId + * @return {Promise} updated info message + */ + static markSentAsReadMessages(conversationId) { + return this.update( + { + conversationId: conversationId, + userId: { $exists: true }, + isCustomerRead: { $exists: false }, + }, + { $set: { isCustomerRead: true } }, + { multi: true }, + ); + } } MessageSchema.loadClass(Message); diff --git a/src/index.js b/src/index.js index 5ec1d901b..508f68bbf 100755 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,7 @@ import { Strategy as AnonymousStrategy } from 'passport-anonymous'; import { Customers, Users } from './db/models'; import { connect } from './db/connection'; import schema from './data'; +import './cronJobs'; // load environment variables dotenv.config(); From f2d6603ed63a707bd172c23ca17ed69edd96e79d Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 22 Oct 2017 14:15:06 +0800 Subject: [PATCH 103/318] Merge form field with common fields module --- src/__tests__/formDb.test.js | 193 +++----------------------- src/__tests__/formMutations.test.js | 105 +------------- src/data/resolvers/form.js | 8 +- src/data/resolvers/mutations/forms.js | 87 +----------- src/data/schema/form.js | 50 +------ src/db/factories.js | 15 +- src/db/models/Forms.js | 117 ++-------------- src/db/models/index.js | 3 +- 8 files changed, 45 insertions(+), 533 deletions(-) diff --git a/src/__tests__/formDb.test.js b/src/__tests__/formDb.test.js index 6ed119235..0474fdc04 100644 --- a/src/__tests__/formDb.test.js +++ b/src/__tests__/formDb.test.js @@ -2,8 +2,8 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { userFactory, formFactory, formFieldFactory, integrationFactory } from '../db/factories'; -import { Forms, Users, FormFields, Integrations } from '../db/models'; +import { userFactory, formFactory, fieldFactory, integrationFactory } from '../db/factories'; +import { Forms, Users, Fields, Integrations } from '../db/models'; import toBeType from 'jest-tobetype'; expect.extend(toBeType); @@ -124,14 +124,15 @@ describe('test exception in remove form method', async () => { afterEach(async () => { await Users.remove({}); await Forms.remove({}); - await FormFields.remove({}); + await Fields.remove({}); await Integrations.remove({}); }); test('check if errors are being thrown as intended', async () => { expect.assertions(2); - await formFieldFactory(_form._id, { + await fieldFactory({ + contentTypeId: _form._id, type: 'input', validation: 'number', text: 'form field text', @@ -144,7 +145,7 @@ describe('test exception in remove form method', async () => { expect(e.message).toEqual('You cannot delete this form. This form has some fields.'); } - await FormFields.remove({}); + await Fields.remove({}); await integrationFactory({ formId: _form._id, @@ -162,168 +163,6 @@ describe('test exception in remove form method', async () => { }); }); -describe('add form field', async () => { - let _user; - let _form; - - beforeEach(async () => { - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - }); - - afterEach(async () => { - await Users.remove({}); - await FormFields.remove({}); - await Forms.remove({}); - }); - - test('check whether form fields are being created successfully', async () => { - const doc = { - type: 'input', - validation: 'number', - text: 'How old are you?', - description: 'Form field description', - options: ['This', 'should', 'not', 'be', 'here', 'tho'], - isRequired: false, - }; - - const newFormField = await FormFields.createFormField(_form._id, doc); - - expect(newFormField.formId).toEqual(_form._id); - expect(newFormField.order).toEqual(0); - expect(newFormField.type).toEqual(doc.type); - expect(newFormField.validation).toEqual(doc.validation); - expect(newFormField.text).toEqual(doc.text); - expect(newFormField.description).toEqual(doc.description); - expect(newFormField.options).toEqual(expect.arrayContaining(doc.options)); - expect(newFormField.isRequired).toEqual(doc.isRequired); - }); -}); - -describe('update form field test', async () => { - let _user; - let _form; - let _form_field; - - beforeEach(async () => { - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - _form_field = await formFieldFactory(_form._id, {}); - }); - - afterEach(async () => { - await Users.remove({}); - await FormFields.remove({}); - await Forms.remove({}); - }); - - test('check whether form fields are being updated successfully', async () => { - const doc = { - type: 'textarea', - validation: 'date', - text: 'How old are you? 1', - description: 'Form field description 1', - options: ['This', 'should', 'not', 'be', 'here', 'tho', '1'], - isRequired: true, - }; - - const updatedFormField = await FormFields.updateFormField(_form_field._id, doc); - - expect(updatedFormField.formId).toEqual(_form._id); - expect(updatedFormField.type).toEqual(doc.type); - expect(updatedFormField.validation).toEqual(doc.validation); - expect(updatedFormField.text).toEqual(doc.text); - expect(updatedFormField.description).toEqual(doc.description); - expect(updatedFormField.isRequired).toBe(doc.isRequired); - - for (let item of doc.options) { - expect(updatedFormField.options).toContain(item); - } - - expect(updatedFormField.options.length).toEqual(7); - }); -}); - -describe('remove form field', async () => { - let _user; - let _form; - let _form_field; - - beforeEach(async () => { - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - _form_field = await formFieldFactory(_form._id, {}); - }); - - afterEach(async () => { - await Users.remove({}); - await FormFields.remove({}); - await Forms.remove({}); - }); - - test('check whether form fields are being removed successfully', async () => { - await FormFields.removeFormField(_form_field._id); - - expect(await FormFields.find({}).count()).toEqual(0); - }); -}); - -describe('test of update order of form fields', async () => { - let _user; - let _form; - let _formField; - let _formField2; - let _formField3; - - /** - * Testing with an _user object and a _form object with 3 fields in it - * to test the setting the new order - */ - beforeEach(async () => { - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - _formField = await formFieldFactory(_form._id, {}); - _formField2 = await formFieldFactory(_form._id, {}); - _formField3 = await formFieldFactory(_form._id, {}); - }); - - /** - * Deleting the data that was used in test - */ - afterEach(async () => { - await Users.remove({}); - await FormFields.remove({}); - await Forms.remove({}); - }); - - test('check whether order values on form fields are being updated successfully', async () => { - expect(_formField.order).toBe(0); - expect(_formField2.order).toBe(1); - expect(_formField3.order).toBe(2); - - const orderDictArray = [ - { _id: _formField3._id, order: 10 }, - { _id: _formField2._id, order: 9 }, - { _id: _formField._id, order: 8 }, - ]; - - await Forms.updateFormFieldsOrder(orderDictArray); - const ff1 = await FormFields.findOne({ _id: _formField3._id }); - - expect(ff1.order).toBe(10); - expect(ff1.text).toBe(_formField3.text); - - const ff2 = await FormFields.findOne({ _id: _formField2._id }); - - expect(ff2.order).toBe(9); - expect(ff2.text).toBe(_formField2.text); - - const ff3 = await FormFields.findOne({ _id: _formField._id }); - expect(ff3.order).toBe(8); - expect(ff3.text).toBe(_formField.text); - }); -}); - describe('form duplication', () => { let _user; let _form; @@ -331,14 +170,14 @@ describe('form duplication', () => { beforeEach(async () => { _user = await userFactory({}); _form = await formFactory({ createdUserId: _user._id }); - await formFieldFactory(_form._id, {}); - await formFieldFactory(_form._id, {}); - await formFieldFactory(_form._id, {}); + await fieldFactory({ contentTypeId: _form._id }); + await fieldFactory({ contentTypeId: _form._id }); + await fieldFactory({ contentTypeId: _form._id }); }); afterEach(async () => { await Users.remove({}); - await FormFields.remove({}); + await Fields.remove({}); await Forms.remove({}); }); @@ -351,10 +190,14 @@ describe('form duplication', () => { expect(duplicatedForm.code.length).toEqual(6); expect(duplicatedForm.createdUserId).toBe(_form.createdUserId); - const formFieldsCount = await FormFields.find({}).count(); - const duplicateFormFieldsCount = await FormFields.find({ formId: duplicatedForm._id }).count(); + const fieldsCount = await Fields.find({}).count(); + + const duplicatedFieldsCount = await Fields.find({ + contentType: 'form', + contentTypeId: duplicatedForm._id, + }).count(); - expect(formFieldsCount).toEqual(6); - expect(duplicateFormFieldsCount).toEqual(3); + expect(fieldsCount).toEqual(6); + expect(duplicatedFieldsCount).toEqual(3); }); }); diff --git a/src/__tests__/formMutations.test.js b/src/__tests__/formMutations.test.js index c4a8e61a5..f3fb42563 100644 --- a/src/__tests__/formMutations.test.js +++ b/src/__tests__/formMutations.test.js @@ -4,14 +4,13 @@ import { connect, disconnect } from '../db/connection'; import formMutations from '../data/resolvers/mutations/forms'; import { userFactory } from '../db/factories'; -import { Forms, Users, FormFields } from '../db/models'; +import { Forms, Users } from '../db/models'; beforeAll(() => connect()); afterAll(() => disconnect()); describe('form and formField mutations', () => { const _formId = 'formId'; - const _formFieldId = 'formFieldId'; let _user; beforeEach(async () => { @@ -23,29 +22,16 @@ describe('form and formField mutations', () => { }); test('test if `logging required` error is working as intended', () => { - expect.assertions(8); + expect.assertions(4); // Login required ================== - expect(() => formMutations.formsCreate(null, {}, {})).toThrowError('Login required'); - + expect(() => formMutations.formsAdd(null, {}, {})).toThrowError('Login required'); expect(() => formMutations.formsEdit(null, {}, {})).toThrowError('Login required'); - expect(() => formMutations.formsRemove(null, {}, {})).toThrowError('Login required'); - - expect(() => formMutations.formsAddFormField(null, {}, {})).toThrowError('Login required'); - - expect(() => formMutations.formsEditFormField(null, {}, {})).toThrowError('Login required'); - - expect(() => formMutations.formsRemoveFormField(null, {}, {})).toThrowError('Login required'); - - expect(() => formMutations.formsUpdateFormFieldsOrder(null, {}, {})).toThrowError( - 'Login required', - ); - expect(() => formMutations.formsDuplicate(null, {}, {})).toThrowError('Login required'); }); - test(`test mutations.formsCreate`, async () => { + test(`test mutations.formsAdd`, async () => { Forms.createForm = jest.fn(); const doc = { @@ -53,13 +39,13 @@ describe('form and formField mutations', () => { description: 'Test form description', }; - await formMutations.formsCreate(null, doc, { user: _user }); + await formMutations.formsAdd(null, doc, { user: _user }); expect(Forms.createForm).toBeCalledWith(doc, _user); expect(Forms.createForm.mock.calls.length).toBe(1); }); - test('test mutations.formUpdate', async () => { + test('test mutations.formsUpdate', async () => { const doc = { _id: _formId, title: 'Test form 2', @@ -77,89 +63,12 @@ describe('form and formField mutations', () => { expect(Forms.updateForm.mock.calls.length).toBe(1); }); - test('test mutations.formsAddFormField', async () => { - const doc = { - formId: _formId, - type: 'input', - validation: 'number', - text: 'How old are you?', - description: 'Form field description', - options: ['This', 'should', 'not', 'be', 'here', 'tho'], - isRequired: false, - }; - - FormFields.createFormField = jest.fn(); - - await formMutations.formsAddFormField(null, doc, { user: _user }); - - delete doc.formId; - - expect(FormFields.createFormField).toBeCalledWith(_formId, doc); - expect(FormFields.createFormField.mock.calls.length).toBe(1); - }); - - test('test mutations.formsEditFormField', async () => { - const doc = { - _id: _formFieldId, - type: 'mutation input 1', - validation: 'mutation number 1', - text: 'mutation - How old are you? 1', - description: 'mutation - Form field description 1', - options: ['This', 'should', 'not', 'be', 'here', 'tho', '1'], - isRequired: true, - }; - - FormFields.updateFormField = jest.fn(); - - await formMutations.formsEditFormField(null, doc, { user: _user }); - - delete doc._id; - - expect(FormFields.updateFormField).toBeCalledWith(_formFieldId, doc); - expect(FormFields.updateFormField.mock.calls.length).toBe(1); - }); - - test('test mutations.formsRemoveFormField', async () => { - FormFields.removeFormField = jest.fn(); - - await formMutations.formsRemoveFormField(null, { _id: _formFieldId }, { user: _user }); - - expect(FormFields.removeFormField).toBeCalledWith(_formFieldId); - expect(FormFields.removeFormField.mock.calls.length).toBe(1); - - // test mutations.formsRemove =========== + test('test mutations.formsRemove', async () => { Forms.removeForm = jest.fn(); await formMutations.formsRemove(null, { _id: _formId }, { user: _user }); expect(Forms.removeForm).toBeCalledWith(_formId); - expect(Forms.removeForm.mock.calls.length).toBe(1); - }); - - test('test mutations.formsUpdateFormFieldsOrder', async () => { - const doc = { - orderDics: [ - { - _id: 'test form field id', - order: 10, - }, - { - _id: 'test form field id 2', - order: 11, - }, - { - _id: 'test form field id 3', - order: 12, - }, - ], - }; - - Forms.updateFormFieldsOrder = jest.fn(); - - await formMutations.formsUpdateFormFieldsOrder(null, doc, { user: _user }); - - expect(Forms.updateFormFieldsOrder).toBeCalledWith(doc.orderDics); - expect(Forms.updateFormFieldsOrder.mock.calls.length).toBe(1); }); test('test mutations.formsDuplicate', async () => { diff --git a/src/data/resolvers/form.js b/src/data/resolvers/form.js index 3b86d72a7..ff8b4c563 100644 --- a/src/data/resolvers/form.js +++ b/src/data/resolvers/form.js @@ -1,7 +1 @@ -import { FormFields } from '../../db/models'; - -export default { - fields(form) { - return FormFields.find({ formId: form._id }).sort({ order: 1 }); - }, -}; +export default {}; diff --git a/src/data/resolvers/mutations/forms.js b/src/data/resolvers/mutations/forms.js index 639b9e595..4f3c41034 100644 --- a/src/data/resolvers/mutations/forms.js +++ b/src/data/resolvers/mutations/forms.js @@ -1,4 +1,4 @@ -import { Forms, FormFields } from '../../../db/models'; +import { Forms } from '../../../db/models'; export default { /** @@ -11,7 +11,7 @@ export default { * @return {Promise} return Promise resolving Form document * @throws {Error} throws Error('Login required') if user is not logged in */ - formsCreate(root, doc, { user }) { + formsAdd(root, doc, { user }) { if (!user) { throw new Error('Login required'); } @@ -57,89 +57,6 @@ export default { return Forms.removeForm(_id); }, - /** - * Adds a form field to the form - * @param {Object} root - * @param {Object} object2 - Form object - * @param {string} object2.formId - Form id - * @param {string} object2.type - Form field type - * @param {string} object2.validation - Form field data validation type - * @param {string} object2.text - Form field text - * @param {string} object2.description - Form field description - * @param {Array} object2.options - Form field options - * @param {Boolean} object2.isRequired - Shows whether the field is required or not - * @param {Object} object3 - Middleware data - * @param {Object} object3.user - The user making this action - * @return {Promise} return Promise resolving new FormField document - * @throws {Error} throws Error('Login required') if user is not logged in - */ - formsAddFormField(root, { formId, ...formFieldDoc }, { user }) { - if (!user) { - throw new Error('Login required'); - } - - return FormFields.createFormField(formId, formFieldDoc); - }, - - /** - * @param {Object} root - * @param {string} object2 - Form field object - * @param {string} object2._id - Form field id - * @param {string} object2.type - Form field type - * @param {string} object2.validation - Form field data validation type - * @param {string} object2.text - Form field text - * @param {string} object2.description - Form field description - * @param {Array} object2.options - Form field options for select type - * @param {Boolean} object2.isRequired - * @param {Object} object3 - Middleware data - * @param {Object} object3.user - The user making this action - * @return {Promise} return Promise resolving updated FormField - * @throws {Error} throws Error('Login required') if user is not logged in - */ - formsEditFormField(root, { _id, ...formFieldDoc }, { user }) { - if (!user) { - throw new Error('Login required'); - } - - return FormFields.updateFormField(_id, formFieldDoc); - }, - - /** - * Remove a form field - * @param {Object} root - * @param {Object} object2 - Graphql input data - * @param {string} object2._id - Form field id - * @param {Object} object3 - Middleware data - * @param {Object} object3.user - The user making this action - * @return {Promise} - * @throws {Error} throws Error('Login required') if user is not logged in - */ - formsRemoveFormField(root, { _id }, { user }) { - if (!user) { - throw new Error('Login required'); - } - - return FormFields.removeFormField(_id); - }, - - /** - * Rearranges order based on given value - * @param {Object} root - * @param {Object} object2 - Graphql input data - * @param {Object} object2.orderDics - Dictionary containing order values for form fields - * @param {Object} object3 - The middleware data - * @param {Object} object3.user - The user making this action - * @return {Promise} - * @throws {Error} throws Error('Login required') if user is not logged in - */ - formsUpdateFormFieldsOrder(root, { orderDics }, { user }) { - if (!user) { - throw new Error('Login required'); - } - - return Forms.updateFormFieldsOrder(orderDics); - }, - /** * Duplicates the form and its fields * @param {Object} root diff --git a/src/data/schema/form.js b/src/data/schema/form.js index fca8aaec5..593efea37 100644 --- a/src/data/schema/form.js +++ b/src/data/schema/form.js @@ -6,57 +6,13 @@ export const types = ` description: String createdUserId: String createdDate: Date - - fields: [FormField] - } - - type FormField { - _id: String! - formId: String - type: String - validation: String - text: String - description: String - options: [String] - isRequired: Boolean - order: Int - } - - input OrderDicItem { - _id: String! - order: Int! } `; export const mutations = ` - formsCreate(title: String!, description: String): Form - - formsEdit(_id: String!, title: String!, description: String): Boolean - - formsRemove(_id: String!): Boolean - - formsAddFormField( - formId: String!, - type: String!, - validation: String, - text: String, - description: String, - options: [String], - isRequired: Boolean): FormField - - formsEditFormField( - _id: String!, - type: String!, - validation: String, - text: String, - description: String, - options: [String], - isRequired: Boolean): Boolean - - formsRemoveFormField(_id: String!): Boolean - - formsUpdateFormFieldsOrder(orderDics: [OrderDicItem]): Boolean - + formsAdd(title: String!, description: String): Form + formsEdit(_id: String!, title: String!, description: String): Form + formsRemove(_id: String!): String formsDuplicate(_id: String!): Form `; diff --git a/src/db/factories.js b/src/db/factories.js index 724fd6014..701b6420c 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -16,7 +16,6 @@ import { Segments, EngageMessages, Forms, - FormFields, Fields, Companies, NotificationConfigurations, @@ -171,6 +170,8 @@ export const customerFactory = (params = {}) => { export const fieldFactory = (params = {}) => { const field = new Fields({ + contentType: params.contentType || 'form', + contentTypeId: params.contentTypeId || 'DFAFDASFDASFDSFDASFASF', type: params.type || 'input', validation: params.validation || 'number', text: params.text || faker.random.word(), @@ -239,18 +240,6 @@ export const formFactory = async ({ title, code, description, createdUserId }) = ); }; -export const formFieldFactory = (formId, params) => { - return FormFields.createFormField(formId || Random.id(), { - type: params.type || 'input', - name: faker.random.word(), - validation: params.validation || 'number', - text: faker.random.word(), - description: faker.random.word(), - isRequired: params.isRequired || false, - number: faker.random.word(), - }); -}; - export const notificationConfigurationFactory = params => { let { isAllowed } = params; if (isAllowed == null) { diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index f7cbf0bf8..a5e8037a6 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -1,7 +1,7 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -import Integrations from './Integrations'; -import { FORM_FIELDS } from '../../data/constants'; +import { Integrations, Fields } from './'; +import { FIELD_CONTENT_TYPES } from '../../data/constants'; // schema for form document const FormSchema = mongoose.Schema({ @@ -82,7 +82,7 @@ class Form { * @throws {Error} throws Error if this form has fields or if used in an integration */ static async removeForm(_id) { - const fieldCount = await FormFields.find({ formId: _id }).count(); + const fieldCount = await Fields.find({ contentTypeId: _id }).count(); if (fieldCount > 0) { throw new Error('You cannot delete this form. This form has some fields.'); @@ -97,24 +97,10 @@ class Form { return this.remove({ _id }); } - /** - * Update order fields of form fields - * @param {Object[]} orderDics - dictionary containing order values with user ids - * @param {string} orderDics[]._id - _id of FormField - * @param {string} orderDics[].order - order of FormField - * @return {Null} - */ - static async updateFormFieldsOrder(orderDics) { - // update each field's order - for (let orderDic of orderDics) { - await FormFields.updateFormField(orderDic._id, { order: orderDic.order }); - } - } - /** * Duplicates form and form fields of the form * @param {string} _id - form id - * @return {FormField} - returns the duplicated copy of the form + * @return {Field} - returns the duplicated copy of the form */ static async duplicate(_id) { const form = await this.findOne({ _id }); @@ -129,10 +115,12 @@ class Form { ); // duplicate fields =================== - const formFields = await FormFields.find({ formId: _id }); + const fields = await Fields.find({ contentTypeId: _id }); - for (let field of formFields) { - await FormFields.createFormField(newForm._id, { + for (let field of fields) { + await Fields.createField({ + contentType: FIELD_CONTENT_TYPES.FORM, + contentTypeId: newForm._id, type: field.type, validation: field.validation, text: field.text, @@ -148,90 +136,7 @@ class Form { } FormSchema.loadClass(Form); -export const Forms = mongoose.model('forms', FormSchema); - -// schema for form fields -const FormFieldSchema = mongoose.Schema({ - _id: { - type: String, - default: () => Random.id(), - }, - type: { - type: String, - enum: FORM_FIELDS.TYPES.ALL, - }, - validation: { - type: String, - enum: FORM_FIELDS.VALIDATION.ALL, - }, - text: String, - description: { - type: String, - required: false, - }, - options: { - type: [String], - required: false, - }, - isRequired: Boolean, - formId: String, - order: { - type: Number, - required: false, - }, -}); - -class FormField { - /** - * Creates a new form field document - * @param {string} formId - Form id - * @param {Object} doc - FormField document object - * @param {string} doc.type - The type of form field (input, textarea, ...) - * @param {string} doc.validation - The type of data to validate to (nummber, date, ...) - * @param {string} doc.text - FormField text - * @param {string} doc.description - FormField description - * @param {String[]} doc.options - FormField select options (checkbox, radion buttons, ...) - * @param {Boolean} doc.isRequired - checks whether value is filled or not on validation - * @return {Promise} - return Promise resolving created FormField document - */ - static async createFormField(formId, doc) { - const lastField = await FormFields.findOne({}, { order: 1 }, { sort: { order: -1 } }); - - doc.formId = formId; - // If there is no field then start with 0 - doc.order = lastField ? lastField.order + 1 : 0; - - return this.create(doc); - } - - /** - * Update a form field document - * @param {string} _id - id of the form document - * @param {Object} doc - FormField document or object - * @param {string} doc.type - The type of form field (input, textarea, etc...) - * @param {string} doc.validation - The type of data to validate to (nummber, date, ...) - * @param {Number} doc.order - FormField order value - * @param {string} doc.text - FormField text - * @param {string} doc.description - FormField description - * @param {String[]} doc.options - FormField select options (checkbox, radion buttons, ...) - * @param {Boolean} doc.isRequired checks whether value is filled or not on validation - * @return {Promise} return Promise resolving updated FormField document - */ - static async updateFormField(_id, doc) { - await this.update({ _id }, { $set: doc }, { runValidators: true }); - return this.findOne({ _id }); - } - - /** - * Remove form field - * @param {string} _id - FormField id - * @return {Promise} - */ - static removeFormField(_id) { - return this.remove({ _id }); - } -} +const Forms = mongoose.model('forms', FormSchema); -FormFieldSchema.loadClass(FormField); -export const FormFields = mongoose.model('form_fields', FormFieldSchema); +export default Forms; diff --git a/src/db/models/index.js b/src/db/models/index.js index e3abe7d64..92206bfa7 100644 --- a/src/db/models/index.js +++ b/src/db/models/index.js @@ -7,7 +7,7 @@ import Integrations from './Integrations'; import EngageMessages from './Engages'; import Tags from './Tags'; import Fields from './Fields'; -import { Forms, FormFields } from './Forms'; +import Forms from './Forms'; import InternalNotes from './InternalNotes'; import Customers from './Customers'; import Companies from './Companies'; @@ -28,7 +28,6 @@ export { Brands, Integrations, Forms, - FormFields, EngageMessages, Tags, Fields, From a95e349f6d5174601261aa66cf7dd43bee1ec8c3 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 23 Oct 2017 11:29:02 +0800 Subject: [PATCH 104/318] Added node-schedule to package.json --- package.json | 1 + yarn.lock | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 02a5e22c5..1c4af1bc6 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "moment": "^2.18.1", "mongoose": "^4.9.2", "mongoose-type-email": "^1.0.5", + "node-schedule": "^1.2.5", "nodemailer": "^4.1.3", "passport": "^0.4.0", "passport-anonymous": "^1.0.1", diff --git a/yarn.lock b/yarn.lock index d354fbde4..1c270ef03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1138,6 +1138,13 @@ create-error-class@^3.0.0: dependencies: capture-stack-trace "^1.0.0" +cron-parser@^2.4.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.4.3.tgz#cae844c20117fc72c678f63ac83c7884be199e78" + dependencies: + is-nan "^1.2.1" + moment-timezone "^0.5.0" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -1220,7 +1227,7 @@ default-require-extensions@^1.0.0: dependencies: strip-bom "^2.0.0" -define-properties@^1.1.2: +define-properties@^1.1.1, define-properties@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" dependencies: @@ -2232,6 +2239,12 @@ is-my-json-valid@^2.10.0: jsonpointer "^4.0.0" xtend "^4.0.0" +is-nan@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.2.1.tgz#9faf65b6fb6db24b7f5c0628475ea71f988401e2" + dependencies: + define-properties "^1.1.1" + is-npm@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" @@ -2952,6 +2965,10 @@ log-update@^1.0.2: ansi-escapes "^1.0.0" cli-cursor "^1.0.2" +long-timeout@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" @@ -3077,6 +3094,16 @@ minimist@~0.0.1: dependencies: minimist "0.0.8" +moment-timezone@^0.5.0: + version "0.5.13" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.13.tgz#99ce5c7d827262eb0f1f702044177f60745d7b90" + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0": + version "2.19.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167" + moment@^2.18.1: version "2.18.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" @@ -3186,6 +3213,14 @@ node-pre-gyp@^0.6.36: tar "^2.2.1" tar-pack "^3.4.0" +node-schedule@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-1.2.5.tgz#fb30f4e4d1dd1e81c536f9495d5da0e9e2d7de14" + dependencies: + cron-parser "^2.4.0" + long-timeout "0.1.1" + sorted-array-functions "^1.0.0" + nodemailer@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.1.3.tgz#4125a6ef79ecfb68357a65c34e4810f210ae120c" @@ -4039,6 +4074,10 @@ sntp@2.x.x: dependencies: hoek "4.x.x" +sorted-array-functions@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.0.0.tgz#c0b554d9e709affcbe56d34c1b2514197fd38279" + source-map-support@^0.4.15: version "0.4.18" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" From 915d80373ab76c7ed4945694a502739bd7968343 Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Mon, 23 Oct 2017 13:47:40 +0800 Subject: [PATCH 105/318] add cronjob folder & update conversation refactor --- src/__tests__/conversationDb.test.js | 9 +-- .../conversations.js} | 9 ++- src/cronJobs/index.js | 5 ++ src/db/models/Conversations.js | 59 ++++++++++--------- 4 files changed, 47 insertions(+), 35 deletions(-) rename src/{cronJobs.js => cronJobs/conversations.js} (94%) create mode 100644 src/cronJobs/index.js diff --git a/src/__tests__/conversationDb.test.js b/src/__tests__/conversationDb.test.js index 728c4f322..a759ba038 100644 --- a/src/__tests__/conversationDb.test.js +++ b/src/__tests__/conversationDb.test.js @@ -65,6 +65,8 @@ describe('Conversation db', () => { } }); test('Create conversation message', async () => { + expect.assertions(17); + // get messageCount before add message const prevConversationObj = await Conversations.findOne({ _id: _doc.conversationId }); @@ -194,7 +196,7 @@ describe('Conversation db', () => { _id: _conversation.id, }); - expect(conversationObjWithParticipatedUser.participatedUserIds.indexOf(_user.id)).toBe(-1); + expect(conversationObjWithParticipatedUser.participatedUserIds.includes(_user.id)).toBeFalsy(); }); test('Conversation mark as read', async () => { @@ -229,7 +231,7 @@ describe('Conversation db', () => { _id: _conversationMessage.conversationId, }); - await ConversationMessages.removeMessage({ _id: _conversationMessage._id }); + await ConversationMessages.removeMessages({ _id: { $in: [_conversationMessage._id] } }); expect(await ConversationMessages.find({ _id: _conversationMessage._id }).count()).toBe(0); @@ -252,8 +254,7 @@ describe('Conversation db', () => { expect(await ConversationMessages.getAdminMessages(_conversation._id).count()).toBe(1); - const msarm = await ConversationMessages.markSentAsReadMessages(_conversation._id); - console.log(msarm); + await ConversationMessages.markSentAsReadMessages(_conversation._id); const messagesMarkAsRead = await ConversationMessages.find({ _id: _conversation._id }); diff --git a/src/cronJobs.js b/src/cronJobs/conversations.js similarity index 94% rename from src/cronJobs.js rename to src/cronJobs/conversations.js index 1c76730fa..842203bbd 100644 --- a/src/cronJobs.js +++ b/src/cronJobs/conversations.js @@ -1,9 +1,12 @@ import schedule from 'node-schedule'; import { _ } from 'underscore'; import moment from 'moment'; -import { sendEmail } from './data/utils'; -import { Conversations, Brands, Customers, Users, Messages } from './db/models'; +import { sendEmail } from '../data/utils'; +import { Conversations, Brands, Customers, Users, Messages } from '../db/models'; +/** +* Send conversation messages to customer +*/ export const sendMessageEmail = async () => { // new or open conversations const conversations = await Conversations.newOrOpenConversation(); @@ -84,6 +87,6 @@ export const sendMessageEmail = async () => { * └───────────────────────── second (0 - 59, OPTIONAL) */ // every 10 minutes -schedule.scheduleJob('*/10 * * * * *', function() { +schedule.scheduleJob('*/10 * * * *', function() { sendMessageEmail(); }); diff --git a/src/cronJobs/index.js b/src/cronJobs/index.js new file mode 100644 index 000000000..e38728988 --- /dev/null +++ b/src/cronJobs/index.js @@ -0,0 +1,5 @@ +import { sendMessageEmail } from './conversations'; + +export default { + ...sendMessageEmail, +}; diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index 87ea23f4d..407d14a17 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -234,6 +234,7 @@ class Conversation { * Add participated user to conversation * @param {list} _ids * @param {String} userId + * @param {Boolean} toggle - add only if true * @return {Promise} Updated conversation list */ static async toggleParticipatedUsers(_ids, userId) { @@ -297,6 +298,23 @@ class Conversation { status: { $in: [CONVERSATION_STATUSES.NEW, CONVERSATION_STATUSES.OPEN] }, }); } + + /** + * Add participated user + * @param {String} conversationId + * @param {String} userId + * @return {Promise} updated conversation id + */ + static addParticipatedUsers(conversationId, userId) { + if (conversationId && userId) { + return this.update( + { _id: conversationId }, + { + $addToSet: { participatedUserIds: userId }, + }, + ); + } + } } ConversationSchema.loadClass(Conversation); @@ -325,7 +343,6 @@ class Message { * @return {Promise} Newly created message object */ static async createMessage(doc) { - // const message = Object.assign({ createdAt: new Date() }, doc); const message = await this.create({ ...doc, createdAt: new Date(), @@ -338,25 +355,11 @@ class Message { await Conversations.update({ _id: message.conversationId }, { $set: { messageCount } }); // add created user to participators - if (message.conversationId && message.userId) { - await Conversations.update( - { _id: message.conversationId }, - { - $addToSet: { participatedUserIds: message.userId }, - }, - ); - } + await Conversations.addParticipatedUsers(message.conversationId, message.userId); // add mentioned users to participators for (let userId of message.mentionedUserIds) { - if (message.conversationId && userId) { - await Conversations.update( - { _id: message.conversationId }, - { - $addToSet: { participatedUserIds: userId }, - }, - ); - } + await Conversations.addParticipatedUsers(message.conversationId, userId); } return message; @@ -364,7 +367,7 @@ class Message { /** * Create a conversation - * @param {Object} doc conversation messsage fields + * @param {Object} doc - conversation message fields * @param {Object} user object * @return {Promise} Newly created conversation object */ @@ -390,11 +393,11 @@ class Message { } /** - * Remove a message + * Remove a messages * @param {Object} selector - * @return {Promise} Deleted message object + * @return {Promise} Deleted messages info */ - static async removeMessage(selector) { + static async removeMessages(selector) { const messages = await this.find(selector); const result = await this.remove(selector); @@ -410,10 +413,10 @@ class Message { } /** - * user's last non answered question - * @param {String} conversationId - * @return {Promise} message object - */ + * User's last non answered question + * @param {String} conversationId + * @return {Promise} message object + */ static getNonAsnweredMessage(conversationId) { return this.findOne({ conversationId: conversationId, @@ -422,7 +425,7 @@ class Message { } /** - * get admin messages + * Get admin messages * @param {String} conversationId * @return {Promise} messages */ @@ -438,9 +441,9 @@ class Message { } /** - * mark sent messages as read + * Mark sent messages as read * @param {String} conversationId - * @return {Promise} updated info message + * @return {Promise} updated messages info */ static markSentAsReadMessages(conversationId) { return this.update( From f8691222c9226bac0c40da5a8846e2bca77343e3 Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 24 Oct 2017 14:37:44 +0800 Subject: [PATCH 106/318] Add user authentication --- package.json | 5 +- src/auth.js | 122 +++++++++++++++++++++++++ src/data/resolvers/mutations/index.js | 2 + src/data/resolvers/mutations/users.js | 7 ++ src/data/schema/index.js | 3 +- src/data/schema/user.js | 9 ++ src/db/models/Users.js | 8 +- src/index.js | 26 +----- yarn.lock | 125 ++++++++++++++++++++------ 9 files changed, 246 insertions(+), 61 deletions(-) create mode 100644 src/auth.js create mode 100644 src/data/resolvers/mutations/users.js diff --git a/package.json b/package.json index 02a5e22c5..fe658a0c6 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ ] }, "dependencies": { + "bcrypt": "^1.0.3", "body-parser": "^1.17.1", "cors": "^2.8.1", "dotenv": "^4.0.0", @@ -52,14 +53,12 @@ "graphql-subscriptions": "^0.4.3", "graphql-tools": "^1.0.0", "handlebars": "^4.0.10", + "jsonwebtoken": "^8.1.0", "meteor-random": "^0.0.3", "moment": "^2.18.1", "mongoose": "^4.9.2", "mongoose-type-email": "^1.0.5", "nodemailer": "^4.1.3", - "passport": "^0.4.0", - "passport-anonymous": "^1.0.1", - "passport-http-bearer": "^1.0.1", "strip": "^3.0.0", "subscriptions-transport-ws": "^0.7.3", "underscore": "^1.8.3", diff --git a/src/auth.js b/src/auth.js new file mode 100644 index 000000000..730df9f86 --- /dev/null +++ b/src/auth.js @@ -0,0 +1,122 @@ +import jwt from 'jsonwebtoken'; +import bcrypt from 'bcrypt'; +import { Users } from './db/models'; + +const SECRET = 'dfjklsafjjekjtejifjidfjsfd'; + +/* + * Creates regular and refresh tokens using given user information + * @param {Object} _user - User object + * @param {String} secret - Token secret + * @return [String] - list of tokens + */ +const createTokens = async (_user, secret) => { + const user = { _id: _user._id, email: _user.email, details: _user.details }; + + const createToken = await jwt.sign({ user }, secret, { expiresIn: '20m' }); + + const createRefreshToken = await jwt.sign({ user }, secret, { expiresIn: '7d' }); + + return [createToken, createRefreshToken]; +}; + +/* + * Renews tokens + * @param {String} token + * @param {String} refreshToken + * @return {Object} renewed tokens with user + */ +export const refreshTokens = async (token, refreshToken) => { + let _id = null; + + try { + // validate refresh token + const { user } = jwt.verify(refreshToken, SECRET); + + _id = user._id; + + // if refresh token is expired then force to login + } catch (e) { + return {}; + } + + const user = await Users.findOne({ _id }); + + // recreate tokens + const [newToken, newRefreshToken] = await createTokens(user, SECRET); + + return { + token: newToken, + refreshToken: newRefreshToken, + user, + }; +}; + +/* + * Validates user credentials and generates tokens + * @param {Object} args + * @param {String} args.email - User email + * @param {String} args.password - User password + * @return {Object} - generated tokens + */ +export const login = async ({ email, password }) => { + const user = await Users.findOne({ email }); + + if (!user) { + // user with provided email not found + throw new Error('Invalid login'); + } + + const valid = await bcrypt.compare(password, user.password); + + if (!valid) { + // bad password + throw new Error('Invalid login'); + } + + // create tokens + const [token, refreshToken] = await createTokens(user, SECRET); + + return { + token, + refreshToken, + }; +}; + +/* + * Finds user object by passed tokens + * @param {Object} req - Request object + * @param {Object} res - Response object + * @param {Function} next - Next function + */ +export const userMiddleware = async (req, res, next) => { + const token = req.headers['x-token']; + + if (token) { + try { + // verify user token and retrieve stored user information + const { user } = jwt.verify(token, SECRET); + + // save user in request + req.user = user; + + // if token is invalid or expired + } catch (e) { + const refreshToken = req.headers['x-refresh-token']; + + // create new tokens using refresh token & refresh token + const newTokens = await refreshTokens(token, refreshToken); + + if (newTokens.token && newTokens.refreshToken) { + res.set('Access-Control-Expose-Headers', 'x-token, x-refresh-token'); + res.set('x-token', newTokens.token); + res.set('x-refresh-token', newTokens.refreshToken); + } + + // save user in request + req.user = newTokens.user; + } + } + + next(); +}; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index af1db451e..b1e9b9f46 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -1,3 +1,4 @@ +import users from './users'; import conversations from './conversations'; import tags from './tags'; import engages from './engages'; @@ -15,6 +16,7 @@ import integrations from './integrations'; import notifications from './notifications'; export default { + ...users, ...conversations, ...tags, ...engages, diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js new file mode 100644 index 000000000..bece5b6f0 --- /dev/null +++ b/src/data/resolvers/mutations/users.js @@ -0,0 +1,7 @@ +import { login } from '../../../auth'; + +export default { + login(root, args) { + return login(args); + }, +}; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index 7d2fe9871..07b793116 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -1,4 +1,4 @@ -import { types as UserTypes, queries as UserQueries } from './user'; +import { types as UserTypes, queries as UserQueries, mutations as UserMutations } from './user'; import { types as CompanyTypes, @@ -127,6 +127,7 @@ export const queries = ` export const mutations = ` type Mutation { + ${UserMutations} ${CompanyMutations} ${ConversationMutations} ${EngageMutations} diff --git a/src/data/schema/user.js b/src/data/schema/user.js index 5ac7efe31..51243064e 100644 --- a/src/data/schema/user.js +++ b/src/data/schema/user.js @@ -5,6 +5,11 @@ export const types = ` details: JSON emails: JSON } + + type AuthPayload { + token: String! + refreshToken: String! + } `; export const queries = ` @@ -12,3 +17,7 @@ export const queries = ` userDetail(_id: String): User usersTotalCount: Int `; + +export const mutations = ` + login(email: String!, password: String!): AuthPayload! +`; diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 5bcd99ba9..487e1e153 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -1,11 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -const EmailSchema = mongoose.Schema({ - address: String, - verified: Boolean, -}); - const UserSchema = mongoose.Schema({ _id: { type: String, @@ -13,8 +8,9 @@ const UserSchema = mongoose.Schema({ default: () => Random.id(), }, username: String, + password: String, details: Object, - emails: [EmailSchema], + email: String, }); const Users = mongoose.model('users', UserSchema); diff --git a/src/index.js b/src/index.js index 5ec1d901b..b4361d93a 100755 --- a/src/index.js +++ b/src/index.js @@ -8,12 +8,10 @@ import { createServer } from 'http'; import { execute, subscribe } from 'graphql'; import { graphqlExpress, graphiqlExpress } from 'graphql-server-express'; import { SubscriptionServer } from 'subscriptions-transport-ws'; -import passport from 'passport'; -import { Strategy as BearerStrategy } from 'passport-http-bearer'; -import { Strategy as AnonymousStrategy } from 'passport-anonymous'; -import { Customers, Users } from './db/models'; +import { Customers } from './db/models'; import { connect } from './db/connection'; import schema from './data'; +import { userMiddleware } from './auth'; // load environment variables dotenv.config(); @@ -28,27 +26,9 @@ app.use(bodyParser.json()); app.use(cors()); -passport.use( - new BearerStrategy(function(token, cb) { - Users.findById(token, function(err, user) { - if (err) { - return cb(err); - } - if (!user) { - return cb(null, false); - } - return cb(null, user); - }); - }), -); - -// All queries, mutations and subscriptions must be available -// for unauthenticated requests. -passport.use(new AnonymousStrategy()); - app.use( '/graphql', - passport.authenticate(['bearer', 'anonymous'], { session: false }), + userMiddleware, graphqlExpress(req => ({ schema, context: { user: req.user } })), ); diff --git a/yarn.lock b/yarn.lock index d354fbde4..a5acdc6bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -793,12 +793,23 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" +base64url@2.0.0, base64url@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" dependencies: tweetnacl "^0.14.3" +bcrypt@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-1.0.3.tgz#b02ddc6c0b52ea16b8d3cf375d5a32e780dab548" + dependencies: + nan "2.6.2" + node-pre-gyp "0.6.36" + binary-extensions@^1.0.0: version "1.10.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" @@ -890,6 +901,10 @@ bson@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.4.tgz#93c10d39eaa5b58415cbc4052f3e53e562b0b72c" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + buffer-shims@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" @@ -1304,6 +1319,13 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ecdsa-sig-formatter@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" + dependencies: + base64url "^2.0.0" + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -2714,6 +2736,21 @@ jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" +jsonwebtoken@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz#c6397cd2e5fd583d65c007a83dc7bb78e6982b83" + dependencies: + jws "^3.1.4" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.0.0" + xtend "^4.0.1" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -2723,6 +2760,23 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jwa@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" + dependencies: + base64url "2.0.0" + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.9" + safe-buffer "^5.0.1" + +jws@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" + dependencies: + base64url "^2.0.0" + jwa "^1.1.4" + safe-buffer "^5.0.1" + kareem@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/kareem/-/kareem-1.5.0.tgz#e3e4101d9dcfde299769daf4b4db64d895d17448" @@ -2907,6 +2961,10 @@ lodash.defaults@^3.1.2: lodash.assign "^3.0.0" lodash.restparam "^3.0.0" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -2915,10 +2973,26 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + lodash.isobject@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + lodash.isstring@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" @@ -2931,6 +3005,10 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" @@ -3134,7 +3212,7 @@ mquery@2.3.1: regexp-clone "0.0.1" sliced "0.0.5" -ms@2.0.0: +ms@2.0.0, ms@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -3146,6 +3224,10 @@ mute-stream@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" +nan@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" + nan@^2.3.0: version "2.7.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" @@ -3171,6 +3253,20 @@ node-notifier@^5.0.2: shellwords "^0.1.0" which "^1.2.12" +node-pre-gyp@0.6.36: + version "0.6.36" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" + dependencies: + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.0.2" + rc "^1.1.7" + request "^2.81.0" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^2.2.1" + tar-pack "^3.4.0" + node-pre-gyp@^0.6.36: version "0.6.37" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.37.tgz#3c872b236b2e266e4140578fe1ee88f693323a05" @@ -3425,29 +3521,6 @@ parseurl@~1.3.1, parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" -passport-anonymous@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/passport-anonymous/-/passport-anonymous-1.0.1.tgz#241e37274ec44dfb7f6cad234b41c438386bc117" - dependencies: - passport-strategy "1.x.x" - -passport-http-bearer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/passport-http-bearer/-/passport-http-bearer-1.0.1.tgz#147469ea3669e2a84c6167ef99dbb77e1f0098a8" - dependencies: - passport-strategy "1.x.x" - -passport-strategy@1.x.x: - version "1.0.0" - resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" - -passport@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.0.tgz#c5095691347bd5ad3b5e180238c3914d16f05811" - dependencies: - passport-strategy "1.x.x" - pause "0.0.1" - path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -3498,10 +3571,6 @@ pause-stream@0.0.11: dependencies: through "~2.3" -pause@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" From 594c3ff279f3e91afdb514b563a74de357735ef9 Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 24 Oct 2017 18:24:07 +0800 Subject: [PATCH 107/318] Add users mutations --- src/__tests__/userMutations.test.js | 17 +++++++++++++++++ src/auth.js | 4 ++++ src/data/resolvers/mutations/engageUtils.js | 4 ++-- src/data/resolvers/mutations/users.js | 4 ++-- src/data/resolvers/queries/users.js | 8 ++++++++ src/data/schema/user.js | 1 + 6 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 src/__tests__/userMutations.test.js diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js new file mode 100644 index 000000000..65c1afe2f --- /dev/null +++ b/src/__tests__/userMutations.test.js @@ -0,0 +1,17 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import auth from '../auth'; +import usersMutations from '../data/resolvers/mutations/users'; + +describe('User mutations', () => { + test('Login', async () => { + auth.login = jest.fn(); + + const doc = { email: 'test@erxes.io', password: 'password' }; + + await usersMutations.login({}, doc); + + expect(auth.login).toBeCalledWith(doc); + }); +}); diff --git a/src/auth.js b/src/auth.js index 730df9f86..ed55c45d3 100644 --- a/src/auth.js +++ b/src/auth.js @@ -120,3 +120,7 @@ export const userMiddleware = async (req, res, next) => { next(); }; + +export default { + login, +}; diff --git a/src/data/resolvers/mutations/engageUtils.js b/src/data/resolvers/mutations/engageUtils.js index fe8b3e449..0c3cb77f9 100644 --- a/src/data/resolvers/mutations/engageUtils.js +++ b/src/data/resolvers/mutations/engageUtils.js @@ -72,7 +72,7 @@ const sendViaEmail = async message => { const { templateId, subject, content } = message.email; const user = await Users.findOne({ _id: fromUserId }); - const userEmail = user.emails.pop(); + const userEmail = user.email; const template = await EmailTemplates.findOne({ _id: templateId }); // find matched customers @@ -102,7 +102,7 @@ const sendViaEmail = async message => { const transporter = await createTransporter(); transporter.sendMail( { - from: userEmail.address, + from: userEmail, to: customer.email, subject: replacedSubject, html: replacedContent, diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index bece5b6f0..60469b6aa 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -1,7 +1,7 @@ -import { login } from '../../../auth'; +import auth from '../../../auth'; export default { login(root, args) { - return login(args); + return auth.login(args); }, }; diff --git a/src/data/resolvers/queries/users.js b/src/data/resolvers/queries/users.js index 2d1bf6d70..ddf6367e1 100644 --- a/src/data/resolvers/queries/users.js +++ b/src/data/resolvers/queries/users.js @@ -35,4 +35,12 @@ export default { usersTotalCount() { return Users.find({}).count(); }, + + /** + * Current user + * @return {Promise} total count + */ + currentUser(root, args, { user }) { + return user; + }, }; diff --git a/src/data/schema/user.js b/src/data/schema/user.js index 51243064e..f55465735 100644 --- a/src/data/schema/user.js +++ b/src/data/schema/user.js @@ -16,6 +16,7 @@ export const queries = ` users(limit: Int): [User] userDetail(_id: String): User usersTotalCount: Int + currentUser: User `; export const mutations = ` From 3c80e05b110086bcd9523442a28be98f2e6074f0 Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Tue, 24 Oct 2017 18:31:05 +0800 Subject: [PATCH 108/318] add cronjob test --- src/__tests__/conversationCronJob.test.js | 124 ++++++++++++++++++++ src/__tests__/conversationMutations.test.js | 4 +- src/cronJobs/conversations.js | 24 ++-- src/cronJobs/index.js | 4 +- 4 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/conversationCronJob.test.js diff --git a/src/__tests__/conversationCronJob.test.js b/src/__tests__/conversationCronJob.test.js new file mode 100644 index 000000000..122d8202a --- /dev/null +++ b/src/__tests__/conversationCronJob.test.js @@ -0,0 +1,124 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import moment from 'moment'; +import { connect, disconnect } from '../db/connection'; +import { + Conversations, + ConversationMessages, + Brands, + Users, + Customers, + Integrations, +} from '../db/models'; +import { + conversationFactory, + userFactory, + customerFactory, + brandFactory, + conversationMessageFactory, + integrationFactory, +} from '../db/factories'; +import { sendMessageEmail } from '../cronJobs/conversations'; +import utils from '../data/utils'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('Cronjob conversation send email', () => { + let _conversation; + let _conversationMessage; + let _customer; + let _brand; + let _user; + + beforeEach(async () => { + // Creating test data + + _customer = await customerFactory(); + _brand = await brandFactory(); + _user = await userFactory(); + const _integration = await integrationFactory({ brandId: _brand._id }); + + _conversation = await conversationFactory({ + customerId: _customer._id, + assignedUserId: _user._id, + brandId: _brand._id, + integrationId: _integration._id, + }); + + _conversationMessage = await conversationMessageFactory({ + conversationId: _conversation._id, + userId: _user._id, + }); + }); + + afterEach(async () => { + // Clearing test data + await Conversations.remove({}); + await Users.remove({}); + await Customers.remove({}); + await Brands.remove({}); + await Integrations.remove({}); + await ConversationMessages.remove({}); + }); + + test('Conversations utils', async () => { + const spyEmail = jest.spyOn(utils, 'sendEmail'); + + Conversations.newOrOpenConversation = jest.fn(() => [_conversation]); + ConversationMessages.getNonAsnweredMessage = jest.fn(() => _conversationMessage); + ConversationMessages.getAdminMessages = jest.fn(() => [_conversationMessage]); + ConversationMessages.markSentAsReadMessages = jest.fn(); + + await sendMessageEmail(); + + expect(Conversations.newOrOpenConversation.mock.calls.length).toBe(1); + + expect(ConversationMessages.getNonAsnweredMessage.mock.calls.length).toBe(1); + expect(ConversationMessages.getNonAsnweredMessage).toBeCalledWith(_conversation._id); + + expect(ConversationMessages.getAdminMessages.mock.calls.length).toBe(1); + expect(ConversationMessages.getAdminMessages).toBeCalledWith(_conversation.id); + + expect(ConversationMessages.getAdminMessages.mock.calls.length).toBe(1); + expect(ConversationMessages.getAdminMessages).toBeCalledWith(_conversation.id); + + expect(spyEmail.mock.calls.length).toBe(1); + + const question = _conversationMessage; + question.createdAt = moment(_conversationMessage.createdAt).format('DD MMM YY, HH:mm'); + + const data = { + customer: _customer, + question, + brand: _brand, + }; + const answer = _conversationMessage; + answer.user = _user; + answer.createdAt = moment(_conversationMessage.createdAt).format('DD MMM YY, HH:mm'); + data.answers = [answer]; + + // send email + expect(spyEmail).toBeCalledWith({ + to: _customer.email, + title: `Reply from "${_brand.name}"`, + template: { + name: 'conversationCron', + isCustom: true, + data, + }, + }); + + expect(ConversationMessages.markSentAsReadMessages.mock.calls.length).toBe(1); + expect(ConversationMessages.markSentAsReadMessages).toBeCalledWith(_conversation.id); + }); + + test('Conversations utils without customer', async () => { + _conversation.customerId = null; + await _conversation.save(); + + await sendMessageEmail(); + }); +}); diff --git a/src/__tests__/conversationMutations.test.js b/src/__tests__/conversationMutations.test.js index 6d219438a..c4a4ced4c 100644 --- a/src/__tests__/conversationMutations.test.js +++ b/src/__tests__/conversationMutations.test.js @@ -2,7 +2,7 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { Conversations, ConversationMessages, Users } from '../db/models'; +import { Conversations, ConversationMessages, Users, Customers, Integrations } from '../db/models'; import { conversationFactory, conversationMessageFactory, @@ -47,6 +47,8 @@ describe('Conversation message mutations', () => { await Conversations.remove({}); await ConversationMessages.remove({}); await Users.remove({}); + await Integrations.remove({}); + await Customers.remove({}); }); test('Conversation login required functions', async () => { diff --git a/src/cronJobs/conversations.js b/src/cronJobs/conversations.js index 842203bbd..087556d05 100644 --- a/src/cronJobs/conversations.js +++ b/src/cronJobs/conversations.js @@ -1,8 +1,15 @@ import schedule from 'node-schedule'; import { _ } from 'underscore'; import moment from 'moment'; -import { sendEmail } from '../data/utils'; -import { Conversations, Brands, Customers, Users, Messages } from '../db/models'; +import utils from '../data/utils'; +import { + Conversations, + Brands, + Customers, + Users, + ConversationMessages, + Integrations, +} from '../db/models'; /** * Send conversation messages to customer @@ -13,7 +20,8 @@ export const sendMessageEmail = async () => { for (let conversation of conversations) { const customer = await Customers.findOne({ _id: conversation.customerId }); - const brand = await Brands.findOne({ _id: conversation.brandId }); + const integration = await Integrations.findOne({ _id: conversation.integrationId }); + const brand = await Brands.findOne({ _id: integration.brandId }); if (!customer || !customer.email) { return; @@ -23,14 +31,14 @@ export const sendMessageEmail = async () => { } // user's last non answered question - const question = (await Messages.getNonAsnweredMessage(conversation._id)) || {}; + const question = (await ConversationMessages.getNonAsnweredMessage(conversation._id)) || {}; question.createdAt = moment(question.createdAt).format('DD MMM YY, HH:mm'); // generate admin unread answers const answers = []; - const adminMessages = await Messages.getAdminMessages(conversation._id); + const adminMessages = await ConversationMessages.getAdminMessages(conversation._id); for (let message of adminMessages) { const answer = message; @@ -60,9 +68,9 @@ export const sendMessageEmail = async () => { } // send email - sendEmail({ + utils.sendEmail({ to: customer.email, - subject: `Reply from "${brand.name}"`, + title: `Reply from "${brand.name}"`, template: { name: 'conversationCron', isCustom: true, @@ -71,7 +79,7 @@ export const sendMessageEmail = async () => { }); // mark sent messages as read - Messages.markSentAsReadMessages(conversation._id); + ConversationMessages.markSentAsReadMessages(conversation._id); } }; diff --git a/src/cronJobs/index.js b/src/cronJobs/index.js index e38728988..7c6bbf320 100644 --- a/src/cronJobs/index.js +++ b/src/cronJobs/index.js @@ -1,5 +1,5 @@ -import { sendMessageEmail } from './conversations'; +import conversations from './conversations'; export default { - ...sendMessageEmail, + ...conversations, }; From cbe245ac085978a94dbbf145a417642d8d5438bd Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 24 Oct 2017 19:05:44 +0800 Subject: [PATCH 109/318] Pass conversation cron test --- src/__tests__/conversationCronJob.test.js | 29 +++++++++++++++++++---- src/db/factories.js | 2 +- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/__tests__/conversationCronJob.test.js b/src/__tests__/conversationCronJob.test.js index 122d8202a..ceeb648a8 100644 --- a/src/__tests__/conversationCronJob.test.js +++ b/src/__tests__/conversationCronJob.test.js @@ -88,20 +88,22 @@ describe('Cronjob conversation send email', () => { expect(spyEmail.mock.calls.length).toBe(1); const question = _conversationMessage; - question.createdAt = moment(_conversationMessage.createdAt).format('DD MMM YY, HH:mm'); + question.createdAt = moment(question.createdAt).format('DD MMM YY, HH:mm'); const data = { customer: _customer, question, brand: _brand, }; + const answer = _conversationMessage; + answer.user = _user; answer.createdAt = moment(_conversationMessage.createdAt).format('DD MMM YY, HH:mm'); data.answers = [answer]; - // send email - expect(spyEmail).toBeCalledWith({ + // send email: check called parameters ================ + const expectedArgs = { to: _customer.email, title: `Reply from "${_brand.name}"`, template: { @@ -109,8 +111,27 @@ describe('Cronjob conversation send email', () => { isCustom: true, data, }, - }); + }; + + const calledArgs = spyEmail.mock.calls[0][0]; + + expect(expectedArgs.to).toBe(calledArgs.to); + expect(expectedArgs.title).toBe(calledArgs.title); + expect(expectedArgs.template.name).toBe(calledArgs.template.name); + expect(expectedArgs.template.isCustom).toBe(calledArgs.template.isCustom); + + expect(expectedArgs.template.data.question.toJSON()).toEqual( + calledArgs.template.data.question.toJSON(), + ); + + expect(expectedArgs.template.data.brand.toJSON()).toEqual( + calledArgs.template.data.brand.toJSON(), + ); + expect(expectedArgs.template.data.customer.toJSON()).toEqual( + calledArgs.template.data.customer.toJSON(), + ); + // mark as read: check called parameters =============== expect(ConversationMessages.markSentAsReadMessages.mock.calls.length).toBe(1); expect(ConversationMessages.markSentAsReadMessages).toBeCalledWith(_conversation.id); }); diff --git a/src/db/factories.js b/src/db/factories.js index 387def62e..b9e1770f5 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -81,7 +81,7 @@ export const brandFactory = (params = {}) => { const brand = new Brands({ name: faker.random.word(), code: params.code || faker.random.word(), - userId: () => Random.id(), + userId: Random.id(), description: params.description || faker.random.word(), emailConfig: { type: 'simple', From a5c63192e59fb83f22f24a3067a9f23827d8a0a5 Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Wed, 25 Oct 2017 11:12:34 +0800 Subject: [PATCH 110/318] update conversation cron job test --- src/__tests__/conversationCronJob.test.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/__tests__/conversationCronJob.test.js b/src/__tests__/conversationCronJob.test.js index ceeb648a8..e04689fa0 100644 --- a/src/__tests__/conversationCronJob.test.js +++ b/src/__tests__/conversationCronJob.test.js @@ -32,6 +32,7 @@ describe('Cronjob conversation send email', () => { let _customer; let _brand; let _user; + let _integration; beforeEach(async () => { // Creating test data @@ -39,7 +40,8 @@ describe('Cronjob conversation send email', () => { _customer = await customerFactory(); _brand = await brandFactory(); _user = await userFactory(); - const _integration = await integrationFactory({ brandId: _brand._id }); + + _integration = await integrationFactory({ brandId: _brand._id }); _conversation = await conversationFactory({ customerId: _customer._id, @@ -72,6 +74,11 @@ describe('Cronjob conversation send email', () => { ConversationMessages.getAdminMessages = jest.fn(() => [_conversationMessage]); ConversationMessages.markSentAsReadMessages = jest.fn(); + // create fake emailSignatures =================== + _user.emailSignatures = [{ brandId: _brand.id, signature: 'test' }]; + + Users.findOne = jest.fn(() => _user); + await sendMessageEmail(); expect(Conversations.newOrOpenConversation.mock.calls.length).toBe(1); @@ -142,4 +149,17 @@ describe('Cronjob conversation send email', () => { await sendMessageEmail(); }); + + test('Conversations utils without brand', async () => { + _integration.brandId = null; + await _integration.save(); + + await sendMessageEmail(); + }); + + test('Conversations utils without answer messages', async () => { + ConversationMessages.getAdminMessages = jest.fn(() => []); + + await sendMessageEmail(); + }); }); From 125323b685f25d83b2d7bf2800d2765cc79cc16d Mon Sep 17 00:00:00 2001 From: Munkhbold Date: Wed, 25 Oct 2017 15:28:07 +0800 Subject: [PATCH 111/318] remove facebook & twitter _id field --- src/db/models/Conversations.js | 126 ++++++++++++++++----------------- 1 file changed, 62 insertions(+), 64 deletions(-) diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index 407d14a17..15f3d7276 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -6,77 +6,75 @@ import { CONVERSATION_STATUSES, FACEBOOK_DATA_KINDS } from '../../data/constants import { Users } from '../../db/models'; -const TwitterDirectMessageSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - senderId: { - type: Number, - }, - senderIdStr: { - type: String, +const TwitterDirectMessageSchema = mongoose.Schema( + { + senderId: { + type: Number, + }, + senderIdStr: { + type: String, + }, + recipientId: { + type: Number, + }, + recipientIdStr: { + type: String, + }, }, - recipientId: { - type: Number, - }, - recipientIdStr: { - type: String, - }, -}); + { _id: false }, +); // Twitter schema -const TwitterSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - idStr: { - type: String, - }, - screenName: { - type: String, - }, - isDirectMessage: { - type: Boolean, +const TwitterSchema = mongoose.Schema( + { + id: { + type: Number, + required: false, + }, + idStr: { + type: String, + }, + screenName: { + type: String, + }, + isDirectMessage: { + type: Boolean, + }, + directMessage: { + type: TwitterDirectMessageSchema, + }, }, - directMessage: { - type: TwitterDirectMessageSchema, - }, -}); + { _id: false }, +); // facebook schema -const FacebookSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - kind: { - type: String, - enum: FACEBOOK_DATA_KINDS.ALL_LIST, - }, - senderName: { - type: String, - }, - senderId: { - type: String, - }, - recipientId: { - type: String, - }, - - // when wall post - postId: { - type: String, +const FacebookSchema = mongoose.Schema( + { + kind: { + type: String, + enum: FACEBOOK_DATA_KINDS.ALL_LIST, + }, + senderName: { + type: String, + }, + senderId: { + type: String, + }, + recipientId: { + type: String, + }, + + // when wall post + postId: { + type: String, + }, + + pageId: { + type: String, + }, }, - - pageId: { - type: String, - }, -}); + { _id: false }, +); // Conversation schema const ConversationSchema = mongoose.Schema({ From 4ce1ddffcf45c27879455f34f68abb81e70338c7 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 25 Oct 2017 15:33:07 +0800 Subject: [PATCH 112/318] Remove _config.yml --- _config.yml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 _config.yml diff --git a/_config.yml b/_config.yml deleted file mode 100644 index ddeb671b6..000000000 --- a/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-time-machine \ No newline at end of file From e79a27f77d439851f17210ed739b74700f43845d Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 27 Oct 2017 12:25:45 +0800 Subject: [PATCH 113/318] Implement forgot password functionality --- .env.sample | 13 +++-- src/__tests__/userMutations.test.js | 20 +++++++ src/auth.js | 84 +++++++++++++++++++++++++++ src/data/resolvers/mutations/users.js | 8 +++ src/data/schema/user.js | 2 + src/data/utils.js | 2 +- src/db/models/Users.js | 2 + 7 files changed, 126 insertions(+), 5 deletions(-) diff --git a/.env.sample b/.env.sample index 433fab65b..0f160e675 100644 --- a/.env.sample +++ b/.env.sample @@ -1,4 +1,9 @@ -NODE_ENV=development -PORT=3400 -MONGO_URL=mongodb://localhost:3001/meteor -TEST_MONGO_URL=mongodb://localhost/erxesApiTest +NODE_ENV='development' +PORT=3300 +MONGO_URL=mongodb://localhost/erxes +TEST_MONGO_URL=mongodb://localhost/test +MAIN_APP_DOMAIN=http://localhost:3000 +COMPANY_EMAIL_FROM='noreply@erxes.io' +MAIL_SERVICE='sendgrid' +MAIL_USER='apikey' +MAIL_PASS='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' diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js index 65c1afe2f..3091c12a2 100644 --- a/src/__tests__/userMutations.test.js +++ b/src/__tests__/userMutations.test.js @@ -14,4 +14,24 @@ describe('User mutations', () => { expect(auth.login).toBeCalledWith(doc); }); + + test('Forgot password', async () => { + auth.forgotPassword = jest.fn(); + + const doc = { email: 'test@erxes.io' }; + + await usersMutations.forgotPassword({}, doc); + + expect(auth.forgotPassword).toBeCalledWith(doc); + }); + + test('Reset password', async () => { + auth.resetPassword = jest.fn(); + + const doc = { token: '2424920429402', newPassword: 'newPassword' }; + + await usersMutations.resetPassword({}, doc); + + expect(auth.resetPassword).toBeCalledWith(doc); + }); }); diff --git a/src/auth.js b/src/auth.js index ed55c45d3..8ee6e1478 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,6 +1,8 @@ import jwt from 'jsonwebtoken'; import bcrypt from 'bcrypt'; +import crypto from 'crypto'; import { Users } from './db/models'; +import { sendEmail } from './data/utils'; const SECRET = 'dfjklsafjjekjtejifjidfjsfd'; @@ -83,6 +85,86 @@ export const login = async ({ email, password }) => { }; }; +/* + * Sends reset password link to found user's email + * @param {String} email - Registered user's email + * @return {String} link - Reset password link + */ +export const forgotPassword = async ({ email }) => { + // find user + const user = await Users.findOne({ email }); + + if (!user) { + throw new Error('Invalid email'); + } + + // create the random token + const buffer = await crypto.randomBytes(20); + const token = buffer.toString('hex'); + + // save token & expiration date + await Users.findByIdAndUpdate( + { _id: user._id }, + { + resetPasswordToken: token, + resetPasswordExpires: Date.now() + 86400000, + }, + ); + + // send email ============== + const { COMPANY_EMAIL_FROM, MAIN_APP_DOMAIN } = process.env; + + const link = `${MAIN_APP_DOMAIN}/reset-password?token=${token}`; + + sendEmail({ + toEmails: [email], + fromEmail: COMPANY_EMAIL_FROM, + title: 'Reset password', + template: { + name: 'base', + data: { + content: link, + }, + }, + }); + + return link; +}; + +/* + * Resets user password by given token & password + * @param {String} token - User's temporary token for reset password + * @param {String} newPassword - New password + * @return {Promise} - Update user response + */ +export const resetPassword = async ({ token, newPassword }) => { + // find user by token + const user = await Users.findOne({ + resetPasswordToken: token, + resetPasswordExpires: { + $gt: Date.now(), + }, + }); + + if (!user) { + throw new Error('Password reset token is invalid or has expired.'); + } + + if (!newPassword) { + throw new Error('Password is required.'); + } + + // set new password + return Users.findByIdAndUpdate( + { _id: user._id }, + { + password: bcrypt.hashSync(newPassword, 10), + resetPasswordToken: undefined, + resetPasswordExpires: undefined, + }, + ); +}; + /* * Finds user object by passed tokens * @param {Object} req - Request object @@ -123,4 +205,6 @@ export const userMiddleware = async (req, res, next) => { export default { login, + forgotPassword, + resetPassword, }; diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index 60469b6aa..2e34e4b4d 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -4,4 +4,12 @@ export default { login(root, args) { return auth.login(args); }, + + forgotPassword(root, args) { + return auth.forgotPassword(args); + }, + + resetPassword(root, args) { + return auth.resetPassword(args); + }, }; diff --git a/src/data/schema/user.js b/src/data/schema/user.js index f55465735..2e8c9acad 100644 --- a/src/data/schema/user.js +++ b/src/data/schema/user.js @@ -21,4 +21,6 @@ export const queries = ` export const mutations = ` login(email: String!, password: String!): AuthPayload! + forgotPassword(email: String!): String! + resetPassword(token: String!, newPassword: String!): String `; diff --git a/src/data/utils.js b/src/data/utils.js index cdf10f3d7..afe76b56f 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -69,7 +69,7 @@ export const sendEmail = async ({ toEmails, fromEmail, title, template }) => { return; } - const transporter = createTransporter(); + const transporter = await createTransporter(); const { isCustom, data, name } = template; diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 487e1e153..e3e8d3e16 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -9,6 +9,8 @@ const UserSchema = mongoose.Schema({ }, username: String, password: String, + resetPasswordToken: String, + resetPasswordExpires: Date, details: Object, email: String, }); From b80d8c3d05b1787cd3e1be40ca65372c9c7ddfc7 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 28 Oct 2017 12:34:42 +0800 Subject: [PATCH 114/318] Add engage extra types --- src/data/resolvers/mutations/engages.js | 2 +- src/data/schema/engage.js | 45 ++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/data/resolvers/mutations/engages.js b/src/data/resolvers/mutations/engages.js index 5d1c82bb2..b96fa1aa8 100644 --- a/src/data/resolvers/mutations/engages.js +++ b/src/data/resolvers/mutations/engages.js @@ -54,7 +54,7 @@ export default { /** * Remove message - * @param {String} _id - Engage message id + * @param {String} _id - Engage message id * @return {Promise} */ engageMessageRemove(root, _id, { user }) { diff --git a/src/data/schema/engage.js b/src/data/schema/engage.js index 4b543c645..dc0b46825 100644 --- a/src/data/schema/engage.js +++ b/src/data/schema/engage.js @@ -21,6 +21,28 @@ export const types = ` segment: Segment fromUser: User } + + input EngageMessageMessengerRule { + _id : String!, + kind: String!, + text: String!, + condition: String!, + value: String, + } + + input EngageMessageEmail { + templateId: String, + subject: String!, + content: String!, + } + + input EngageMessageMessenger { + brandId: String!, + kind: String, + sentAs: String, + content: String, + rules: [EngageMessageMessengerRule], + } `; export const queries = ` @@ -30,12 +52,25 @@ export const queries = ` engageMessagesTotalCount: Int `; +const commonParams = ` + title: String!, + kind: String!, + method: String!, + fromUserId: String!, + isDraft: Boolean, + isLive: Boolean, + stopDate: Date, + segmentId: String, + customerIds: [String], + tagIds: [String], + email: EngageMessageEmail, + messenger: EngageMessageMessenger, +`; + export const mutations = ` - engageMessageAdd(title: String!, kind: String!, - segmentId: String!, method: String!, fromUserId: String!): EngageMessage - engageMessageEdit(_id: String!, title: String!, kind: String!, - segmentId: String!, method: String!, fromUserId: String!): EngageMessage - engageMessageRemove(ids: [String!]!): EngageMessage + engageMessageAdd(${commonParams}): EngageMessage + engageMessageEdit(_id: String!, ${commonParams}): EngageMessage + engageMessageRemove(_id: String!): EngageMessage engageMessageSetLive(_id: String!): EngageMessage engageMessageSetPause(_id: String!): EngageMessage engageMessageSetLiveManual(_id: String!): EngageMessage From a258e45213fbb712bf25bb5af04b791be0ae8c24 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 29 Oct 2017 17:49:03 +0800 Subject: [PATCH 115/318] Fix engage list yours filter --- src/data/resolvers/queries/engages.js | 22 +++++++++++----------- src/data/schema/tag.js | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/data/resolvers/queries/engages.js b/src/data/resolvers/queries/engages.js index 1769efc5c..69fbb4b2a 100644 --- a/src/data/resolvers/queries/engages.js +++ b/src/data/resolvers/queries/engages.js @@ -7,7 +7,7 @@ const count = selector => EngageMessages.find(selector).count(); const tagQueryBuilder = tagId => ({ tagIds: tagId }); // status query builder -const statusQueryBuilder = status => { +const statusQueryBuilder = (status, user) => { if (status === 'live') { return { isLive: true }; } @@ -21,7 +21,7 @@ const statusQueryBuilder = status => { } if (status === 'yours') { - return { fromUserId: '' }; + return { fromUserId: user._id }; } return {}; @@ -36,7 +36,7 @@ const countsByKind = async () => ({ }); // count for each status type -const countsByStatus = async ({ kind }) => { +const countsByStatus = async ({ kind, user }) => { const query = {}; if (kind) { @@ -47,12 +47,12 @@ const countsByStatus = async ({ kind }) => { live: await count({ ...query, ...statusQueryBuilder('live') }), draft: await count({ ...query, ...statusQueryBuilder('draft') }), paused: await count({ ...query, ...statusQueryBuilder('paused') }), - yours: await count({ ...query, ...statusQueryBuilder('yours') }), + yours: await count({ ...query, ...statusQueryBuilder('yours', user) }), }; }; // cout for each tag -const countsByTag = async ({ kind, status }) => { +const countsByTag = async ({ kind, status, user }) => { let query = {}; if (kind) { @@ -60,7 +60,7 @@ const countsByTag = async ({ kind, status }) => { } if (status) { - query = { ...query, ...statusQueryBuilder(status) }; + query = { ...query, ...statusQueryBuilder(status, user) }; } const tags = await Tags.find({ type: 'engageMessage' }); @@ -84,17 +84,17 @@ export default { * @param {String} args.status * @return {Object} counts map */ - async engageMessageCounts(root, { name, kind, status }) { + async engageMessageCounts(root, { name, kind, status }, { user }) { if (name === 'kind') { return countsByKind(); } if (name === 'status') { - return countsByStatus({ kind }); + return countsByStatus({ kind, user }); } if (name === 'tag') { - return countsByTag({ kind, status }); + return countsByTag({ kind, status, user }); } }, @@ -107,7 +107,7 @@ export default { * @param {[String]} args.ids * @return {Promise} filtered messages list by given parameters */ - engageMessages(root, { kind, status, tag, ids }) { + engageMessages(root, { kind, status, tag, ids }, { user }) { if (ids) { return EngageMessages.find({ _id: { $in: ids } }); } @@ -121,7 +121,7 @@ export default { // filter by status if (status) { - query = { ...query, ...statusQueryBuilder(status) }; + query = { ...query, ...statusQueryBuilder(status, user) }; } // filter by tag diff --git a/src/data/schema/tag.js b/src/data/schema/tag.js index 23a451247..05919b69e 100644 --- a/src/data/schema/tag.js +++ b/src/data/schema/tag.js @@ -17,5 +17,5 @@ export const mutations = ` tagsAdd(name: String!, type: String!, colorCode: String): Tag tagsEdit(_id: String!, name: String!, type: String!, colorCode: String): Tag tagsRemove(ids: [String!]!): Tag - tagsTag(type: String!, targetIds: [String!]!, tagIds: [String!]!): Tag + tagsTag(type: String!, targetIds: [String!]!, tagIds: [String!]!): String `; From 1c71aa4fc9b68b7dd8da258edbd38b8f130bd1e1 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Mon, 30 Oct 2017 19:02:45 +0800 Subject: [PATCH 116/318] Add knowledgeBaseDb.test.js with knowledge base topic tests --- src/__tests__/knowledgeBaseDb.test.js | 102 ++++++++++++ src/data/constants.js | 12 +- src/data/resolvers/mutations/index.js | 2 + src/data/resolvers/mutations/knowledgeBase.js | 79 +++++++++ src/data/schema/index.js | 7 +- src/data/schema/knowledgeBase.js | 35 ++++ src/db/factories.js | 16 +- src/db/models/KnowledgeBase.js | 157 ++++++++++++++---- 8 files changed, 374 insertions(+), 36 deletions(-) create mode 100644 src/__tests__/knowledgeBaseDb.test.js create mode 100644 src/data/resolvers/mutations/knowledgeBase.js diff --git a/src/__tests__/knowledgeBaseDb.test.js b/src/__tests__/knowledgeBaseDb.test.js new file mode 100644 index 000000000..b465036d6 --- /dev/null +++ b/src/__tests__/knowledgeBaseDb.test.js @@ -0,0 +1,102 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { knowledgeBaseCategoryFactory, brandFactory, userFactory } from '../db/factories'; +import { KnowledgeBaseTopics, KnowledgeBaseCategories, Brands } from '../db/models'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('test knowledge base models', () => { + describe('topics', () => { + let _user; + + beforeAll(async () => { + _user = await userFactory(); + }); + + afterEach(async () => { + await KnowledgeBaseTopics.remove({}); + await Brands.remove({}); + await KnowledgeBaseCategories.remove({}); + }); + + test(`expect Error('userId must be supplied') to be called as intended`, () => { + expect.assertions(1); + + try { + KnowledgeBaseTopics.createDoc({}, null); + } catch (e) { + expect(e.message).toBe('userId must be supplied'); + } + }); + + test('create topic', async () => { + let categoryA = await knowledgeBaseCategoryFactory({}); + let categoryB = await knowledgeBaseCategoryFactory({}); + let brand = await brandFactory({}); + + const doc = { + title: 'Test topic title', + description: 'Test topic description', + categoryIds: [categoryA._id, categoryB._id], + brandId: brand._id, + }; + + const topic = await KnowledgeBaseTopics.createDoc(doc, _user._id); + + expect(topic.title).toBe(doc.title); + expect(topic.description).toBe(doc.description); + expect(topic.categoryIds).toContain(categoryA._id); + expect(topic.brandId).toBe(doc.brandId); + }); + + test('update topic', async () => { + const categoryA = await knowledgeBaseCategoryFactory({}); + const categoryB = await knowledgeBaseCategoryFactory({}); + + const brandA = await brandFactory({}); + const brandB = await brandFactory({}); + + const doc = { + title: 'Test topic title', + description: 'Test topic description', + categoryIds: [categoryA._id, categoryB._id], + brandId: brandA._id, + }; + + const topic = await KnowledgeBaseTopics.createDoc(doc, _user._id); + + topic.title = 'Test topic title 2'; + topic.description = 'Test topic description 2'; + topic.categoryIds = [categoryA._id, categoryB._id]; + topic.brandId = brandB._id; + + const newTopic = await KnowledgeBaseTopics.updateDoc(topic._id, topic.toObject(), _user); + + expect(newTopic._id).toBe(topic._id); + expect(newTopic.title).toBe(topic.title); + expect(newTopic.categoryIds).toContain(categoryA._id); + expect(newTopic.categoryIds).toContain(categoryB._id); + expect(newTopic.brandId).toBe(brandB._id); + }); + + test('remove topic', async () => { + const brand = await brandFactory({}); + const doc = { + title: 'Test topic title', + description: 'Test topic description', + brandId: brand._id, + }; + + const topic = await KnowledgeBaseTopics.createDoc(doc, _user._id); + + expect(await KnowledgeBaseTopics.find().count()).toBe(1); + + await KnowledgeBaseTopics.removeDoc(topic._id); + + expect(await KnowledgeBaseTopics.find().count()).toBe(0); + }); + }); +}); diff --git a/src/data/constants.js b/src/data/constants.js index 66f785af9..8bc837a42 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -13,7 +13,7 @@ export const INTEGRATION_KIND_CHOICES = { FORM: 'form', TWITTER: 'twitter', FACEBOOK: 'facebook', - ALL_LIST: ['messenger', 'form', 'twitter', 'facebook'], + ALL: ['messenger', 'form', 'twitter', 'facebook'], }; export const TAG_TYPES = { @@ -26,7 +26,7 @@ export const TAG_TYPES = { export const FACEBOOK_DATA_KINDS = { FEED: 'feed', MESSENGER: 'messenger', - ALL_LIST: ['feed', 'messenger'], + ALL: ['feed', 'messenger'], }; export const MESSENGER_KINDS = { @@ -67,7 +67,7 @@ export const FORM_SUCCESS_ACTIONS = { EMAIL: 'email', REDIRECT: 'redirect', ONPAGE: 'onPage', - ALL_LIST: ['', 'email', 'redirect', 'onPage'], + ALL: ['', 'email', 'redirect', 'onPage'], }; export const KIND_CHOICES = { @@ -150,3 +150,9 @@ export const SEGMENT_CONTENT_TYPES = { COMPANY: 'company', ALL_LIST: ['customer', 'company'], }; + +export const PUBLISH_STATUSES = { + DRAFT: 'draft', + PUBLISH: 'publish', + ALL: ['draft', 'publish'], +}; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index b1e9b9f46..a161c7baa 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -14,6 +14,7 @@ import channels from './channels'; import forms from './forms'; import integrations from './integrations'; import notifications from './notifications'; +import knowledgeBase from './knowledgeBase'; export default { ...users, @@ -32,4 +33,5 @@ export default { ...forms, ...integrations, ...notifications, + ...knowledgeBase, }; diff --git a/src/data/resolvers/mutations/knowledgeBase.js b/src/data/resolvers/mutations/knowledgeBase.js new file mode 100644 index 000000000..7f67dc3f4 --- /dev/null +++ b/src/data/resolvers/mutations/knowledgeBase.js @@ -0,0 +1,79 @@ +import { + KnowledgeBaseTopics, + KnowledgeBaseCategories, + KnowledgeBaseArticles, +} from '../../../db/models'; + +export default { + knowledgeBaseTopicsAdd(root, doc, { user }) { + if (!user) { + throw new Error('Login required'); + } + + return KnowledgeBaseTopics.createDoc(doc, user._id); + }, + + knowledgeBaseTopicsEdit(root, { _id, ...fields }, { user }) { + if (!user) { + throw new Error('Login required'); + } + + return KnowledgeBaseTopics.updateDoc(_id, fields, user._id); + }, + + knowledgeBaseTopicsRemove(root, { _id }, { user }) { + if (!user) { + throw new Error('Login required'); + } + + return KnowledgeBaseTopics.removeDoc(_id); + }, + + knowledgeBaseCategoriesAdd(root, doc, { user }) { + if (!user) { + throw new Error('Login required'); + } + + return KnowledgeBaseCategories.createDoc(doc, user._id); + }, + + knowledgeBaseCategoriesEdit(root, { _id, ...fields }, { user }) { + if (!user) { + throw new Error('Login required'); + } + + return KnowledgeBaseCategories.updateDoc(_id, fields, user._id); + }, + + knowledgeBaseCategoriesRemove(root, { _id }, { user }) { + if (!user) { + throw new Error('Login required'); + } + + return KnowledgeBaseCategories.removeDoc(_id); + }, + + knowledgeBaseArticlesAdd(root, doc, { user }) { + if (!user) { + throw new Error('Login required'); + } + + return KnowledgeBaseArticles.createDoc(doc, user._id); + }, + + knowledgeBaseArticlesEdit(root, { _id, ...fields }, { user }) { + if (!user) { + throw new Error('Login required'); + } + + return KnowledgeBaseArticles.updateDoc(_id, fields, user._id); + }, + + knowledgeBaseArticlesRemove(root, { _id }, { user }) { + if (!user) { + throw new Error('Login required'); + } + + return KnowledgeBaseArticles.removeDoc(_id); + }, +}; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index 07b793116..53a910dc1 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -64,7 +64,11 @@ import { import { types as InsightTypes, queries as InsightQueries } from './insight'; -import { types as KnowledgeBaseTypes, queries as KnowledgeBaseQueries } from './knowledgeBase'; +import { + types as KnowledgeBaseTypes, + queries as KnowledgeBaseQueries, + mutations as KnowledgeBaseMutations, +} from './knowledgeBase'; import { types as NotificationTypes, @@ -142,6 +146,7 @@ export const mutations = ` ${ChannelMutations} ${FormMutatons} ${IntegrationMutations} + ${KnowledgeBaseMutations} ${NotificationMutations} } `; diff --git a/src/data/schema/knowledgeBase.js b/src/data/schema/knowledgeBase.js index 7e684ffe4..fa30248a0 100644 --- a/src/data/schema/knowledgeBase.js +++ b/src/data/schema/knowledgeBase.js @@ -11,6 +11,13 @@ export const types = ` modifiedDate: Date } + input KnowledgeBaseArticleDoc { + title: String! + summary: String + content: String + status: String + } + type KnowledgeBaseCategory { _id: String title: String @@ -23,6 +30,13 @@ export const types = ` modifiedDate: Date } + input KnowledgeBaseCategoryDoc { + title: String! + description: String + articles: [KnowledgeBaseArticle] + icon: String + } + type KnowledgeBaseTopic { _id: String title: String @@ -34,6 +48,13 @@ export const types = ` modifiedBy: String modifiedDate: Date } + + input KnowledgeBaseTopicDoc { + title: String! + description: String + categoryIds: [String] + brandId: String! + } `; export const queries = ` @@ -49,3 +70,17 @@ export const queries = ` knowledgeBaseArticlesDetail(_id: String!): KnowledgeBaseArticle knowledgeBaseArticlesTotalCount: Int `; + +export const mutations = ` + knowledgeBaseTopicsAdd(topicAdd: KnowledgeBaseTopicDoc): KnowledgeBaseTopic + knowledgeBaseTopicsEdit(_id: String!): Boolean + knowledgeBaseTopicsRemove(_id: String!): Boolean + + knowledgeBaseCategoriesAdd(categoryDoc: KnowledgeBaseCategoryDoc): KnowledgeBaseTopic + knowledgeBaseCategoriesEdit(_id: String!): Boolean + knowledgeBaseCategoriesRemove(_id: String!): Boolean + + knowledgeBaseArticlesAdd(articleDoc: KnowledgeBaseArticleDoc): KnowledgeBaseArticle + knowledgeBaseArticlesEdit(_id: String!): Boolean + knowledgeBaseArticlesRemove(_id: String!): Boolean +`; diff --git a/src/db/factories.js b/src/db/factories.js index b9e1770f5..3ec4cb63d 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -21,6 +21,7 @@ import { NotificationConfigurations, Notifications, Channels, + KnowledgeBaseCategories, } from './models'; export const userFactory = (params = {}) => { @@ -263,7 +264,7 @@ export const notificationFactory = params => { }); }; -export async function channelFactory(params = {}) { +export const channelFactory = async (params = {}) => { const user = await userFactory({}); const obj = Object.assign( @@ -281,4 +282,15 @@ export async function channelFactory(params = {}) { ); return Channels.create(obj); -} +}; + +export const knowledgeBaseCategoryFactory = params => { + const doc = { + title: faker.random.word(), + description: faker.lorem.sentence, + articleIds: [faker.random.word(), faker.random.word()], + icon: faker.random.word(), + }; + + return KnowledgeBaseCategories.createDoc({ ...doc, ...params }, faker.random.word()); +}; diff --git a/src/db/models/KnowledgeBase.js b/src/db/models/KnowledgeBase.js index 9e374a3bc..6aa3ee833 100644 --- a/src/db/models/KnowledgeBase.js +++ b/src/db/models/KnowledgeBase.js @@ -1,68 +1,165 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +import { PUBLISH_STATUSES } from '../../data/constants'; -const KbArticlesSchema = mongoose.Schema({ +const commonFields = { + createdBy: String, + createdDate: { + type: Date, + default: new Date(), + }, + modifiedBy: String, + modifiedDate: Date, +}; + +class KnowledgeBaseCommonDocument { + static createDoc(doc, userId) { + return this.create({ + ...doc, + createdBy: userId, + }); + } + + static async updateDoc(_id, doc, userId) { + await this.update( + { _id }, + { + $set: { + ...doc, + modifiedBy: userId, + modifiedDate: new Date(), + }, + }, + ); + return this.findOne({ _id }); + } + + static removeDoc(_id) { + return this.remove({ _id }); + } +} + +const ArticleSchema = mongoose.Schema({ _id: { type: String, unique: true, default: () => Random.id(), }, - title: String, - summary: String, - content: String, - createdBy: String, - createdDate: Date, - modifiedBy: String, - modifiedDate: Date, - status: String, + title: { + type: String, + required: true, + }, + summary: { + type: String, + required: true, + }, + content: { + type: String, + required: true, + }, + status: { + type: String, + enum: PUBLISH_STATUSES.ALL, + default: PUBLISH_STATUSES.DRAFT, + }, authorDetails: { avatar: String, fullName: String, }, + ...commonFields, }); -const KbCategoriesSchema = mongoose.Schema({ +class Article extends KnowledgeBaseCommonDocument { + static createDoc(doc, userId) { + return super.createDoc(doc, userId); + } + + static updateDoc(_id, doc, userId) { + return super.updateDoc({ _id }, doc, userId); + } +} + +const CategorySchema = mongoose.Schema({ _id: { type: String, unique: true, default: () => Random.id(), }, - title: String, + title: { + type: String, + required: true, + }, description: String, articleIds: { type: [String], required: false, }, - icon: String, - createdBy: String, - createdDate: Date, - modifiedBy: String, - modifiedDate: Date, + icon: { + type: String, + required: true, + }, + ...commonFields, }); -const KbTopicsSchema = mongoose.Schema({ +class Category extends KnowledgeBaseCommonDocument { + static createDoc(doc, userId) { + return super.createDoc(doc, userId); + } + + static updateDoc(_id, doc, userId) { + return super.updateDoc(_id, doc, userId); + } +} + +const TopicSchema = mongoose.Schema({ _id: { type: String, unique: true, default: () => Random.id(), }, - title: String, - brandId: String, + title: { + type: String, + required: true, + }, description: String, + brandId: { + type: String, + required: true, + }, categoryIds: { type: [String], required: false, }, - loadType: String, - createdBy: String, - createdDate: Date, - modifiedBy: String, - modifiedDate: Date, + ...commonFields, }); -export const KnowledgeBaseArticles = mongoose.model('knowledgebase_articles', KbArticlesSchema); -export const KnowledgeBaseCategories = mongoose.model( - 'knowledgebase_categories', - KbCategoriesSchema, -); -export const KnowledgeBaseTopics = mongoose.model('knowledgebase_topics', KbTopicsSchema); +class Topic extends KnowledgeBaseCommonDocument { + static createDoc({ createdBy, createdDate, modifiedBy, modifiedDate, ...docFields }, userId) { + if (!userId) { + throw new Error('userId must be supplied'); + } + + return super.createDoc(docFields, userId); + } + + static updateDoc( + _id, + { createdBy, createdDate, modifiedBy, modifiedDate, ...docFields }, + userId, + ) { + if (!userId) { + throw new Error('userId must be supplied'); + } + + return super.updateDoc(_id, docFields, userId); + } +} + +ArticleSchema.loadClass(Article); +export const KnowledgeBaseArticles = mongoose.model('knowledgebase_articles', ArticleSchema); + +CategorySchema.loadClass(Category); +export const KnowledgeBaseCategories = mongoose.model('knowledgebase_categories', CategorySchema); + +TopicSchema.loadClass(Topic); +export const KnowledgeBaseTopics = mongoose.model('knowledgebase_topics', TopicSchema); From 12fc2b9da6d372d838385eae61ae1837d1760ac6 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Mon, 30 Oct 2017 19:36:46 +0800 Subject: [PATCH 117/318] Add knowledge base category tests --- src/__tests__/knowledgeBaseDb.test.js | 128 ++++++++++++++++++++++++-- src/data/schema/knowledgeBase.js | 2 +- src/db/factories.js | 12 +++ src/db/models/KnowledgeBase.js | 29 +++--- 4 files changed, 148 insertions(+), 23 deletions(-) diff --git a/src/__tests__/knowledgeBaseDb.test.js b/src/__tests__/knowledgeBaseDb.test.js index b465036d6..e681151b9 100644 --- a/src/__tests__/knowledgeBaseDb.test.js +++ b/src/__tests__/knowledgeBaseDb.test.js @@ -2,20 +2,38 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { knowledgeBaseCategoryFactory, brandFactory, userFactory } from '../db/factories'; -import { KnowledgeBaseTopics, KnowledgeBaseCategories, Brands } from '../db/models'; +import toBeType from 'jest-tobetype'; +import { + knowledgeBaseCategoryFactory, + knowledgeBaseArticleFactory, + brandFactory, + userFactory, +} from '../db/factories'; +import { + KnowledgeBaseTopics, + KnowledgeBaseCategories, + KnowledgeBaseArticles, + Brands, + Users, +} from '../db/models'; + +expect.extend(toBeType); beforeAll(() => connect()); afterAll(() => disconnect()); describe('test knowledge base models', () => { - describe('topics', () => { - let _user; + let _user; - beforeAll(async () => { - _user = await userFactory(); - }); + beforeAll(async () => { + _user = await userFactory(); + }); + afterAll(async () => { + await Users.remove({}); + }); + + describe('KnowledgeBaseTopics', () => { afterEach(async () => { await KnowledgeBaseTopics.remove({}); await Brands.remove({}); @@ -32,7 +50,7 @@ describe('test knowledge base models', () => { } }); - test('create topic', async () => { + test('create', async () => { let categoryA = await knowledgeBaseCategoryFactory({}); let categoryB = await knowledgeBaseCategoryFactory({}); let brand = await brandFactory({}); @@ -52,7 +70,7 @@ describe('test knowledge base models', () => { expect(topic.brandId).toBe(doc.brandId); }); - test('update topic', async () => { + test('update', async () => { const categoryA = await knowledgeBaseCategoryFactory({}); const categoryB = await knowledgeBaseCategoryFactory({}); @@ -82,7 +100,7 @@ describe('test knowledge base models', () => { expect(newTopic.brandId).toBe(brandB._id); }); - test('remove topic', async () => { + test('remove', async () => { const brand = await brandFactory({}); const doc = { title: 'Test topic title', @@ -99,4 +117,94 @@ describe('test knowledge base models', () => { expect(await KnowledgeBaseTopics.find().count()).toBe(0); }); }); + + describe('categories', () => { + afterEach(async () => { + await KnowledgeBaseCategories.remove(); + await KnowledgeBaseArticles.remove({}); + }); + + test(`expect Error('userId must be supplied') to be called as intended`, () => { + expect.assertions(1); + + try { + KnowledgeBaseCategories.createDoc({}, null); + } catch (e) { + expect(e.message).toBe('userId must be supplied'); + } + }); + + test('create', async () => { + const article = knowledgeBaseArticleFactory({}); + + const doc = { + title: 'Test category title', + description: 'Test topic description', + articleIds: [article._id], + icon: 'test icon', + }; + + const category = await KnowledgeBaseCategories.createDoc(doc, _user._id); + + expect(category.title).toBe(doc.title); + expect(category.description).toBe(doc.description); + expect(category.articleIds).toContain(article._id); + expect(category.icon).toBe(doc.icon); + // Values related to modification ====== + expect(category.createdBy).toBe(_user._id); + expect(category.createdDate).toBeType('object'); + }); + + test('update', async () => { + const article = await knowledgeBaseArticleFactory({}); + const articleB = await knowledgeBaseArticleFactory({}); + + const doc = { + title: 'Test category title', + description: 'Test category description', + articleIds: [article._id], + icon: 'test icon', + }; + + const category = await KnowledgeBaseCategories.createDoc(doc, _user._id); + + category.title = 'Test category title 2'; + category.description = 'Test category description 2'; + category.articleIds = [article._id, articleB._id]; + category.icon = 'test icon 2'; + + const newCategory = await KnowledgeBaseCategories.updateDoc( + category._id, + category.toObject(), + _user._id, + ); + + expect(newCategory._id).toBe(category._id); + expect(newCategory.title).toBe(category.title); + expect(newCategory.description).toBe(category.description); + expect(newCategory.articleIds).toContain(article._id); + expect(newCategory.articleIds).toContain(articleB._id); + expect(newCategory.icon).toBe(category.icon); + + // Values related to modification ====== + expect(newCategory.modifiedBy).toBe(_user._id); + expect(newCategory.modifiedDate).toBeType('object'); + }); + + test('remove', async () => { + const doc = { + title: 'Test category title', + description: 'Test category description', + icon: 'test icon', + }; + + const category = await KnowledgeBaseCategories.createDoc(doc, _user._id); + + expect(await KnowledgeBaseCategories.find().count()).toBe(1); + + await KnowledgeBaseCategories.removeDoc(category._id); + + expect(await KnowledgeBaseCategories.find().count()).toBe(0); + }); + }); }); diff --git a/src/data/schema/knowledgeBase.js b/src/data/schema/knowledgeBase.js index fa30248a0..691d5a06b 100644 --- a/src/data/schema/knowledgeBase.js +++ b/src/data/schema/knowledgeBase.js @@ -34,7 +34,7 @@ export const types = ` title: String! description: String articles: [KnowledgeBaseArticle] - icon: String + icon: String! } type KnowledgeBaseTopic { diff --git a/src/db/factories.js b/src/db/factories.js index 3ec4cb63d..e5def9952 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -22,6 +22,7 @@ import { Notifications, Channels, KnowledgeBaseCategories, + KnowledgeBaseArticles, } from './models'; export const userFactory = (params = {}) => { @@ -294,3 +295,14 @@ export const knowledgeBaseCategoryFactory = params => { return KnowledgeBaseCategories.createDoc({ ...doc, ...params }, faker.random.word()); }; + +export const knowledgeBaseArticleFactory = params => { + const doc = { + title: faker.random.word(), + summary: faker.lorem.sentence, + content: faker.lorem.sentence, + icon: faker.random.word(), + }; + + return KnowledgeBaseArticles.createDoc({ ...doc, ...params }, faker.random.word()); +}; diff --git a/src/db/models/KnowledgeBase.js b/src/db/models/KnowledgeBase.js index 6aa3ee833..23237d087 100644 --- a/src/db/models/KnowledgeBase.js +++ b/src/db/models/KnowledgeBase.js @@ -14,6 +14,10 @@ const commonFields = { class KnowledgeBaseCommonDocument { static createDoc(doc, userId) { + if (!userId) { + throw new Error('userId must be supplied'); + } + return this.create({ ...doc, createdBy: userId, @@ -21,6 +25,10 @@ class KnowledgeBaseCommonDocument { } static async updateDoc(_id, doc, userId) { + if (!userId) { + throw new Error('userId must be supplied'); + } + await this.update( { _id }, { @@ -61,6 +69,7 @@ const ArticleSchema = mongoose.Schema({ type: String, enum: PUBLISH_STATUSES.ALL, default: PUBLISH_STATUSES.DRAFT, + required: true, }, authorDetails: { avatar: String, @@ -102,12 +111,16 @@ const CategorySchema = mongoose.Schema({ }); class Category extends KnowledgeBaseCommonDocument { - static createDoc(doc, userId) { - return super.createDoc(doc, userId); + static createDoc({ createdBy, createdDate, modifiedBy, modifiedDate, ...docFields }, userId) { + return super.createDoc(docFields, userId); } - static updateDoc(_id, doc, userId) { - return super.updateDoc(_id, doc, userId); + static updateDoc( + _id, + { createdBy, createdDate, modifiedBy, modifiedDate, ...docFields }, + userId, + ) { + return super.updateDoc(_id, docFields, userId); } } @@ -135,10 +148,6 @@ const TopicSchema = mongoose.Schema({ class Topic extends KnowledgeBaseCommonDocument { static createDoc({ createdBy, createdDate, modifiedBy, modifiedDate, ...docFields }, userId) { - if (!userId) { - throw new Error('userId must be supplied'); - } - return super.createDoc(docFields, userId); } @@ -147,10 +156,6 @@ class Topic extends KnowledgeBaseCommonDocument { { createdBy, createdDate, modifiedBy, modifiedDate, ...docFields }, userId, ) { - if (!userId) { - throw new Error('userId must be supplied'); - } - return super.updateDoc(_id, docFields, userId); } } From 92741fec15b4005c1f7ad37484dcc97844f57d16 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Mon, 30 Oct 2017 19:48:34 +0800 Subject: [PATCH 118/318] Add knowledge base article model tests --- src/__tests__/knowledgeBaseDb.test.js | 85 ++++++++++++++++++++++++++- src/db/models/KnowledgeBase.js | 4 -- 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/__tests__/knowledgeBaseDb.test.js b/src/__tests__/knowledgeBaseDb.test.js index e681151b9..80c8541d1 100644 --- a/src/__tests__/knowledgeBaseDb.test.js +++ b/src/__tests__/knowledgeBaseDb.test.js @@ -16,6 +16,7 @@ import { Brands, Users, } from '../db/models'; +import { PUBLISH_STATUSES } from '../data/constants'; expect.extend(toBeType); @@ -118,7 +119,7 @@ describe('test knowledge base models', () => { }); }); - describe('categories', () => { + describe('KnowledgeBaseCategories', () => { afterEach(async () => { await KnowledgeBaseCategories.remove(); await KnowledgeBaseArticles.remove({}); @@ -139,9 +140,9 @@ describe('test knowledge base models', () => { const doc = { title: 'Test category title', - description: 'Test topic description', + description: 'Test category description', articleIds: [article._id], - icon: 'test icon', + icon: 'test category icon', }; const category = await KnowledgeBaseCategories.createDoc(doc, _user._id); @@ -207,4 +208,82 @@ describe('test knowledge base models', () => { expect(await KnowledgeBaseCategories.find().count()).toBe(0); }); }); + + describe('KnowledgeBaseArticles', () => { + afterEach(async () => { + await KnowledgeBaseArticles.remove({}); + }); + + test(`expect Error('userId must be supplied') to be called as intended`, () => { + expect.assertions(1); + + try { + KnowledgeBaseArticles.createDoc({}, null); + } catch (e) { + expect(e.message).toBe('userId must be supplied'); + } + }); + + test('create', async () => { + const doc = { + title: 'Test article title', + summary: 'Test article description', + content: 'Test article content', + status: PUBLISH_STATUSES.DRAFT, + }; + + const article = await KnowledgeBaseArticles.createDoc(doc, _user._id); + + expect(article.title).toBe(doc.title); + expect(article.summary).toBe(doc.summary); + expect(article.content).toBe(doc.content); + expect(article.icon).toBe(doc.icon); + expect(article.status).toBe(PUBLISH_STATUSES.DRAFT); + }); + + test('update', async () => { + const doc = { + title: 'Test article title', + summary: 'Test article description', + content: 'Test article content', + status: PUBLISH_STATUSES.DRAFT, + }; + + const article = await KnowledgeBaseArticles.createDoc(doc, _user._id); + + article.title = 'Test article title 2'; + article.summary = 'Test article description 2'; + article.content = 'Test article content 2'; + article.status = PUBLISH_STATUSES.PUBLISH; + + const updatedArticle = await KnowledgeBaseArticles.updateDoc( + article._id, + article.toObject(), + _user._id, + ); + + expect(updatedArticle.title).toBe(article.title); + expect(updatedArticle.summary).toBe(article.summary); + expect(updatedArticle.content).toBe(article.content); + expect(updatedArticle.icon).toBe(article.icon); + expect(updatedArticle.status).toBe(article.status); + }); + + test('remove', async () => { + const doc = { + title: 'Test article title', + summary: 'Test article description', + content: 'Test article content', + status: PUBLISH_STATUSES.DRAFT, + }; + + const article = await KnowledgeBaseArticles.createDoc(doc, _user._id); + + expect(await KnowledgeBaseArticles.find().count()).toBe(1); + + await KnowledgeBaseArticles.removeDoc(article._id); + + expect(await KnowledgeBaseArticles.find().count()).toBe(0); + }); + }); }); diff --git a/src/db/models/KnowledgeBase.js b/src/db/models/KnowledgeBase.js index 23237d087..de8def71e 100644 --- a/src/db/models/KnowledgeBase.js +++ b/src/db/models/KnowledgeBase.js @@ -71,10 +71,6 @@ const ArticleSchema = mongoose.Schema({ default: PUBLISH_STATUSES.DRAFT, required: true, }, - authorDetails: { - avatar: String, - fullName: String, - }, ...commonFields, }); From 4fdae093c911412e37efe8790631e13789720bbb Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Mon, 30 Oct 2017 20:27:55 +0800 Subject: [PATCH 119/318] Add exception checks on category and article removeDoc methods --- src/__tests__/knowledgeBaseDb.test.js | 65 ++++++++++++++++++++ src/__tests__/knowledgeBaseMutations.test.js | 0 src/db/factories.js | 18 ++++++ src/db/models/KnowledgeBase.js | 16 +++++ 4 files changed, 99 insertions(+) create mode 100644 src/__tests__/knowledgeBaseMutations.test.js diff --git a/src/__tests__/knowledgeBaseDb.test.js b/src/__tests__/knowledgeBaseDb.test.js index 80c8541d1..93d65c9d0 100644 --- a/src/__tests__/knowledgeBaseDb.test.js +++ b/src/__tests__/knowledgeBaseDb.test.js @@ -4,6 +4,7 @@ import { connect, disconnect } from '../db/connection'; import toBeType from 'jest-tobetype'; import { + knowledgeBaseTopicFactory, knowledgeBaseCategoryFactory, knowledgeBaseArticleFactory, brandFactory, @@ -207,6 +208,35 @@ describe('test knowledge base models', () => { expect(await KnowledgeBaseCategories.find().count()).toBe(0); }); + + test(`check if Error('You can not delete this. This category is used in topic.') + is being called as intended`, async () => { + expect.assertions(2); + + const doc = { + title: 'Test category title', + description: 'Test category description', + icon: 'test icon', + }; + + const category = await KnowledgeBaseCategories.createDoc(doc, _user._id); + + const topic = await knowledgeBaseTopicFactory({ + categoryIds: [category._id], + }); + + try { + await KnowledgeBaseCategories.removeDoc(category._id); + } catch (e) { + expect(e.message).toBe('You can not delete this. This category is used in topic.'); + } + + await KnowledgeBaseTopics.removeDoc(topic._id); + + await KnowledgeBaseCategories.removeDoc(category._id); + + expect(await KnowledgeBaseCategories.find().count()).toBe(0); + }); }); describe('KnowledgeBaseArticles', () => { @@ -285,5 +315,40 @@ describe('test knowledge base models', () => { expect(await KnowledgeBaseArticles.find().count()).toBe(0); }); + + test(`check if Error('You can not delete this. This article is used in category.') + is being called as intended`, async () => { + expect.assertions(2); + + const doc = { + title: 'Test article title', + summary: 'Test article description', + content: 'Test article content', + status: PUBLISH_STATUSES.DRAFT, + }; + + const article = await KnowledgeBaseArticles.createDoc(doc, _user._id); + + const categoryDoc = { + title: 'Test category title', + description: 'Test category description', + articleIds: [article._id], + icon: 'test icon', + }; + + const category = await KnowledgeBaseCategories.createDoc(categoryDoc, _user._id); + + try { + await KnowledgeBaseArticles.removeDoc(article._id); + } catch (e) { + expect(e.message).toBe('You can not delete this. This article is used in category.'); + } + + await KnowledgeBaseCategories.removeDoc(category._id); + + await KnowledgeBaseArticles.removeDoc(article._id); + + expect(await KnowledgeBaseArticles.find().count()).toBe(0); + }); }); }); diff --git a/src/__tests__/knowledgeBaseMutations.test.js b/src/__tests__/knowledgeBaseMutations.test.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/db/factories.js b/src/db/factories.js index e5def9952..94364d0b4 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -21,6 +21,7 @@ import { NotificationConfigurations, Notifications, Channels, + KnowledgeBaseTopics, KnowledgeBaseCategories, KnowledgeBaseArticles, } from './models'; @@ -285,6 +286,23 @@ export const channelFactory = async (params = {}) => { return Channels.create(obj); }; +export const knowledgeBaseTopicFactory = params => { + const doc = { + title: faker.random.word(), + description: faker.lorem.sentence, + brandId: faker.random.word(), + catgoryIds: [faker.random.word()], + }; + + return KnowledgeBaseTopics.createDoc( + { + ...doc, + ...params, + }, + faker.random.word(), + ); +}; + export const knowledgeBaseCategoryFactory = params => { const doc = { title: faker.random.word(), diff --git a/src/db/models/KnowledgeBase.js b/src/db/models/KnowledgeBase.js index de8def71e..7acd3d8d4 100644 --- a/src/db/models/KnowledgeBase.js +++ b/src/db/models/KnowledgeBase.js @@ -82,6 +82,14 @@ class Article extends KnowledgeBaseCommonDocument { static updateDoc(_id, doc, userId) { return super.updateDoc({ _id }, doc, userId); } + + static async removeDoc(_id) { + if ((await KnowledgeBaseCategories.find({ articleIds: _id }).count()) > 0) { + throw new Error('You can not delete this. This article is used in category.'); + } + + return this.remove({ _id }); + } } const CategorySchema = mongoose.Schema({ @@ -118,6 +126,14 @@ class Category extends KnowledgeBaseCommonDocument { ) { return super.updateDoc(_id, docFields, userId); } + + static async removeDoc(_id) { + if ((await KnowledgeBaseTopics.find({ categoryIds: _id }).count()) > 0) { + throw new Error('You can not delete this. This category is used in topic.'); + } + + return this.remove({ _id }); + } } const TopicSchema = mongoose.Schema({ From d39408be163bd60731dd6934e926357758ba6574 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Mon, 30 Oct 2017 20:49:42 +0800 Subject: [PATCH 120/318] Add kKowledge Base mutation tests --- src/__tests__/knowledgeBaseMutations.test.js | 203 ++++++++++++++++++ src/data/resolvers/mutations/knowledgeBase.js | 18 +- src/data/schema/knowledgeBase.js | 2 +- 3 files changed, 213 insertions(+), 10 deletions(-) diff --git a/src/__tests__/knowledgeBaseMutations.test.js b/src/__tests__/knowledgeBaseMutations.test.js index e69de29bb..5f6f22200 100644 --- a/src/__tests__/knowledgeBaseMutations.test.js +++ b/src/__tests__/knowledgeBaseMutations.test.js @@ -0,0 +1,203 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import knowledgeBaseMutations from '../data/resolvers/mutations/knowledgeBase'; +import { KnowledgeBaseTopics, KnowledgeBaseCategories, KnowledgeBaseArticles } from '../db/models'; + +describe('mutations', () => { + let _user = { + _id: 'fakeUserId', + }; + + test(`test if Error('Login required') error is working as intended`, async () => { + expect.assertions(9); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(knowledgeBaseMutations.topicsAdd); + expectError(knowledgeBaseMutations.topicsEdit); + expectError(knowledgeBaseMutations.topicsRemove); + + expectError(knowledgeBaseMutations.categoriesAdd); + expectError(knowledgeBaseMutations.categoriesEdit); + expectError(knowledgeBaseMutations.categoriesRemove); + + expectError(knowledgeBaseMutations.articlesAdd); + expectError(knowledgeBaseMutations.articlesEdit); + expectError(knowledgeBaseMutations.articlesRemove); + }); + + describe('topic mutaions', () => { + test('topicsAdd', () => { + KnowledgeBaseTopics.createDoc = jest.fn(); + + const doc = { + title: 'Test topic title', + description: 'Test topic description', + categoryIds: ['fakeCategoryId'], + brandId: 'fakeBrandId', + }; + + knowledgeBaseMutations.topicsAdd(null, doc, { user: _user }); + + expect(KnowledgeBaseTopics.createDoc).toBeCalledWith(doc, _user._id); + expect(KnowledgeBaseTopics.createDoc.mock.calls.length).toBe(1); + + KnowledgeBaseTopics.createDoc.mockRestore(); + }); + + test('topicsEdit', () => { + KnowledgeBaseTopics.updateDoc = jest.fn(); + + const doc = { + title: 'Test topic title', + description: 'Test topic description', + categoryIds: ['fakeCategoryId'], + brandId: 'fakeBrandId', + }; + + const updateDoc = { + _id: 'fakeTopicId', + ...doc, + }; + + knowledgeBaseMutations.topicsEdit(null, updateDoc, { user: _user }); + + expect(KnowledgeBaseTopics.updateDoc).toBeCalledWith(updateDoc._id, doc, _user._id); + expect(KnowledgeBaseTopics.updateDoc.mock.calls.length).toBe(1); + + KnowledgeBaseTopics.updateDoc.mockRestore(); + }); + + test('topicsRemove', () => { + KnowledgeBaseTopics.removeDoc = jest.fn(); + + const fakeTopicId = 'fakeTopicId'; + + knowledgeBaseMutations.topicsRemove(null, { _id: fakeTopicId }, { user: _user }); + + expect(KnowledgeBaseTopics.removeDoc).toBeCalledWith(fakeTopicId); + expect(KnowledgeBaseTopics.removeDoc.mock.calls.length).toBe(1); + + KnowledgeBaseTopics.removeDoc.mockRestore(); + }); + }); + + describe('category mutaions', () => { + test('categoriesAdd', () => { + KnowledgeBaseCategories.createDoc = jest.fn(); + + const doc = { + title: 'Test topic title', + description: 'Test topic description', + categoryIds: ['fakeCategoryId'], + brandId: 'fakeBrandId', + }; + + knowledgeBaseMutations.categoriesAdd(null, doc, { user: _user }); + + expect(KnowledgeBaseCategories.createDoc).toBeCalledWith(doc, _user._id); + expect(KnowledgeBaseCategories.createDoc.mock.calls.length).toBe(1); + + KnowledgeBaseCategories.createDoc.mockRestore(); + }); + + test('categoriesEdit', () => { + KnowledgeBaseCategories.updateDoc = jest.fn(); + + const doc = { + title: 'Test category title', + description: 'Test category description', + articles: ['fakeArticleId'], + icon: 'fake icon', + }; + + const updateDoc = { + _id: 'fakeCategoryId', + ...doc, + }; + + knowledgeBaseMutations.categoriesEdit(null, updateDoc, { user: _user }); + + expect(KnowledgeBaseCategories.updateDoc).toBeCalledWith(updateDoc._id, doc, _user._id); + expect(KnowledgeBaseCategories.updateDoc.mock.calls.length).toBe(1); + + KnowledgeBaseCategories.updateDoc.mockRestore(); + }); + + test('categoriesRemove', () => { + KnowledgeBaseCategories.removeDoc = jest.fn(); + + const fakeCategoryId = 'fakeCategoryId'; + + knowledgeBaseMutations.categoriesRemove(null, { _id: fakeCategoryId }, { user: _user }); + + expect(KnowledgeBaseCategories.removeDoc).toBeCalledWith(fakeCategoryId); + expect(KnowledgeBaseCategories.removeDoc.mock.calls.length).toBe(1); + + KnowledgeBaseCategories.removeDoc.mockRestore(); + }); + }); + + describe('article mutations', () => { + test('articlesAdd', () => { + KnowledgeBaseArticles.createDoc = jest.fn(); + + const doc = { + title: 'Test article title', + summary: 'Test article summary', + content: 'Test article content', + status: 'Test article status', + }; + + knowledgeBaseMutations.articlesAdd(null, doc, { user: _user }); + + expect(KnowledgeBaseArticles.createDoc).toBeCalledWith(doc, _user._id); + expect(KnowledgeBaseArticles.createDoc.mock.calls.length).toBe(1); + + KnowledgeBaseArticles.createDoc.mockRestore(); + }); + + test('articlesEdit', () => { + KnowledgeBaseArticles.updateDoc = jest.fn(); + + const doc = { + title: 'Test article title', + summary: 'Test article summary', + content: 'Test article content', + status: 'Test article status', + }; + + const updateDoc = { + _id: 'fakeArticleId', + ...doc, + }; + + knowledgeBaseMutations.articlesEdit(null, updateDoc, { user: _user }); + + expect(KnowledgeBaseArticles.updateDoc).toBeCalledWith(updateDoc._id, doc, _user._id); + expect(KnowledgeBaseArticles.updateDoc.mock.calls.length).toBe(1); + + KnowledgeBaseArticles.updateDoc.mockRestore(); + }); + + test('articlesRemove', () => { + KnowledgeBaseArticles.removeDoc = jest.fn(); + + const fakeArticleId = 'fakeArticleId'; + + knowledgeBaseMutations.articlesRemove(null, { _id: fakeArticleId }, { user: _user }); + + expect(KnowledgeBaseArticles.removeDoc).toBeCalledWith(fakeArticleId); + expect(KnowledgeBaseArticles.removeDoc.mock.calls.length).toBe(1); + + KnowledgeBaseArticles.removeDoc.mockRestore(); + }); + }); +}); diff --git a/src/data/resolvers/mutations/knowledgeBase.js b/src/data/resolvers/mutations/knowledgeBase.js index 7f67dc3f4..6c95d1110 100644 --- a/src/data/resolvers/mutations/knowledgeBase.js +++ b/src/data/resolvers/mutations/knowledgeBase.js @@ -5,7 +5,7 @@ import { } from '../../../db/models'; export default { - knowledgeBaseTopicsAdd(root, doc, { user }) { + topicsAdd(root, doc, { user }) { if (!user) { throw new Error('Login required'); } @@ -13,7 +13,7 @@ export default { return KnowledgeBaseTopics.createDoc(doc, user._id); }, - knowledgeBaseTopicsEdit(root, { _id, ...fields }, { user }) { + topicsEdit(root, { _id, ...fields }, { user }) { if (!user) { throw new Error('Login required'); } @@ -21,7 +21,7 @@ export default { return KnowledgeBaseTopics.updateDoc(_id, fields, user._id); }, - knowledgeBaseTopicsRemove(root, { _id }, { user }) { + topicsRemove(root, { _id }, { user }) { if (!user) { throw new Error('Login required'); } @@ -29,7 +29,7 @@ export default { return KnowledgeBaseTopics.removeDoc(_id); }, - knowledgeBaseCategoriesAdd(root, doc, { user }) { + categoriesAdd(root, doc, { user }) { if (!user) { throw new Error('Login required'); } @@ -37,7 +37,7 @@ export default { return KnowledgeBaseCategories.createDoc(doc, user._id); }, - knowledgeBaseCategoriesEdit(root, { _id, ...fields }, { user }) { + categoriesEdit(root, { _id, ...fields }, { user }) { if (!user) { throw new Error('Login required'); } @@ -45,7 +45,7 @@ export default { return KnowledgeBaseCategories.updateDoc(_id, fields, user._id); }, - knowledgeBaseCategoriesRemove(root, { _id }, { user }) { + categoriesRemove(root, { _id }, { user }) { if (!user) { throw new Error('Login required'); } @@ -53,7 +53,7 @@ export default { return KnowledgeBaseCategories.removeDoc(_id); }, - knowledgeBaseArticlesAdd(root, doc, { user }) { + articlesAdd(root, doc, { user }) { if (!user) { throw new Error('Login required'); } @@ -61,7 +61,7 @@ export default { return KnowledgeBaseArticles.createDoc(doc, user._id); }, - knowledgeBaseArticlesEdit(root, { _id, ...fields }, { user }) { + articlesEdit(root, { _id, ...fields }, { user }) { if (!user) { throw new Error('Login required'); } @@ -69,7 +69,7 @@ export default { return KnowledgeBaseArticles.updateDoc(_id, fields, user._id); }, - knowledgeBaseArticlesRemove(root, { _id }, { user }) { + articlesRemove(root, { _id }, { user }) { if (!user) { throw new Error('Login required'); } diff --git a/src/data/schema/knowledgeBase.js b/src/data/schema/knowledgeBase.js index 691d5a06b..315e87dcd 100644 --- a/src/data/schema/knowledgeBase.js +++ b/src/data/schema/knowledgeBase.js @@ -15,7 +15,7 @@ export const types = ` title: String! summary: String content: String - status: String + status: String! } type KnowledgeBaseCategory { From bcfcbac1f9646a5991cfe85f4daab3824fa1f5b7 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Mon, 30 Oct 2017 21:25:13 +0800 Subject: [PATCH 121/318] Add documentation on knowledge base models and mutations --- src/__tests__/knowledgeBaseDb.test.js | 14 ++- src/data/resolvers/mutations/channels.js | 1 + src/data/resolvers/mutations/knowledgeBase.js | 87 +++++++++++++++ src/db/models/KnowledgeBase.js | 102 ++++++++++++++++++ 4 files changed, 203 insertions(+), 1 deletion(-) diff --git a/src/__tests__/knowledgeBaseDb.test.js b/src/__tests__/knowledgeBaseDb.test.js index 93d65c9d0..22faf0e04 100644 --- a/src/__tests__/knowledgeBaseDb.test.js +++ b/src/__tests__/knowledgeBaseDb.test.js @@ -42,7 +42,8 @@ describe('test knowledge base models', () => { await KnowledgeBaseCategories.remove({}); }); - test(`expect Error('userId must be supplied') to be called as intended`, () => { + test(`check if Error('userId must be supplied') + is being called as intended on create method`, () => { expect.assertions(1); try { @@ -52,6 +53,17 @@ describe('test knowledge base models', () => { } }); + test(`check if Error('userId must be supplied') + is being called as intended on update method`, async () => { + expect.assertions(1); + + try { + await KnowledgeBaseTopics.updateDoc('fakeId', {}, null); + } catch (e) { + expect(e.message).toBe('userId must be supplied'); + } + }); + test('create', async () => { let categoryA = await knowledgeBaseCategoryFactory({}); let categoryB = await knowledgeBaseCategoryFactory({}); diff --git a/src/data/resolvers/mutations/channels.js b/src/data/resolvers/mutations/channels.js index cbe82b1b1..9c9341500 100644 --- a/src/data/resolvers/mutations/channels.js +++ b/src/data/resolvers/mutations/channels.js @@ -34,6 +34,7 @@ export default { * @param {string} doc.description - Channel description * @param {String[]} doc.memberIds - Members assigned to the channel being created * @param {String[]} doc.integrationIds - Integrations related to the channel + * @param {Object} object3 - Graphql input data * @param {Object|string} user - User making this action * @return {Promise} return Promise resolving created Channel document * @throws {Error} throws Error('Login required') if user is not logged in diff --git a/src/data/resolvers/mutations/knowledgeBase.js b/src/data/resolvers/mutations/knowledgeBase.js index 6c95d1110..cc4e916a6 100644 --- a/src/data/resolvers/mutations/knowledgeBase.js +++ b/src/data/resolvers/mutations/knowledgeBase.js @@ -5,6 +5,15 @@ import { } from '../../../db/models'; export default { + /** + * Create topic document + * @param {Object} root + * @param {KnowledgeBaseTopic} doc - KnowledgeBaseTopic object + * @param {Object} object3 - Graphql input data + * @param {Object} object3.user - User object supplied by middleware + * @return {Promise} - returns Promise resolving created document + * @throws {Error} - throws Error('Login required') if user object is not supplied + */ topicsAdd(root, doc, { user }) { if (!user) { throw new Error('Login required'); @@ -13,6 +22,16 @@ export default { return KnowledgeBaseTopics.createDoc(doc, user._id); }, + /** + * Update topic document + * @param {Object} root + * @param {KnowledgeBaseTopic} doc - KnowledgeBaseTopic object + * @param {string} doc._id - KnowledgeBaseTopic document id + * @param {Object} object3 - Graphql input data + * @param {Object} object3.user - User object supplied by middleware + * @return {Promise} - returns Promise resolving modified document + * @throws {Error} - throws Error('Login required') if user object is not supplied + */ topicsEdit(root, { _id, ...fields }, { user }) { if (!user) { throw new Error('Login required'); @@ -21,6 +40,16 @@ export default { return KnowledgeBaseTopics.updateDoc(_id, fields, user._id); }, + /** + * Remove topic document + * @param {Object} root + * @param {Object} doc - KnowledgeBaseTopic object + * @param {string} doc._id - KnowledgeBaseTopic document id + * @param {Object} object3 - Graphql input data + * @param {Object} object3.user - User object supplied by middleware + * @return {Promise} + * @throws {Error} - throws Error('Login required') if user object is not supplied + */ topicsRemove(root, { _id }, { user }) { if (!user) { throw new Error('Login required'); @@ -29,6 +58,15 @@ export default { return KnowledgeBaseTopics.removeDoc(_id); }, + /** + * Create category document + * @param {Object} root + * @param {KnowledgeBaseCategory} doc - KnowledgeBaseCategory object + * @param {Object} object3 - Graphql input data + * @param {Object} object3.user - User object supplied by middleware + * @return {Promise} - returns Promise resolving created document + * @throws {Error} - throws Error('Login required') if user object is not supplied + */ categoriesAdd(root, doc, { user }) { if (!user) { throw new Error('Login required'); @@ -37,6 +75,16 @@ export default { return KnowledgeBaseCategories.createDoc(doc, user._id); }, + /** + * Update category document + * @param {Object} root + * @param {KnowledgeBaseCategory} doc - KnowledgeBaseCategory object + * @param {string} doc._id - KnowledgeBaseCategory document id + * @param {Object} object3 - Graphql input data + * @param {Object} object3.user - User object supplied by middleware + * @return {Promise} - returns Promise resolving modified document + * @throws {Error} - throws Error('Login required') if user object is not supplied + */ categoriesEdit(root, { _id, ...fields }, { user }) { if (!user) { throw new Error('Login required'); @@ -45,6 +93,16 @@ export default { return KnowledgeBaseCategories.updateDoc(_id, fields, user._id); }, + /** + * Remove category document + * @param {Object} root + * @param {Object} doc - KnowledgeBaseCategory object + * @param {string} doc._id - KnowledgeBaseCategory document id + * @param {Object} object3 - Graphql input data + * @param {Object} object3.user - User object supplied by middleware + * @return {Promise} + * @throws {Error} - throws Error('Login required') if user object is not supplied + */ categoriesRemove(root, { _id }, { user }) { if (!user) { throw new Error('Login required'); @@ -53,6 +111,15 @@ export default { return KnowledgeBaseCategories.removeDoc(_id); }, + /** + * Create article document + * @param {Object} root + * @param {KnowledgeBaseArticle} doc - KnowledgeBasecategory object + * @param {Object} object3 - Graphql input data + * @param {Object} object3.user - User object supplied by middleware + * @return {Promise} - returns Promise resolving created document + * @throws {Error} - throws Error('Login required') if user object is not supplied + */ articlesAdd(root, doc, { user }) { if (!user) { throw new Error('Login required'); @@ -61,6 +128,16 @@ export default { return KnowledgeBaseArticles.createDoc(doc, user._id); }, + /** + * Update article document + * @param {Object} root + * @param {KnowledgeBaseArticle} doc - KnowledgeBaseArticle object + * @param {string} doc._id - KnowledgeBaseArticle document id + * @param {Object} object3 - Graphql input data + * @param {Object} object3.user - User object supplied by middleware + * @return {Promise} - returns Promise resolving modified document + * @throws {Error} - throws Error('Login required') if user object is not supplied + */ articlesEdit(root, { _id, ...fields }, { user }) { if (!user) { throw new Error('Login required'); @@ -69,6 +146,16 @@ export default { return KnowledgeBaseArticles.updateDoc(_id, fields, user._id); }, + /** + * Remove article document + * @param {Object} root + * @param {Object} doc - KnowledgeBaseArticle object + * @param {string} doc._id - KnowledgeBaseArticle document id + * @param {Object} object3 - Graphql input data + * @param {Object} object3.user - User object supplied by middleware + * @return {Promise} + * @throws {Error} - throws Error('Login required') if user object is not supplied + */ articlesRemove(root, { _id }, { user }) { if (!user) { throw new Error('Login required'); diff --git a/src/db/models/KnowledgeBase.js b/src/db/models/KnowledgeBase.js index 7acd3d8d4..fc4e92bfd 100644 --- a/src/db/models/KnowledgeBase.js +++ b/src/db/models/KnowledgeBase.js @@ -2,6 +2,7 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; import { PUBLISH_STATUSES } from '../../data/constants'; +// Schema for common fields const commonFields = { createdBy: String, createdDate: { @@ -12,7 +13,18 @@ const commonFields = { modifiedDate: Date, }; +/** + * Base class for Knowledge base classes + */ class KnowledgeBaseCommonDocument { + /** + * Create document with given parameters, also set createdBy by given + * userId and also check if its supplied + * @param {Object} doc - Knowledge base document object + * @param {string} userId - User id of the creator of this document + * @return {Promise} - returns Promise resolving newly added document + * @throws {Error} - throws Error('userId must be supplied') if the userId is not supplied + */ static createDoc(doc, userId) { if (!userId) { throw new Error('userId must be supplied'); @@ -24,6 +36,13 @@ class KnowledgeBaseCommonDocument { }); } + /** + * Update knowledge base document + * @param {string} - Id of the document + * @param {Object} - Document Object + * @param {string} - The user id of the modifier + * @return {Promsie} - returns Promise resolving updated document + */ static async updateDoc(_id, doc, userId) { if (!userId) { throw new Error('userId must be supplied'); @@ -39,9 +58,15 @@ class KnowledgeBaseCommonDocument { }, }, ); + return this.findOne({ _id }); } + /** + * Removed document + * @param {string} _id - Document id + * @return {Promise} + */ static removeDoc(_id) { return this.remove({ _id }); } @@ -75,14 +100,42 @@ const ArticleSchema = mongoose.Schema({ }); class Article extends KnowledgeBaseCommonDocument { + /** + * Create KnowledgeBaseArticle document + * @param {Object} doc - KnowledgeBaseArticle object + * @param {string} doc.title - KnowledgeBaseArticle title + * @param {string} doc.summary - KnowledgeBaseArticle summary + * @param {string} doc.content - KnowledgeBaseArticle content + * @param {string} doc.status - KnowledgeBaseArticle status (currently: 'draft' or 'publish') + * @param {string} userId - User id of the creator of this document + * @return {Promise} - returns Promise resolving created document + */ static createDoc(doc, userId) { return super.createDoc(doc, userId); } + /** + * Update KnowledgeBaseArticle document + * @param {Object} _id - KnowldegeBaseArticle document id + * @param {Object} doc - KnowledgeBaseArticle object + * @param {string} doc.title - KnowledgeBaseArticle title + * @param {string} doc.summary - KnowledgeBaseArticle summary + * @param {string} doc.content - KnowledgeBaseArticle content + * @param {string} doc.status - KnowledgeBaseArticle status (currently: 'draft' or 'publish') + * @param {string} userId - User id of the modifier of this document + * @return {Promise} - returns Promise resolving modified document + */ static updateDoc(_id, doc, userId) { return super.updateDoc({ _id }, doc, userId); } + /** + * Removes KnowledgeBaseArticle document + * @param {Object} _id - KnowldegeBaseArticle document id + * @return {Promise} + * @throws {Error} - Thrwos Error('You can not delete this. This article is used in category.') + * if there are categories using this article + */ static async removeDoc(_id) { if ((await KnowledgeBaseCategories.find({ articleIds: _id }).count()) > 0) { throw new Error('You can not delete this. This article is used in category.'); @@ -115,10 +168,31 @@ const CategorySchema = mongoose.Schema({ }); class Category extends KnowledgeBaseCommonDocument { + /** + * Create KnowledgeBaseCategory document + * @param {Object} doc - KnowledgeBaseCategory object + * @param {string} doc.title - KnowledgeBaseCategory title + * @param {string} doc.description - KnowledgeBaseCategory description + * @param {string[]} doc.articleIds - KnowledgeBaseCategory articleIds + * @param {string} doc.icon - Select icon name + * @param {string} userId - User id of the creator of this document + * @return {Promise} - returns Promise resolving created document + */ static createDoc({ createdBy, createdDate, modifiedBy, modifiedDate, ...docFields }, userId) { return super.createDoc(docFields, userId); } + /** + * Update KnowledgeBaseCategory document + * @param {Object} _id - KnowledgeBaseCategory document id + * @param {Object} doc - KnowledgeBaseCategory object + * @param {string} doc.title - KnowledgeBaseCategory title + * @param {string} doc.description - KnowledgeBaseCategory description + * @param {string[]} doc.articleIds - KnowledgeBaseCategory articleIds + * @param {string} doc.icon - Select icon name + * @param {string} userId - User id of the modifier of this document + * @return {Promise} - returns Promise resolving modified document + */ static updateDoc( _id, { createdBy, createdDate, modifiedBy, modifiedDate, ...docFields }, @@ -127,6 +201,13 @@ class Category extends KnowledgeBaseCommonDocument { return super.updateDoc(_id, docFields, userId); } + /** + * Removes KnowledgeBaseCategory document + * @param {Object} _id - KnowledgeBaseCategory document id + * @return {Promise} + * @throws {Error} - Thrwos Error('You can not delete this. This category is used in topic.') + * if there are topics using this category + */ static async removeDoc(_id) { if ((await KnowledgeBaseTopics.find({ categoryIds: _id }).count()) > 0) { throw new Error('You can not delete this. This category is used in topic.'); @@ -159,10 +240,31 @@ const TopicSchema = mongoose.Schema({ }); class Topic extends KnowledgeBaseCommonDocument { + /** + * Create KnowledgeBaseTopic document + * @param {Object} doc - KnowledgeBaseTopic object + * @param {string} doc.title - KnowledgeBaseTopic title + * @param {string} doc.description - KnowledgeBaseTopic description + * @param {string[]} doc.categoryIds - KnowledgeBaseTopic category ids + * @param {string} doc.brandId - Id of the brand related to this topic + * @param {string} userId - User id of the creator of this document + * @return {Promise} - returns Promise resolving created document + */ static createDoc({ createdBy, createdDate, modifiedBy, modifiedDate, ...docFields }, userId) { return super.createDoc(docFields, userId); } + /** + * Update KnowledgeBaseTopic document + * @param {Object} _id - KnowledgeBaseTopic document id + * @param {Object} doc - KnowledgeBaseTopic object + * @param {string} doc.title - KnowledgeBaseTopic title + * @param {string} doc.description - KnowledgeBaseTopic description + * @param {string[]} doc.categoryIds - KnowledgeBaseTopic category ids + * @param {string} doc.brandId - Id of the brand related to this topic + * @param {string} userId - User id of the modifier of this document + * @return {Promise} - returns Promise resolving modified document + */ static updateDoc( _id, { createdBy, createdDate, modifiedBy, modifiedDate, ...docFields }, From 5a7fc820c7777e98c13052b124f82f89a088149e Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Tue, 31 Oct 2017 15:19:25 +0800 Subject: [PATCH 122/318] Fix knowledge base mutations --- src/__tests__/knowledgeBaseMutations.test.js | 46 +++++++++++-------- src/data/resolvers/mutations/knowledgeBase.js | 18 ++++---- src/data/schema/knowledgeBase.js | 8 ++-- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/__tests__/knowledgeBaseMutations.test.js b/src/__tests__/knowledgeBaseMutations.test.js index 5f6f22200..136c1e860 100644 --- a/src/__tests__/knowledgeBaseMutations.test.js +++ b/src/__tests__/knowledgeBaseMutations.test.js @@ -20,20 +20,20 @@ describe('mutations', () => { } }; - expectError(knowledgeBaseMutations.topicsAdd); - expectError(knowledgeBaseMutations.topicsEdit); - expectError(knowledgeBaseMutations.topicsRemove); + expectError(knowledgeBaseMutations.knowledgeBaseTopicsAdd); + expectError(knowledgeBaseMutations.knowledgeBaseTopicsEdit); + expectError(knowledgeBaseMutations.knowledgeBaseTopicsRemove); - expectError(knowledgeBaseMutations.categoriesAdd); - expectError(knowledgeBaseMutations.categoriesEdit); - expectError(knowledgeBaseMutations.categoriesRemove); + expectError(knowledgeBaseMutations.knowledgeBaseCategoriesAdd); + expectError(knowledgeBaseMutations.knowledgeBaseCategoriesEdit); + expectError(knowledgeBaseMutations.knowledgeBaseCategoriesRemove); - expectError(knowledgeBaseMutations.articlesAdd); - expectError(knowledgeBaseMutations.articlesEdit); - expectError(knowledgeBaseMutations.articlesRemove); + expectError(knowledgeBaseMutations.knowledgeBaseArticlesAdd); + expectError(knowledgeBaseMutations.knowledgeBaseArticlesEdit); + expectError(knowledgeBaseMutations.knowledgeBaseArticlesRemove); }); - describe('topic mutaions', () => { + describe('topic mutations', () => { test('topicsAdd', () => { KnowledgeBaseTopics.createDoc = jest.fn(); @@ -44,7 +44,7 @@ describe('mutations', () => { brandId: 'fakeBrandId', }; - knowledgeBaseMutations.topicsAdd(null, doc, { user: _user }); + knowledgeBaseMutations.knowledgeBaseTopicsAdd(null, doc, { user: _user }); expect(KnowledgeBaseTopics.createDoc).toBeCalledWith(doc, _user._id); expect(KnowledgeBaseTopics.createDoc.mock.calls.length).toBe(1); @@ -67,7 +67,7 @@ describe('mutations', () => { ...doc, }; - knowledgeBaseMutations.topicsEdit(null, updateDoc, { user: _user }); + knowledgeBaseMutations.knowledgeBaseTopicsEdit(null, updateDoc, { user: _user }); expect(KnowledgeBaseTopics.updateDoc).toBeCalledWith(updateDoc._id, doc, _user._id); expect(KnowledgeBaseTopics.updateDoc.mock.calls.length).toBe(1); @@ -80,7 +80,7 @@ describe('mutations', () => { const fakeTopicId = 'fakeTopicId'; - knowledgeBaseMutations.topicsRemove(null, { _id: fakeTopicId }, { user: _user }); + knowledgeBaseMutations.knowledgeBaseTopicsRemove(null, { _id: fakeTopicId }, { user: _user }); expect(KnowledgeBaseTopics.removeDoc).toBeCalledWith(fakeTopicId); expect(KnowledgeBaseTopics.removeDoc.mock.calls.length).toBe(1); @@ -100,7 +100,7 @@ describe('mutations', () => { brandId: 'fakeBrandId', }; - knowledgeBaseMutations.categoriesAdd(null, doc, { user: _user }); + knowledgeBaseMutations.knowledgeBaseCategoriesAdd(null, doc, { user: _user }); expect(KnowledgeBaseCategories.createDoc).toBeCalledWith(doc, _user._id); expect(KnowledgeBaseCategories.createDoc.mock.calls.length).toBe(1); @@ -123,7 +123,7 @@ describe('mutations', () => { ...doc, }; - knowledgeBaseMutations.categoriesEdit(null, updateDoc, { user: _user }); + knowledgeBaseMutations.knowledgeBaseCategoriesEdit(null, updateDoc, { user: _user }); expect(KnowledgeBaseCategories.updateDoc).toBeCalledWith(updateDoc._id, doc, _user._id); expect(KnowledgeBaseCategories.updateDoc.mock.calls.length).toBe(1); @@ -136,7 +136,11 @@ describe('mutations', () => { const fakeCategoryId = 'fakeCategoryId'; - knowledgeBaseMutations.categoriesRemove(null, { _id: fakeCategoryId }, { user: _user }); + knowledgeBaseMutations.knowledgeBaseCategoriesRemove( + null, + { _id: fakeCategoryId }, + { user: _user }, + ); expect(KnowledgeBaseCategories.removeDoc).toBeCalledWith(fakeCategoryId); expect(KnowledgeBaseCategories.removeDoc.mock.calls.length).toBe(1); @@ -156,7 +160,7 @@ describe('mutations', () => { status: 'Test article status', }; - knowledgeBaseMutations.articlesAdd(null, doc, { user: _user }); + knowledgeBaseMutations.knowledgeBaseArticlesAdd(null, doc, { user: _user }); expect(KnowledgeBaseArticles.createDoc).toBeCalledWith(doc, _user._id); expect(KnowledgeBaseArticles.createDoc.mock.calls.length).toBe(1); @@ -179,7 +183,7 @@ describe('mutations', () => { ...doc, }; - knowledgeBaseMutations.articlesEdit(null, updateDoc, { user: _user }); + knowledgeBaseMutations.knowledgeBaseArticlesEdit(null, updateDoc, { user: _user }); expect(KnowledgeBaseArticles.updateDoc).toBeCalledWith(updateDoc._id, doc, _user._id); expect(KnowledgeBaseArticles.updateDoc.mock.calls.length).toBe(1); @@ -192,7 +196,11 @@ describe('mutations', () => { const fakeArticleId = 'fakeArticleId'; - knowledgeBaseMutations.articlesRemove(null, { _id: fakeArticleId }, { user: _user }); + knowledgeBaseMutations.knowledgeBaseArticlesRemove( + null, + { _id: fakeArticleId }, + { user: _user }, + ); expect(KnowledgeBaseArticles.removeDoc).toBeCalledWith(fakeArticleId); expect(KnowledgeBaseArticles.removeDoc.mock.calls.length).toBe(1); diff --git a/src/data/resolvers/mutations/knowledgeBase.js b/src/data/resolvers/mutations/knowledgeBase.js index cc4e916a6..de8dcfbb2 100644 --- a/src/data/resolvers/mutations/knowledgeBase.js +++ b/src/data/resolvers/mutations/knowledgeBase.js @@ -14,7 +14,7 @@ export default { * @return {Promise} - returns Promise resolving created document * @throws {Error} - throws Error('Login required') if user object is not supplied */ - topicsAdd(root, doc, { user }) { + knowledgeBaseTopicsAdd(root, doc, { user }) { if (!user) { throw new Error('Login required'); } @@ -32,7 +32,7 @@ export default { * @return {Promise} - returns Promise resolving modified document * @throws {Error} - throws Error('Login required') if user object is not supplied */ - topicsEdit(root, { _id, ...fields }, { user }) { + knowledgeBaseTopicsEdit(root, { _id, ...fields }, { user }) { if (!user) { throw new Error('Login required'); } @@ -50,7 +50,7 @@ export default { * @return {Promise} * @throws {Error} - throws Error('Login required') if user object is not supplied */ - topicsRemove(root, { _id }, { user }) { + knowledgeBaseTopicsRemove(root, { _id }, { user }) { if (!user) { throw new Error('Login required'); } @@ -67,7 +67,7 @@ export default { * @return {Promise} - returns Promise resolving created document * @throws {Error} - throws Error('Login required') if user object is not supplied */ - categoriesAdd(root, doc, { user }) { + knowledgeBaseCategoriesAdd(root, doc, { user }) { if (!user) { throw new Error('Login required'); } @@ -85,7 +85,7 @@ export default { * @return {Promise} - returns Promise resolving modified document * @throws {Error} - throws Error('Login required') if user object is not supplied */ - categoriesEdit(root, { _id, ...fields }, { user }) { + knowledgeBaseCategoriesEdit(root, { _id, ...fields }, { user }) { if (!user) { throw new Error('Login required'); } @@ -103,7 +103,7 @@ export default { * @return {Promise} * @throws {Error} - throws Error('Login required') if user object is not supplied */ - categoriesRemove(root, { _id }, { user }) { + knowledgeBaseCategoriesRemove(root, { _id }, { user }) { if (!user) { throw new Error('Login required'); } @@ -120,7 +120,7 @@ export default { * @return {Promise} - returns Promise resolving created document * @throws {Error} - throws Error('Login required') if user object is not supplied */ - articlesAdd(root, doc, { user }) { + knowledgeBaseArticlesAdd(root, doc, { user }) { if (!user) { throw new Error('Login required'); } @@ -138,7 +138,7 @@ export default { * @return {Promise} - returns Promise resolving modified document * @throws {Error} - throws Error('Login required') if user object is not supplied */ - articlesEdit(root, { _id, ...fields }, { user }) { + knowledgeBaseArticlesEdit(root, { _id, ...fields }, { user }) { if (!user) { throw new Error('Login required'); } @@ -156,7 +156,7 @@ export default { * @return {Promise} * @throws {Error} - throws Error('Login required') if user object is not supplied */ - articlesRemove(root, { _id }, { user }) { + knowledgeBaseArticlesRemove(root, { _id }, { user }) { if (!user) { throw new Error('Login required'); } diff --git a/src/data/schema/knowledgeBase.js b/src/data/schema/knowledgeBase.js index 315e87dcd..d1db407ce 100644 --- a/src/data/schema/knowledgeBase.js +++ b/src/data/schema/knowledgeBase.js @@ -33,7 +33,7 @@ export const types = ` input KnowledgeBaseCategoryDoc { title: String! description: String - articles: [KnowledgeBaseArticle] + articleIds: [String] icon: String! } @@ -72,15 +72,15 @@ export const queries = ` `; export const mutations = ` - knowledgeBaseTopicsAdd(topicAdd: KnowledgeBaseTopicDoc): KnowledgeBaseTopic + knowledgeBaseTopicsAdd(doc: KnowledgeBaseTopicDoc): KnowledgeBaseTopic knowledgeBaseTopicsEdit(_id: String!): Boolean knowledgeBaseTopicsRemove(_id: String!): Boolean - knowledgeBaseCategoriesAdd(categoryDoc: KnowledgeBaseCategoryDoc): KnowledgeBaseTopic + knowledgeBaseCategoriesAdd(doc: KnowledgeBaseCategoryDoc): KnowledgeBaseCategory knowledgeBaseCategoriesEdit(_id: String!): Boolean knowledgeBaseCategoriesRemove(_id: String!): Boolean - knowledgeBaseArticlesAdd(articleDoc: KnowledgeBaseArticleDoc): KnowledgeBaseArticle + knowledgeBaseArticlesAdd(doc: KnowledgeBaseArticleDoc): KnowledgeBaseArticle knowledgeBaseArticlesEdit(_id: String!): Boolean knowledgeBaseArticlesRemove(_id: String!): Boolean `; From eb8d04924f9eff8cc856b69c00d1bcfa58d4ac2b Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 1 Nov 2017 13:26:06 +0800 Subject: [PATCH 123/318] Add twitter integration implementation & tests --- package.json | 3 + src/__tests__/conversationDb.test.js | 12 + src/__tests__/social/twitter.test.js | 405 ++++++++++++++++ src/__tests__/social/twitterTracker.test.js | 71 +++ src/cronJobs/conversations.js | 1 + src/data/resolvers/queries/engages.js | 2 +- src/db/factories.js | 27 +- src/db/models/Conversations.js | 22 + src/db/models/Integrations.js | 6 +- src/social/index.js | 1 + src/social/schemas.js | 28 ++ src/social/twitter.js | 249 ++++++++++ src/social/twitterTracker.js | 52 ++ yarn.lock | 497 +++++++++++++++++++- 14 files changed, 1346 insertions(+), 30 deletions(-) create mode 100755 src/__tests__/social/twitter.test.js create mode 100644 src/__tests__/social/twitterTracker.test.js create mode 100755 src/social/index.js create mode 100755 src/social/schemas.js create mode 100755 src/social/twitter.js create mode 100644 src/social/twitterTracker.js diff --git a/package.json b/package.json index cd22c527b..4d9a31c9b 100644 --- a/package.json +++ b/package.json @@ -60,8 +60,11 @@ "mongoose-type-email": "^1.0.5", "node-schedule": "^1.2.5", "nodemailer": "^4.1.3", + "sinon": "^4.0.2", + "social-oauth-client": "^0.1.6", "strip": "^3.0.0", "subscriptions-transport-ws": "^0.7.3", + "twit": "^2.2.9", "underscore": "^1.8.3", "validator": "^9.0.0" }, diff --git a/src/__tests__/conversationDb.test.js b/src/__tests__/conversationDb.test.js index a759ba038..48f09dcfc 100644 --- a/src/__tests__/conversationDb.test.js +++ b/src/__tests__/conversationDb.test.js @@ -264,4 +264,16 @@ describe('Conversation db', () => { expect(await Conversations.newOrOpenConversation().count()).toBe(1); }); + + test('Reopen', async () => { + const conversation = await conversationFactory({ + status: 'closed', + readUserIds: ['DFJAKSFJDKFJSDF'], + }); + + const updatedConversation = await Conversations.reopen(conversation._id); + + expect(updatedConversation.status).toBe('open'); + expect(updatedConversation.readUserIds.length).toBe(0); + }); }); diff --git a/src/__tests__/social/twitter.test.js b/src/__tests__/social/twitter.test.js new file mode 100755 index 000000000..cd16fd9ec --- /dev/null +++ b/src/__tests__/social/twitter.test.js @@ -0,0 +1,405 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import Twit from 'twit'; +import sinon from 'sinon'; +import { connect, disconnect } from '../../db/connection'; +import { integrationFactory, conversationFactory } from '../../db/factories'; +import { CONVERSATION_STATUSES } from '../../data/constants'; +import { Conversations, ConversationMessages, Customers, Integrations } from '../../db/models'; +import { + TwitMap, + getOrCreateCommonConversation, + tweetReply, + getOrCreateDirectMessageConversation, +} from '../../social/twitter'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('twitter integration', () => { + describe('get or create conversation', () => { + let _integration; + + const twitterUser = { + id: 2442424242, + id_str: '2442424242', + name: 'username', + screen_name: 'screen name', + profile_image_url: 'profile_image_url', + }; + + beforeEach(async () => { + _integration = await integrationFactory(); + }); + + afterEach(async () => { + await Integrations.remove({}); + await Conversations.remove({}); + await ConversationMessages.remove({}); + await Customers.remove({}); + }); + + it('common', async () => { + const tweetId = 2424244244; + + // create conversation + await conversationFactory({ + integrationId: _integration._id, + status: CONVERSATION_STATUSES.NEW, + twitterData: { + id: tweetId, + isDirectMessage: false, + }, + }); + + // replying to old tweet + await getOrCreateCommonConversation( + { + in_reply_to_status_id: tweetId, + user: twitterUser, + }, + _integration, + ); + + // must not created new conversation + expect(await Conversations.find().count()).toEqual(1); + + const conversation = await Conversations.findOne({}); + + // status must updated as open + expect(conversation.status).toEqual(CONVERSATION_STATUSES.OPEN); + }); + + it('direct message', async () => { + const senderId = 2424424242; + const recipientId = 92442424424242; + + // create conversation + await conversationFactory({ + integrationId: _integration._id, + twitterData: { + isDirectMessage: true, + directMessage: { + senderId, + senderIdStr: senderId.toString(), + recipientId, + recipientIdStr: recipientId.toString(), + }, + }, + }); + + // direct message + await getOrCreateDirectMessageConversation( + { + id: 42242242, + id_str: '42242242', + screen_name: 'screen_name', + sender_id: senderId, + sender_id_str: senderId.toString(), + recipient_id: recipientId, + recipient_id_str: recipientId.toString(), + sender: twitterUser, + }, + _integration, + ); + + // must not created new conversation + expect(await Conversations.find().count()).toBe(1); + + const conversation = await Conversations.findOne({}); + + // status must updated as open + expect(conversation.status).toBe(CONVERSATION_STATUSES.OPEN); + }); + }); + + describe('reply', () => { + let _integration; + let twit; + let stub; + + beforeEach(async () => { + const sandbox = sinon.sandbox.create(); + + // create integration + _integration = await integrationFactory({}); + + // Twit instance + twit = new Twit({ + consumer_key: 'consumer_key', + consumer_secret: 'consumer_secret', + access_token: 'access_token', + access_token_secret: 'token_secret', + }); + + // save twit instance + TwitMap[_integration._id] = twit; + + // twit.post + stub = sandbox.stub(twit, 'post').callsFake(() => {}); + }); + + afterEach(async () => { + // unwrap the spy + twit.post.restore(); + await Conversations.remove({}); + await Integrations.remove({}); + await ConversationMessages.remove({}); + await Customers.remove({}); + }); + + it('direct message', async () => { + const text = 'reply'; + const senderId = 242424242; + + const conversation = await conversationFactory({ + integrationId: _integration._id, + twitterData: { + isDirectMessage: true, + directMessage: { + senderId, + senderIdStr: senderId.toString(), + recipientId: 535335353, + recipientIdStr: '535335353', + }, + }, + }); + + // action + await tweetReply(conversation, text); + + // check twit post params + expect( + stub.calledWith('direct_messages/new', { + user_id: senderId.toString(), + text, + }), + ).toBe(true); + }); + + it('tweet', async () => { + const text = 'reply'; + const tweetIdStr = '242424242'; + const screenName = 'test'; + + const conversation = await conversationFactory({ + integrationId: _integration._id, + twitterData: { + isDirectMessage: false, + idStr: tweetIdStr, + screenName, + }, + }); + + // action + await tweetReply(conversation, text); + + // check twit post params + expect( + stub.calledWith('statuses/update', { + status: `@${screenName} ${text}`, + + // replying tweet id + in_reply_to_status_id: tweetIdStr, + }), + ).toBe(true); + }); + }); + + describe('tweet', () => { + let _integration; + + beforeEach(async () => { + // create integration + _integration = await integrationFactory({}); + }); + + afterEach(async () => { + await Conversations.remove({}); + await Integrations.remove({}); + await ConversationMessages.remove({}); + await Customers.remove({}); + }); + + it('mention', async () => { + let tweetText = '@test hi'; + const tweetId = 242424242424; + const tweetIdStr = '242424242424'; + const screenName = 'screen_name'; + const userName = 'username'; + const profileImageUrl = 'profile_image_url'; + const twitterUserId = 24242424242; + const twitterUserIdStr = '24242424242'; + + // regular tweet + const data = { + text: tweetText, + // tweeted user's info + user: { + id: twitterUserId, + id_str: twitterUserIdStr, + name: userName, + screen_name: screenName, + profile_image_url: profileImageUrl, + }, + + // tweet id + id: tweetId, + id_str: tweetIdStr, + }; + + // call action + await getOrCreateCommonConversation(data, _integration); + + expect(await Conversations.find().count()).toBe(1); // 1 conversation + expect(await Customers.find().count()).toBe(1); // 1 customer + expect(await ConversationMessages.find().count()).toBe(1); // 1 message + + let conversation = await Conversations.findOne(); + const customer = await Customers.findOne(); + const message = await ConversationMessages.findOne(); + + // check conversation field values + expect(conversation.integrationId).toBe(_integration._id); + expect(conversation.customerId).toBe(customer._id); + expect(conversation.status).toBe(CONVERSATION_STATUSES.NEW); + expect(conversation.content).toBe(tweetText); + expect(conversation.twitterData.id).toBe(tweetId); + expect(conversation.twitterData.idStr).toBe(tweetIdStr); + expect(conversation.twitterData.screenName).toBe(screenName); + expect(conversation.twitterData.isDirectMessage).toBe(false); + + // check customer field values + expect(customer.integrationId).toBe(_integration._id); + expect(customer.twitterData.id).toBe(twitterUserId); + expect(customer.twitterData.idStr).toBe(twitterUserIdStr); + expect(customer.twitterData.name).toBe(userName); + expect(customer.twitterData.screenName).toBe(screenName); + expect(customer.twitterData.profileImageUrl).toBe(profileImageUrl); + + // check message field values + expect(message.conversationId).toBe(conversation._id); + expect(message.customerId).toBe(customer._id); + expect(message.internal).toBe(false); + expect(message.content).toBe(tweetText); + + // tweet reply =============== + tweetText = 'reply'; + const newTweetId = 2442442424; + + data.text = tweetText; + data.in_reply_to_status_id = tweetId; + data.id = newTweetId; + data.idStr = newTweetId.toString(); + + // call action + await getOrCreateCommonConversation(data, _integration); + + // must not be created new conversation + expect(await Conversations.find().count()).toBe(1); + + // must not be created new customer + expect(await Customers.find().count()).toBe(1); + + // must be created new message + expect(await ConversationMessages.find().count()).toBe(2); + + // check conversation field updates + conversation = await Conversations.findOne(); + expect(conversation.readUserIds.length).toBe(0); + + const newMessage = await ConversationMessages.findOne({ _id: { $ne: message._id } }); + + // check message fields + expect(newMessage.content).toBe(tweetText); + }); + + it('direct message', async () => { + // try using non existing integration + expect(await getOrCreateDirectMessageConversation({}, { _id: 'dffdfd' })).toBe(null); + + // direct message + const data = { + id: 33324242424242, + id_str: '33324242424242', + text: 'direct message', + sender_id: 24242424242, + sender_id_str: '24242424242', + recipient_id: 343424242424242, + recipient_id_str: '343424242424242', + sender: { + id: 24242424242, + id_str: '24242424242', + name: 'username', + screen_name: 'screen_name', + profile_image_url: 'profile_image_url', + }, + }; + + // call action + await getOrCreateDirectMessageConversation(data, _integration); + + expect(await Conversations.find().count()).toBe(1); // 1 conversation + expect(await Customers.find().count()).toBe(1); // 1 customer + expect(await ConversationMessages.find().count()).toBe(1); // 1 message + + let conv = await Conversations.findOne(); + const customer = await Customers.findOne(); + const message = await ConversationMessages.findOne(); + + // check conv field values + expect(conv.integrationId).toBe(_integration._id); + expect(conv.customerId).toBe(customer._id); + expect(conv.status).toBe(CONVERSATION_STATUSES.NEW); + expect(conv.content).toBe(data.text); + expect(conv.twitterData.id).toBe(data.id); + expect(conv.twitterData.idStr).toBe(data.id_str); + expect(conv.twitterData.screenName).toBe(data.sender.screen_name); + expect(conv.twitterData.isDirectMessage).toBe(true); + expect(conv.twitterData.directMessage.senderId).toBe(data.sender_id); + expect(conv.twitterData.directMessage.senderIdStr).toBe(data.sender_id_str); + expect(conv.twitterData.directMessage.recipientId).toBe(data.recipient_id); + expect(conv.twitterData.directMessage.recipientIdStr).toBe(data.recipient_id_str); + + // check customer field values + expect(customer.integrationId).toBe(_integration._id); + expect(customer.twitterData.id).toBe(data.sender_id); + expect(customer.twitterData.idStr).toBe(data.sender_id_str); + expect(customer.twitterData.name).toBe(data.sender.name); + expect(customer.twitterData.screenName).toBe(data.sender.screen_name); + expect(customer.twitterData.profileImageUrl).toBe(data.sender.profile_image_url); + + // check message field values + expect(message.conversationId).toBe(conv._id); + expect(message.customerId).toBe(customer._id); + expect(message.internal).toBe(false); + expect(message.content).toBe(data.text); + + // tweet reply =============== + data.text = 'reply'; + data.id = 3434343434; + + // call action + await getOrCreateDirectMessageConversation(data, _integration); + + // must not be created new conversation + expect(await Conversations.find().count()).toBe(1); + + // must not be created new customer + expect(await Customers.find().count()).toBe(1); + + // must be created new message + expect(await ConversationMessages.find().count()).toBe(2); + + // check conversation field updates + conv = await Conversations.findOne(); + expect(conv.readUserIds.length).toBe(0); + + const newMessage = await ConversationMessages.findOne({ _id: { $ne: message._id } }); + + // check message fields + expect(newMessage.content).toBe(data.text); + }); + }); +}); diff --git a/src/__tests__/social/twitterTracker.test.js b/src/__tests__/social/twitterTracker.test.js new file mode 100644 index 000000000..9e4927cd7 --- /dev/null +++ b/src/__tests__/social/twitterTracker.test.js @@ -0,0 +1,71 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { receiveTimeLineResponse } from '../../social/twitter'; +import { integrationFactory, conversationFactory } from '../../db/factories'; +import { Integrations, Conversations, ConversationMessages } from '../../db/models'; +import { connect, disconnect } from '../../db/connection'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('twitter integration tracker', () => { + describe('receive timeline response', () => { + let _integration; + + const data = { + in_reply_to_status_id: 1, + entities: { + user_mentions: [], + }, + user: { + id: 1, + }, + }; + + beforeEach(async () => { + _integration = await integrationFactory({ + twitterData: { + id: 1, + }, + }); + }); + + afterEach(async () => { + await Integrations.remove({}); + await Conversations.remove({}); + await ConversationMessages.remove({}); + }); + + it('check delete integration', async () => { + const response = await receiveTimeLineResponse({ + _id: 'DFAFDFSD', + twitterData: {}, + }); + + expect(response).toBe(null); + }); + + it('receive reply', async () => { + // non existing conversation ========= + await receiveTimeLineResponse(_integration, data); + expect(await ConversationMessages.count()).toBe(0); + + // existing conversation =========== + await conversationFactory({ twitterData: { id: 1 } }); + + await receiveTimeLineResponse(_integration, data); + + expect(await ConversationMessages.count()).toBe(1); + }); + + it('user mentions', async () => { + data.in_reply_to_status_id = null; + data.entities.user_mentions = [{ id: 1 }]; + + await receiveTimeLineResponse(_integration, data); + + expect(await ConversationMessages.count()).toBe(1); + }); + }); +}); diff --git a/src/cronJobs/conversations.js b/src/cronJobs/conversations.js index 087556d05..d2115d423 100644 --- a/src/cronJobs/conversations.js +++ b/src/cronJobs/conversations.js @@ -26,6 +26,7 @@ export const sendMessageEmail = async () => { if (!customer || !customer.email) { return; } + if (!brand) { return; } diff --git a/src/data/resolvers/queries/engages.js b/src/data/resolvers/queries/engages.js index 69fbb4b2a..2778ba85d 100644 --- a/src/data/resolvers/queries/engages.js +++ b/src/data/resolvers/queries/engages.js @@ -84,7 +84,7 @@ export default { * @param {String} args.status * @return {Object} counts map */ - async engageMessageCounts(root, { name, kind, status }, { user }) { + engageMessageCounts(root, { name, kind, status }, { user }) { if (name === 'kind') { return countsByKind(); } diff --git a/src/db/factories.js b/src/db/factories.js index b9e1770f5..abc280371 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -1,6 +1,6 @@ import faker from 'faker'; import Random from 'meteor-random'; -import { MODULES, CONVERSATION_STATUSES } from '../data/constants'; +import { MODULES } from '../data/constants'; import { Users, @@ -184,11 +184,15 @@ export const fieldFactory = (params = {}) => { }; export const conversationFactory = (params = {}) => { + const doc = { + content: faker.lorem.sentence(), + customerId: Random.id(), + integrationId: Random.id(), + }; + return Conversations.createConversation({ - content: params.content || faker.lorem.sentence(), - customerId: params.customerId || Random.id(), - integrationId: params.integrationId || Random.id(), - status: CONVERSATION_STATUSES.NEW, + ...doc, + ...params, }); }; @@ -212,17 +216,20 @@ export const conversationMessageFactory = (params = {}) => { export const integrationFactory = (params = {}) => { const kind = params.kind || 'messenger'; - return Integrations.create({ + const doc = { name: faker.random.word(), kind, - brandId: params.brandId || Random.id(), - formId: params.formId || Random.id(), - messengerData: params.messengerData || { welcomeMessage: 'welcome', notifyCustomer: true }, + brandId: Random.id(), + formId: Random.id(), + messengerData: { welcomeMessage: 'welcome', notifyCustomer: true }, formData: params.formData === 'form' ? params.formData : kind === 'form' ? { thankContent: 'thankContent' } : null, - }); + }; + + Object.assign(doc, params); + return Integrations.create(doc); }; export const formFactory = async ({ title, code, description, createdUserId }) => { diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index 15f3d7276..b4df6d0e2 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -136,6 +136,28 @@ class Conversation { }); } + /* + * Reopens conversation + * @param {String} _id - Conversation id + * @return {Object} updated conversation + */ + static async reopen(_id) { + await this.update( + { _id }, + { + $set: { + // reset read state + readUserIds: [], + + // if closed, reopen + status: CONVERSATION_STATUSES.OPEN, + }, + }, + ); + + return this.findOne({ _id }); + } + /** * Assign user to conversation * @param {list} conversationIds diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index e0029b744..3709ed29e 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -10,6 +10,8 @@ import { MESSENGER_DATA_AVAILABILITY, } from '../../data/constants'; +import { TwitterSchema, FacebookSchema } from '../../social/schemas'; + // subdocument schema for MessengerOnlineHours const MessengerOnlineHoursSchema = mongoose.Schema( { @@ -112,8 +114,8 @@ const IntegrationSchema = mongoose.Schema({ formId: String, formData: FormDataSchema, messengerData: MessengerDataSchema, - twitterData: Object, - facebookData: Object, + twitterData: TwitterSchema, + facebookData: FacebookSchema, uiOptions: UiOptionsSchema, }); diff --git a/src/social/index.js b/src/social/index.js new file mode 100755 index 000000000..1fcce2e07 --- /dev/null +++ b/src/social/index.js @@ -0,0 +1 @@ +import './twitter'; diff --git a/src/social/schemas.js b/src/social/schemas.js new file mode 100755 index 000000000..01346565e --- /dev/null +++ b/src/social/schemas.js @@ -0,0 +1,28 @@ +import mongoose from 'mongoose'; + +export const TwitterSchema = mongoose.Schema( + { + id: { + type: Number, + }, + token: { + type: String, + }, + tokenSecret: { + type: String, + }, + }, + { _id: false }, +); + +export const FacebookSchema = mongoose.Schema( + { + appId: { + type: String, + }, + pageIds: { + type: [String], + }, + }, + { _id: false }, +); diff --git a/src/social/twitter.js b/src/social/twitter.js new file mode 100755 index 000000000..b27f9ffb7 --- /dev/null +++ b/src/social/twitter.js @@ -0,0 +1,249 @@ +import { Customers, ConversationMessages, Conversations, Integrations } from '../db/models'; +import { CONVERSATION_STATUSES } from '../data/constants'; + +/* + * Get or create customer using twitter data + * @param {String} integrationId - Integration id + * @param {Object} user - User + * @return customer id + */ +const getOrCreateCustomer = async (integrationId, user) => { + const customer = await Customers.findOne({ + integrationId, + 'twitterData.id': user.id, + }); + + if (customer) { + return customer._id; + } + + // create customer + return await Customers.create({ + name: user.name, + integrationId, + twitterData: { + id: user.id, + idStr: user.id_str, + name: user.name, + screenName: user.screen_name, + profileImageUrl: user.profile_image_url, + }, + }); +}; + +/* + * Create new message + * @param {Object} conversation + * @param {String} content + * @param {Object} user + * @return newly created message id + */ +const createMessage = async (conversation, content, user) => { + const customerId = await getOrCreateCustomer(conversation.integrationId, user); + + // create new message + const messageId = await ConversationMessages.create({ + conversationId: conversation._id, + customerId, + content, + internal: false, + }); + + // TODO notify subscription server new message + + return messageId; +}; + +/* + * Create new conversation by regular tweet + * @param {Object} data - Twitter stream data + * @param {Object} integration + * @return previous or newly conversation object + */ +export const getOrCreateCommonConversation = async (data, integration) => { + let conversation; + + if (data.in_reply_to_status_id) { + // find conversation by tweet id + conversation = await Conversations.findOne({ + 'twitterData.id': data.in_reply_to_status_id, + }); + + if (conversation) { + // if closed, reopen it + await Conversations.reopen(conversation._id); + + // create new message + await createMessage(conversation, data.text, data.user); + } + + // create new conversation + } else { + const customerId = await getOrCreateCustomer(integration._id, data.user); + + const conversationId = await Conversations.create({ + content: data.text, + integrationId: integration._id, + customerId, + status: CONVERSATION_STATUSES.NEW, + + // save tweet id + twitterData: { + id: data.id, + idStr: data.id_str, + screenName: data.user.screen_name, + isDirectMessage: false, + }, + }); + + conversation = await Conversations.findOne({ _id: conversationId }); + + // create new message + await createMessage(conversation, data.text, data.user); + } + + return conversation; +}; + +/* + * Create new conversation by direct message + * @param {Object} data - Twitter stream data + * @param {Object} integration + * @return previous or newly conversation object + */ +export const getOrCreateDirectMessageConversation = async (data, integration) => { + // When situations like integration is deleted but trackIntegration + // version of that integration is still running, new conversations being + // created using non existing integrationId + if (!await Integrations.findOne({ _id: integration._id })) { + return null; + } + + let conversation = await Conversations.findOne({ + 'twitterData.isDirectMessage': true, + $or: [ + { + 'twitterData.directMessage.senderId': data.sender_id, + 'twitterData.directMessage.recipientId': data.recipient_id, + }, + { + 'twitterData.directMessage.senderId': data.recipient_id, + 'twitterData.directMessage.recipientId': data.sender_id, + }, + ], + }); + + if (conversation) { + // if closed, reopen it + await Conversations.reopen(conversation._id); + + // create new message + await createMessage(conversation, data.text, data.sender); + + // create new conversation + } else { + const customerId = await getOrCreateCustomer(integration._id, data.sender); + + const conversationId = await Conversations.create({ + content: data.text, + integrationId: integration._id, + customerId, + status: CONVERSATION_STATUSES.NEW, + + // save tweet id + twitterData: { + id: data.id, + idStr: data.id_str, + screenName: data.sender.screen_name, + isDirectMessage: true, + directMessage: { + senderId: data.sender_id, + senderIdStr: data.sender_id_str, + recipientId: data.recipient_id, + recipientIdStr: data.recipient_id_str, + }, + }, + }); + + conversation = await Conversations.findOne({ _id: conversationId }); + + // create new message + await createMessage(conversation, data.text, data.sender); + } + + return conversation; +}; + +export const receiveTimeLineResponse = async (integration, data) => { + const integrationUserId = integration.twitterData.id; + const integrationOnDb = await Integrations.findOne({ _id: integration._id }); + + // When situations like integration is deleted but trackIntegration + // version of that integration is still running, new conversations being + // created using non existing integrationId + if (!integrationOnDb) { + return null; + } + + // if user is replying to some tweet + if (data.in_reply_to_status_id) { + const conversation = await Conversations.findOne({ + 'twitterData.id': data.in_reply_to_status_id, + }); + + // and that tweet must exists + if (conversation) { + return getOrCreateCommonConversation(data, integration); + } + } + + for (let mention of data.entities.user_mentions) { + // listen for only mentioned tweets + if (mention.id === integrationUserId) { + await getOrCreateCommonConversation(data, integration); + } + } + + return null; +}; + +// save twit instances by integration id +export const TwitMap = {}; + +/* + * post reply to twitter + */ +export const tweetReply = (conversation, text) => { + const twit = TwitMap[conversation.integrationId]; + const twitterData = conversation.twitterData; + + // send direct message + if (conversation.twitterData.isDirectMessage) { + return twit.post( + 'direct_messages/new', + { + user_id: twitterData.directMessage.senderIdStr, + text, + }, + /* istanbul ignore next */ + e => { + if (e) throw Error(e.message); + }, + ); + } + + // send reply + return twit.post( + 'statuses/update', + { + status: `@${twitterData.screenName} ${text}`, + + // replying tweet id + in_reply_to_status_id: twitterData.idStr, + }, + /* istanbul ignore next */ + e => { + if (e) throw Error(e.message); + }, + ); +}; diff --git a/src/social/twitterTracker.js b/src/social/twitterTracker.js new file mode 100644 index 000000000..32e735d86 --- /dev/null +++ b/src/social/twitterTracker.js @@ -0,0 +1,52 @@ +import Twit from 'twit'; +import soc from 'social-oauth-client'; + +import { TwitMap, receiveTimeLineResponse, getOrCreateDirectMessageConversation } from './twitter'; + +const { TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_REDIRECT_URL } = process.env; + +export const trackIntegration = integration => { + // Twit instance + const twit = new Twit({ + consumer_key: TWITTER_CONSUMER_KEY, + consumer_secret: TWITTER_CONSUMER_SECRET, + access_token: integration.twitterData.token, + access_token_secret: integration.twitterData.tokenSecret, + }); + + // save twit instance + TwitMap[integration._id] = twit; + + // create stream + const stream = twit.stream('user'); + + // listen for timeline + stream.on('tweet', data => receiveTimeLineResponse(integration, data)); + + // listen for direct messages + stream.on('direct_message', data => { + getOrCreateDirectMessageConversation(data.direct_message, integration); + }); +}; + +// twitter oauth =============== +export const socTwitter = new soc.Twitter({ + CONSUMER_KEY: TWITTER_CONSUMER_KEY, + CONSUMER_SECRET: TWITTER_CONSUMER_SECRET, + REDIRECT_URL: TWITTER_REDIRECT_URL, +}); + +export const authenticate = (queryParams, callback) => { + // after user clicked authenticate button + socTwitter.callback({ query: queryParams }).then(data => { + // return integration info + callback({ + name: data.info.name, + twitterData: { + id: data.info.id, + token: data.tokens.auth.token, + tokenSecret: data.tokens.auth.token_secret, + }, + }); + }); +}; diff --git a/yarn.lock b/yarn.lock index 50ba370b3..8b2fb7581 100644 --- a/yarn.lock +++ b/yarn.lock @@ -213,6 +213,10 @@ arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" +asn1@0.1.11: + version "0.1.11" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.1.11.tgz#559be18376d08a4ec4dbe80877d27818639b2df7" + asn1@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" @@ -221,6 +225,14 @@ assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" +assert-plus@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.1.5.tgz#ee74009413002d84cec7219c6ac811812e723160" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" @@ -239,25 +251,33 @@ async@2.1.4: dependencies: lodash "^4.14.0" -async@^1.4.0: +async@^1.4.0, async@~1.5.0: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.1.4: +async@^2.0.1, async@^2.1.4: version "2.5.0" resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" dependencies: lodash "^4.14.0" +async@~1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.4.2.tgz#6c9edcb11ced4f0dd2f2d40db0d49a109c088aab" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" -aws4@^1.6.0: +aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" @@ -797,6 +817,17 @@ base64url@2.0.0, base64url@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" +base64url@~0.0.4: + version "0.0.6" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-0.0.6.tgz#9597b36b330db1c42477322ea87ea8027499b82b" + +base64url@~1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-1.0.6.tgz#d64d375d68a7c640d912e2358d170dca5bb54681" + dependencies: + concat-stream "~1.4.7" + meow "~2.0.0" + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" @@ -814,6 +845,18 @@ binary-extensions@^1.0.0: version "1.10.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" +bl@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.0.3.tgz#fc5421a28fd4226036c3b3891a66a25bc64d226e" + dependencies: + readable-stream "~2.0.5" + +bl@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.1.2.tgz#fdca871a99713aa00d19e3bbba41c44787a65398" + dependencies: + readable-stream "~2.0.5" + block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -824,6 +867,10 @@ bluebird@2.10.2: version "2.10.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.10.2.tgz#024a5517295308857f14f91f1106fc3b555f446b" +bluebird@^3.1.5: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + body-parser@^1.17.1: version "1.18.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.1.tgz#9c1629370bcfd42917f30641a2dcbe2ec50d4c26" @@ -839,6 +886,12 @@ body-parser@^1.17.1: raw-body "2.3.2" type-is "~1.6.15" +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + boom@4.x.x: version "4.3.1" resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" @@ -901,7 +954,7 @@ bson@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.4.tgz#93c10d39eaa5b58415cbc4052f3e53e562b0b72c" -buffer-equal-constant-time@1.0.1: +buffer-equal-constant-time@1.0.1, buffer-equal-constant-time@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -931,7 +984,14 @@ callsites@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" -camelcase@^1.0.2: +camelcase-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-1.0.0.tgz#bd1a11bf9b31a1ce493493a930de1a0baf4ad7ec" + dependencies: + camelcase "^1.0.1" + map-obj "^1.0.0" + +camelcase@^1.0.1, camelcase@^1.0.2: version "1.2.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" @@ -947,6 +1007,10 @@ capture-stack-trace@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -1080,6 +1144,14 @@ concat-stream@^1.5.2: readable-stream "^2.2.2" typedarray "^0.0.6" +concat-stream@~1.4.7: + version "1.4.10" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.4.10.tgz#acc3bbf5602cb8cc980c6ac840fa7d8603e3ef36" + dependencies: + inherits "~2.0.1" + readable-stream "~1.1.9" + typedarray "~0.0.5" + configstore@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90" @@ -1111,6 +1183,13 @@ convert-source-map@^1.4.0, convert-source-map@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" +cookie-parser@^1.4.0: + version "1.4.3" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.3.tgz#0fe31fa19d000b95f4aadf1f53fdc2b8a203baa5" + dependencies: + cookie "0.3.1" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -1168,6 +1247,12 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + cryptiles@3.x.x: version "3.1.2" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" @@ -1192,6 +1277,10 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": dependencies: cssom "0.3.x" +ctype@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/ctype/-/ctype-0.5.3.tgz#82c18c2461f74114ef16c135224ad0b9144ca12f" + d@1: version "1.0.0" resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" @@ -1291,6 +1380,10 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" +diff@^3.1.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" + diff@^3.2.0: version "3.3.1" resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" @@ -1326,7 +1419,7 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" -ecdsa-sig-formatter@1.0.9: +ecdsa-sig-formatter@1.0.9, ecdsa-sig-formatter@^1.0.0: version "1.0.9" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" dependencies: @@ -1652,7 +1745,7 @@ express@^4.15.2: utils-merge "1.0.0" vary "~1.1.1" -extend@~3.0.1: +extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -1781,6 +1874,14 @@ forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" +form-data@~1.0.0-rc3, form-data@~1.0.0-rc4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.1.tgz#ae315db9a4907fa065502304a66d7733475ee37c" + dependencies: + async "^2.0.1" + combined-stream "^1.0.5" + mime-types "^2.1.11" + form-data@~2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" @@ -1789,6 +1890,12 @@ form-data@~2.3.1: combined-stream "^1.0.5" mime-types "^2.1.12" +formatio@1.2.0, formatio@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb" + dependencies: + samsam "1.x" + forwarded@~0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -1837,6 +1944,13 @@ function-bind@^1.0.2, function-bind@^1.1.1, function-bind@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" +gapitoken@~0.1.2: + version "0.1.5" + resolved "https://registry.yarnpkg.com/gapitoken/-/gapitoken-0.1.5.tgz#3577fcfb5426be3a7b8ebada92671229d8cc81ce" + dependencies: + jws "~3.0.0" + request "^2.54.0" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -1864,6 +1978,10 @@ get-caller-file@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -1913,6 +2031,33 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +google-auth-library@~0.9.3: + version "0.9.10" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.9.10.tgz#4993dc07bb4834b8ca0350213a6873a32c6051b9" + dependencies: + async "~1.4.2" + gtoken "^1.1.0" + jws "~3.0.0" + lodash.noop "~3.0.0" + request "~2.74.0" + string-template "~0.2.0" + +google-p12-pem@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-0.1.2.tgz#33c46ab021aa734fa0332b3960a9a3ffcb2f3177" + dependencies: + node-forge "^0.7.1" + +googleapis@^2.1.6: + version "2.1.7" + resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-2.1.7.tgz#d8283e16e68e552d28b134db25da52a04483ed4d" + dependencies: + async "~1.5.0" + gapitoken "~0.1.2" + google-auth-library "~0.9.3" + request "~2.65.0" + string-template "~0.2.0" + got@^6.7.1: version "6.7.1" resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" @@ -1984,6 +2129,15 @@ growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" +gtoken@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-1.2.3.tgz#5509571b8afd4322e124cf66cf68115284c476d8" + dependencies: + google-p12-pem "^0.1.0" + jws "^3.0.0" + mime "^1.4.1" + request "^2.72.0" + handlebars@^4.0.10, handlebars@^4.0.3: version "4.0.10" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f" @@ -1998,6 +2152,15 @@ har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" +har-validator@~2.0.2, har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + har-validator@~5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" @@ -2029,6 +2192,15 @@ has@^1.0.1, has@~1.0.1: dependencies: function-bind "^1.0.2" +hawk@~3.1.0, hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + hawk@~6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" @@ -2038,6 +2210,10 @@ hawk@~6.0.2: hoek "4.x.x" sntp "2.x.x" +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + hoek@4.x.x: version "4.2.0" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" @@ -2072,6 +2248,22 @@ http-errors@1.6.2, http-errors@~1.6.2: setprototypeof "1.0.3" statuses ">= 1.3.1 < 2" +http-signature@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-0.11.0.tgz#1796cf67a001ad5cd6849dca0991485f09089fe6" + dependencies: + asn1 "0.1.11" + assert-plus "^0.1.5" + ctype "0.5.3" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -2113,6 +2305,14 @@ imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" +indent-string@^1.1.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-1.2.2.tgz#db99bcc583eb6abbb1e48dcbb1999a986041cb6b" + dependencies: + get-stdin "^4.0.1" + minimist "^1.1.0" + repeating "^1.1.0" + indent-string@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" @@ -2252,7 +2452,7 @@ is-glob@^2.0.0, is-glob@^2.0.1: dependencies: is-extglob "^1.0.0" -is-my-json-valid@^2.10.0: +is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: version "2.16.1" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" dependencies: @@ -2355,6 +2555,10 @@ is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -2773,6 +2977,10 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +just-extend@^1.1.26: + version "1.1.26" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.26.tgz#dba4ad2786d319f1d10afab106e004b5a0851ac2" + jwa@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" @@ -2782,7 +2990,15 @@ jwa@^1.1.4: ecdsa-sig-formatter "1.0.9" safe-buffer "^5.0.1" -jws@^3.1.4: +jwa@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.0.2.tgz#fd79609f1e772e299dce8ddb76d00659dd83511f" + dependencies: + base64url "~0.0.4" + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "^1.0.0" + +jws@^3.0.0, jws@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" dependencies: @@ -2790,6 +3006,13 @@ jws@^3.1.4: jwa "^1.1.4" safe-buffer "^5.0.1" +jws@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.0.0.tgz#da5f267897dd4e9cf8137979db33fc54a3c05418" + dependencies: + base64url "~1.0.4" + jwa "~1.0.0" + kareem@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/kareem/-/kareem-1.5.0.tgz#e3e4101d9dcfde299769daf4b4db64d895d17448" @@ -2974,6 +3197,10 @@ lodash.defaults@^3.1.2: lodash.assign "^3.0.0" lodash.restparam "^3.0.0" +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -3018,6 +3245,10 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" +lodash.noop@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c" + lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" @@ -3026,6 +3257,10 @@ lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" +lodash@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -3043,6 +3278,14 @@ log-update@^1.0.2: ansi-escapes "^1.0.0" cli-cursor "^1.0.2" +lolex@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" + +lolex@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.1.3.tgz#53f893bbe88c80378156240e127126b905c83087" + long-timeout@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" @@ -3080,6 +3323,10 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" +map-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + map-stream@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" @@ -3094,6 +3341,15 @@ mem@^1.1.0: dependencies: mimic-fn "^1.0.0" +meow@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-2.0.0.tgz#8f530a8ecf5d40d3f4b4df93c3472900fba2a8f1" + dependencies: + camelcase-keys "^1.0.0" + indent-string "^1.1.0" + minimist "^1.1.0" + object-assign "^1.0.0" + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -3134,7 +3390,7 @@ mime-db@~1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" -mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17: +mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7: version "2.1.17" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" dependencies: @@ -3144,6 +3400,10 @@ mime@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" +mime@^1.3.4, mime@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" @@ -3158,7 +3418,7 @@ minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@^1.1.1, minimist@^1.2.0, minimist@~1.2.0: +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -3267,6 +3527,20 @@ negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" +nise@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.2.0.tgz#079d6cadbbcb12ba30e38f1c999f36ad4d6baa53" + dependencies: + formatio "^1.2.0" + just-extend "^1.1.26" + lolex "^1.6.0" + path-to-regexp "^1.7.0" + text-encoding "^0.6.4" + +node-forge@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -3317,6 +3591,10 @@ node-schedule@^1.2.5: long-timeout "0.1.1" sorted-array-functions "^1.0.0" +node-uuid@~1.4.3, node-uuid@~1.4.7: + version "1.4.8" + resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" + nodemailer@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.1.3.tgz#4125a6ef79ecfb68357a65c34e4810f210ae120c" @@ -3405,10 +3683,14 @@ number-is-nan@^1.0.0: version "1.4.2" resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.2.tgz#c5e545ab40d22a56b0326531c4beaed7a888b3ea" -oauth-sign@~0.8.2: +oauth-sign@~0.8.0, oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" +object-assign@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-1.0.0.tgz#e65dc8766d3b47b4b8307465c8311da030b070a6" + object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -3586,6 +3868,12 @@ path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -3692,6 +3980,10 @@ punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" +q@^1.4.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + qs@6.5.0: version "6.5.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49" @@ -3700,6 +3992,18 @@ qs@6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" +qs@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-5.2.1.tgz#801fee030e0b9450d6385adc48a4cc55b44aedfc" + +qs@~6.2.0: + version "6.2.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.3.tgz#1cfcb25c10a9b2b483053ff39f5dfc9233908cfe" + +querystring@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + randomatic@^1.1.3: version "1.1.7" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" @@ -3783,6 +4087,26 @@ readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable string_decoder "~1.0.3" util-deprecate "~1.0.1" +readable-stream@~1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + readdirp@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" @@ -3879,13 +4203,19 @@ repeat-string@^1.5.2: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" +repeating@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac" + dependencies: + is-finite "^1.0.0" + repeating@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" dependencies: is-finite "^1.0.0" -request@^2.79.0: +request@^2.54.0, request@^2.65.0, request@^2.68.0, request@^2.72.0, request@^2.79.0: version "2.83.0" resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" dependencies: @@ -3939,6 +4269,56 @@ request@^2.81.0: tunnel-agent "^0.6.0" uuid "^3.1.0" +request@~2.65.0: + version "2.65.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.65.0.tgz#cc1a3bc72b96254734fc34296da322f9486ddeba" + dependencies: + aws-sign2 "~0.6.0" + bl "~1.0.0" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~1.0.0-rc3" + har-validator "~2.0.2" + hawk "~3.1.0" + http-signature "~0.11.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + node-uuid "~1.4.3" + oauth-sign "~0.8.0" + qs "~5.2.0" + stringstream "~0.0.4" + tough-cookie "~2.2.0" + tunnel-agent "~0.4.1" + +request@~2.74.0: + version "2.74.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.74.0.tgz#7693ca768bbb0ea5c8ce08c084a45efa05b892ab" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + bl "~1.1.2" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~1.0.0-rc4" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + node-uuid "~1.4.7" + oauth-sign "~0.8.1" + qs "~6.2.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -4028,6 +4408,10 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +samsam@1.x: + version "1.3.0" + resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" + sane@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/sane/-/sane-2.2.0.tgz#d6d2e2fcab00e3d283c93b912b7c3a20846f1d56" @@ -4042,7 +4426,7 @@ sane@^2.0.0: optionalDependencies: fsevents "^1.1.1" -sax@^1.2.1: +sax@>=0.6.0, sax@^1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -4121,6 +4505,18 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +sinon@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-4.0.2.tgz#c81f62456d37986c84e9f522ddb9ce413bda49d2" + dependencies: + diff "^3.1.0" + formatio "1.2.0" + lodash.get "^4.4.2" + lolex "^2.1.3" + nise "^1.2.0" + supports-color "^4.4.0" + type-detect "^4.0.0" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -4137,12 +4533,30 @@ sliced@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + sntp@2.x.x: version "2.0.2" resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b" dependencies: hoek "4.x.x" +social-oauth-client@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/social-oauth-client/-/social-oauth-client-0.1.6.tgz#e1f58b6e01b21295268d9fcc4e4d87a74f92a08b" + dependencies: + cookie-parser "^1.4.0" + googleapis "^2.1.6" + lodash "^3.10.1" + q "^1.4.1" + querystring "^0.2.0" + request "^2.65.0" + xml2js "^0.4.15" + sorted-array-functions@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.0.0.tgz#c0b554d9e709affcbe56d34c1b2514197fd38279" @@ -4226,6 +4640,10 @@ string-length@^2.0.0: astral-regex "^1.0.0" strip-ansi "^4.0.0" +string-template@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" + string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -4249,13 +4667,17 @@ string.prototype.trim@~1.1.2: es-abstract "^1.5.0" function-bind "^1.0.2" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + string_decoder@~1.0.0, string_decoder@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" dependencies: safe-buffer "~5.1.0" -stringstream@~0.0.5: +stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -4324,6 +4746,12 @@ supports-color@^4.0.0: dependencies: has-flag "^2.0.0" +supports-color@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + dependencies: + has-flag "^2.0.0" + symbol-observable@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" @@ -4398,6 +4826,10 @@ test-exclude@^4.1.1: read-pkg-up "^1.0.1" require-main-filename "^1.0.1" +text-encoding@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" + text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -4428,12 +4860,16 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@^2.3.2, tough-cookie@~2.3.2, tough-cookie@~2.3.3: +tough-cookie@^2.3.2, tough-cookie@~2.3.0, tough-cookie@~2.3.2, tough-cookie@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" dependencies: punycode "^1.4.1" +tough-cookie@~2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.2.2.tgz#c83a1830f4e5ef0b93ef2a3488e724f8de016ac7" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -4452,16 +4888,32 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" +twit@^2.2.9: + version "2.2.9" + resolved "https://registry.yarnpkg.com/twit/-/twit-2.2.9.tgz#6710574f81641daa03796a1b4b8e7b78d3d75676" + dependencies: + bluebird "^3.1.5" + mime "^1.3.4" + request "^2.68.0" + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" dependencies: prelude-ls "~1.1.2" +type-detect@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea" + type-is@~1.6.15: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" @@ -4469,7 +4921,7 @@ type-is@~1.6.15: media-typer "0.3.0" mime-types "~2.1.15" -typedarray@^0.0.6: +typedarray@^0.0.6, typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -4706,6 +5158,17 @@ xml-name-validator@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" +xml2js@^0.4.15: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xmlbuilder@~9.0.1: + version "9.0.4" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f" + xtend@^4.0.0, xtend@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" From 24ed59d8fe9f18615a9ff0b042dfc02791d495f9 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 1 Nov 2017 16:16:56 +0800 Subject: [PATCH 124/318] Begining facebook integration --- package.json | 3 +- src/social/facebook.js | 445 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 447 insertions(+), 1 deletion(-) create mode 100755 src/social/facebook.js diff --git a/package.json b/package.json index 4d9a31c9b..6fda99871 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "cors": "^2.8.1", "dotenv": "^4.0.0", "express": "^4.15.2", + "fbgraph": "^1.4.1", "graphql": "^0.10.1", "graphql-server-core": "^0.8.2", "graphql-server-express": "^0.8.2", @@ -60,7 +61,7 @@ "mongoose-type-email": "^1.0.5", "node-schedule": "^1.2.5", "nodemailer": "^4.1.3", - "sinon": "^4.0.2", + "sinon": "^4.0.1", "social-oauth-client": "^0.1.6", "strip": "^3.0.0", "subscriptions-transport-ws": "^0.7.3", diff --git a/src/social/facebook.js b/src/social/facebook.js new file mode 100755 index 000000000..eca0a1ef4 --- /dev/null +++ b/src/social/facebook.js @@ -0,0 +1,445 @@ +import graph from 'fbgraph'; +import { Integrations, Conversations, ConversationMessages, Customers } from '../db/models'; + +import { + INTEGRATION_KIND_CHOICES, + CONVERSATION_STATUSES, + FACEBOOK_DATA_KINDS, +} from '../data/constants'; + +/* + * Common graph api request wrapper + * catchs auth token or other type of exceptions + */ +export const graphRequest = { + base(method, path, accessToken, ...otherParams) { + // set access token + graph.setAccessToken(accessToken); + + try { + // TODO + return graph[method](otherParams); + + // catch session expired or some other error + } catch (e) { + console.log(e.message); // eslint-disable-line no-console + return e.message; + } + }, + + get(...args) { + return this.base('get', ...args); + }, + + post(...args) { + return this.base('post', ...args); + }, +}; + +/* + * get list of pages that authorized user owns + */ +export const getPageList = accessToken => { + const response = graphRequest.get('/me/accounts?limit=100', accessToken); + + return response.data.map(page => ({ + id: page.id, + name: page.name, + })); +}; + +/* + * save webhook response + * create conversation, customer, message using transmitted data + */ + +export class SaveWebhookResponse { + constructor(userAccessToken, integration, data) { + this.userAccessToken = userAccessToken; + + this.integration = integration; + + // received facebook data + this.data = data; + + this.currentPageId = null; + } + + async start() { + const data = this.data; + const integration = this.integration; + + if (data.object === 'page') { + for (let entry of data.entry) { + // check receiving page is in integration's page list + if (!integration.facebookData.pageIds.includes(entry.id)) { + return; + } + + // set current page + this.currentPageId = entry.id; + + // receive new messenger message + if (entry.messaging) { + await this.viaMessengerEvent(entry); + } + + // receive new feed + if (entry.changes) { + await this.viaFeedEvent(entry); + } + } + } + } + + /* + * via page messenger + */ + async viaMessengerEvent(entry) { + for (let messagingEvent of entry.messaging) { + // someone sent us a message + if (messagingEvent.message) { + await this.getOrCreateConversationByMessenger(messagingEvent); + } + } + } + + /* + * wall post + */ + async viaFeedEvent(entry) { + for (let event of entry.changes) { + // someone posted on our wall + await this.getOrCreateConversationByFeed(event.value); + } + } + + /* + * common get or create conversation helper using both in messenger and feed + */ + async getOrCreateConversation(params) { + // extract params + const { + findSelector, + status, + senderId, + facebookData, + content, + attachments, + msgFacebookData, + } = params; + + let conversation = await Conversations.findOne({ + ...findSelector, + }); + + // create new conversation + if (!conversation) { + const conversationId = await Conversations.create({ + integrationId: this.integration._id, + customerId: await this.getOrCreateCustomer(senderId), + status, + content, + + // save facebook infos + facebookData: { + ...facebookData, + pageId: this.currentPageId, + }, + }); + + conversation = await Conversations.findOne({ _id: conversationId }); + + // update conversation + } else { + await Conversations.update( + { _id: conversation._id }, + { + $set: { + // reset read history + readUserIds: [], + + // if closed, reopen it + status: CONVERSATION_STATUSES.OPEN, + }, + }, + ); + } + + // create new message + return this.createMessage({ + conversation, + userId: senderId, + content, + attachments, + facebookData: msgFacebookData, + }); + } + + /* + * get or create new conversation by feed info + */ + async getOrCreateConversationByFeed(value) { + const commentId = value.comment_id; + + // collect only added actions + if (value.verb !== 'add') { + return; + } + + // ignore duplicated action when like + if (value.verb === 'add' && value.item === 'like') { + return; + } + + // if this is already saved then ignore it + if ( + commentId && + (await ConversationMessages.findOne({ 'facebookData.commentId': commentId })) + ) { + return; + } + + const senderName = value.sender_name; + + // sender_id is giving number values when feed and giving string value + // when messenger. customer.facebookData.senderId has type of string so + // convert it to string + const senderId = value.sender_id.toString(); + + let messageText = value.message; + + // when photo, video share, there will be no text, so link instead + if (!messageText && value.link) { + messageText = value.link; + } + + // when situations like checkin, there will be no text and no link + // if so ignore it + if (!messageText) { + return; + } + + // value.post_id is returning different value even though same post + // with the previous one. So fetch post info via graph api and + // save returned value. This value will always be the same + let postId = value.post_id; + + // get page access token + let response = graphRequest.get( + `${this.currentPageId}/?fields=access_token`, + this.userAccessToken, + ); + + // acess token expired + if (response === 'Error processing https request') { + return; + } + + // get post object + response = graphRequest.get(postId, response.access_token); + + postId = response.id; + + let status = CONVERSATION_STATUSES.NEW; + + // if we are posting from our page, close it automatically + if (this.integration.facebookData.pageIds.includes(senderId)) { + status = CONVERSATION_STATUSES.CLOSED; + } + + await this.getOrCreateConversation({ + findSelector: { + 'facebookData.kind': FACEBOOK_DATA_KINDS.FEED, + 'facebookData.postId': postId, + }, + status, + senderId, + facebookData: { + kind: FACEBOOK_DATA_KINDS.FEED, + senderId, + senderName, + postId, + }, + + // message data + content: messageText, + msgFacebookData: { + senderId, + senderName, + item: value.item, + reactionType: value.reaction_type, + photoId: value.photo_id, + videoId: value.video_id, + link: value.link, + }, + }); + } + + /* + * get or create new conversation by page messenger + */ + async getOrCreateConversationByMessenger(event) { + const senderId = event.sender.id; + const senderName = event.sender.name; + const recipientId = event.recipient.id; + const messageText = event.message.text || 'attachment'; + + // collect attachment's url, type fields + const attachments = (event.message.attachments || []).map(attachment => ({ + type: attachment.type, + url: attachment.payload ? attachment.payload.url : '', + })); + + await this.getOrCreateConversation({ + // try to find conversation by senderId, recipientId keys + findSelector: { + 'facebookData.kind': FACEBOOK_DATA_KINDS.MESSENGER, + $or: [ + { + 'facebookData.senderId': senderId, + 'facebookData.recipientId': recipientId, + }, + { + 'facebookData.senderId': recipientId, + 'facebookData.recipientId': senderId, + }, + ], + }, + status: CONVERSATION_STATUSES.NEW, + senderId, + facebookData: { + kind: FACEBOOK_DATA_KINDS.MESSENGER, + senderId, + senderName, + recipientId, + }, + + // message data + content: messageText, + attachments, + msgFacebookData: {}, + }); + } + + /* + * get or create customer using facebook data + */ + async getOrCreateCustomer(fbUserId) { + const integrationId = this.integration._id; + + const customer = await Customers.findOne({ + integrationId, + 'facebookData.id': fbUserId, + }); + + if (customer) { + return customer._id; + } + + // get page access token + let res = graphRequest.get(`${this.currentPageId}/?fields=access_token`, this.userAccessToken); + + // get user info + res = graphRequest.get(`/${fbUserId}`, res.access_token); + + // when feed response will contain name field + // when messeger response will not contain name field + const name = res.name || `${res.first_name} ${res.last_name}`; + + // create customer + return Customers.create({ + name, + integrationId, + facebookData: { + id: fbUserId, + profilePic: res.profile_pic, + }, + }); + } + + async createMessage({ conversation, userId, content, attachments, facebookData }) { + if (conversation) { + // create new message + const messageId = await ConversationMessages.create({ + conversationId: conversation._id, + customerId: await this.getOrCreateCustomer(userId), + content, + attachments, + facebookData, + internal: false, + }); + + // TODO notify subscription server new message + + return messageId; + } + } +} + +/* + * receive per app webhook response + */ +export const receiveWebhookResponse = async (app, data) => { + const selector = { + kind: INTEGRATION_KIND_CHOICES.FACEBOOK, + 'facebookData.appId': app.id, + }; + + const integrations = await Integrations.find(selector); + + for (let integration of integrations) { + // when new message or other kind of activity in page + const saveWebhookResponse = new SaveWebhookResponse(app.accessToken, integration, data); + + await saveWebhookResponse.start(); + } +}; + +/* + * post reply to page conversation or comment to wall post + */ +export const facebookReply = async (conversation, text, messageId) => { + const { FACEBOOK } = process.env; + + const app = JSON.parse(FACEBOOK).find( + a => a.id === conversation.integration().facebookData.appId, + ); + + // page access token + const response = graphRequest.get( + `${conversation.facebookData.pageId}/?fields=access_token`, + app.accessToken, + ); + + // messenger reply + if (conversation.facebookData.kind === FACEBOOK_DATA_KINDS.MESSENGER) { + return graphRequest.post( + 'me/messages', + response.access_token, + { + recipient: { id: conversation.facebookData.senderId }, + message: { text }, + }, + () => {}, + ); + } + + // feed reply + if (conversation.facebookData.kind === FACEBOOK_DATA_KINDS.FEED) { + const postId = conversation.facebookData.postId; + + // post reply + const commentResponse = graphRequest.post(`${postId}/comments`, response.access_token, { + message: text, + }); + + // save commentId in message object + await ConversationMessages.update( + { _id: messageId }, + { $set: { 'facebookData.commentId': commentResponse.id } }, + ); + } + + return null; +}; From 5cb2d174a8119b3f4dc7c68a568f300c469abaea Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 1 Nov 2017 16:17:50 +0800 Subject: [PATCH 125/318] Add facebook conversationByFeed tests --- .../facebook.conversationByFeed.test.js | 70 +++++++++++++++++++ src/db/factories.js | 4 +- yarn.lock | 13 +++- 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100755 src/__tests__/social/facebook.conversationByFeed.test.js diff --git a/src/__tests__/social/facebook.conversationByFeed.test.js b/src/__tests__/social/facebook.conversationByFeed.test.js new file mode 100755 index 000000000..90582ae4b --- /dev/null +++ b/src/__tests__/social/facebook.conversationByFeed.test.js @@ -0,0 +1,70 @@ +/* eslint-env jest */ + +import sinon from 'sinon'; +import { connect, disconnect } from '../../db/connection'; +import { graphRequest, SaveWebhookResponse } from '../../social/facebook'; +import { Conversations, ConversationMessages } from '../../db/models'; +import { integrationFactory } from '../../db/factories'; +import { CONVERSATION_STATUSES } from '../../data/constants'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('facebook integration: get or create conversation by feed info', () => { + beforeEach(() => { + // mock all requests + sinon.stub(graphRequest, 'get').callsFake(path => { + if (path.includes('/?fields=access_token')) { + return { + access_token: '244242442442', + }; + } + + return {}; + }); + }); + + afterEach(async () => { + // clear + await Conversations.remove({}); + await ConversationMessages.remove({}); + + graphRequest.get.restore(); // unwraps the spy + }); + + it('admin posts', async () => { + const senderId = 'DFDFDEREREEFFFD'; + const postId = 'DFJDFJDIF'; + + // indicating sender is our admins, in other words posting from our page + const pageId = senderId; + + const integration = await integrationFactory({ + facebookData: { + appId: '242424242422', + pageIds: [pageId, 'DFDFDFDFDFD'], + }, + }); + + const saveWebhookResponse = new SaveWebhookResponse('access_token', integration); + + saveWebhookResponse.currentPageId = 'DFDFDFDFDFD'; + + // must be 0 conversations + expect(await Conversations.find().count()).toBe(0); + + await saveWebhookResponse.getOrCreateConversationByFeed({ + verb: 'add', + sender_id: senderId, + post_id: postId, + message: 'hi all', + }); + + expect(await Conversations.find().count()).toBe(1); // 1 conversation + + const conversation = await Conversations.findOne(); + + // our posts will be closed automatically + expect(conversation.status).toBe(CONVERSATION_STATUSES.CLOSED); + }); +}); diff --git a/src/db/factories.js b/src/db/factories.js index abc280371..30d258ebb 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -213,7 +213,7 @@ export const conversationMessageFactory = (params = {}) => { }); }; -export const integrationFactory = (params = {}) => { +export const integrationFactory = async (params = {}) => { const kind = params.kind || 'messenger'; const doc = { @@ -222,13 +222,13 @@ export const integrationFactory = (params = {}) => { brandId: Random.id(), formId: Random.id(), messengerData: { welcomeMessage: 'welcome', notifyCustomer: true }, + facebookData: params.facebookData || {}, formData: params.formData === 'form' ? params.formData : kind === 'form' ? { thankContent: 'thankContent' } : null, }; - Object.assign(doc, params); return Integrations.create(doc); }; diff --git a/yarn.lock b/yarn.lock index 8b2fb7581..e38987c20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1777,6 +1777,13 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" +fbgraph@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/fbgraph/-/fbgraph-1.4.1.tgz#b2aa380f9ef7da302978d0749fad699fb974c104" + dependencies: + qs "^1.2.2" + request "^2.79.0" + figures@^1.3.5, figures@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -3992,6 +3999,10 @@ qs@6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" +qs@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-1.2.2.tgz#19b57ff24dc2a99ce1f8bdf6afcda59f8ef61f88" + qs@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/qs/-/qs-5.2.1.tgz#801fee030e0b9450d6385adc48a4cc55b44aedfc" @@ -4505,7 +4516,7 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" -sinon@^4.0.2: +sinon@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/sinon/-/sinon-4.0.2.tgz#c81f62456d37986c84e9f522ddb9ce413bda49d2" dependencies: From 1560522baafeb67933d33b4f0fb3c506521a1ebb Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 1 Nov 2017 16:19:05 +0800 Subject: [PATCH 126/318] Add facebook getOrCreateConversation tests --- .../facebook.getOrCreateConversation.test.js | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/__tests__/social/facebook.getOrCreateConversation.test.js diff --git a/src/__tests__/social/facebook.getOrCreateConversation.test.js b/src/__tests__/social/facebook.getOrCreateConversation.test.js new file mode 100644 index 000000000..38ee8382b --- /dev/null +++ b/src/__tests__/social/facebook.getOrCreateConversation.test.js @@ -0,0 +1,123 @@ +/* eslint-env jest */ + +import sinon from 'sinon'; +import { connect, disconnect } from '../../db/connection'; +import { graphRequest, SaveWebhookResponse } from '../../social/facebook'; +import { Conversations, ConversationMessages } from '../../db/models'; +import { integrationFactory, customerFactory } from '../../db/factories'; +import { CONVERSATION_STATUSES, FACEBOOK_DATA_KINDS } from '../../data/constants'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('facebook integration: get or create conversation', () => { + const senderId = 2242424244; + const pageId = '2252525525'; + + beforeEach(() => { + // mock all requests + sinon.stub(graphRequest, 'get').callsFake(() => {}); + }); + + afterEach(async () => { + graphRequest.get.restore(); // unwraps the spy + + // clear + await Conversations.remove({}); + await ConversationMessages.remove({}); + }); + + it('get or create conversation', async () => { + const postId = '32242442442'; + const customerId = await customerFactory(); + const integration = await integrationFactory(); + + const saveWebhookResponse = new SaveWebhookResponse('access_token', integration, {}); + saveWebhookResponse.currentPageId = pageId; + + // mock getOrCreateCustomer + sinon.stub(saveWebhookResponse, 'getOrCreateCustomer').callsFake(() => customerId); + + // check initial states + expect(await Conversations.find().count()).toBe(0); + expect(await ConversationMessages.find().count()).toBe(0); + + const facebookData = { + kind: FACEBOOK_DATA_KINDS.FEED, + senderId, + postId, + }; + + const filter = { + 'facebookData.kind': FACEBOOK_DATA_KINDS.FEED, + 'facebookData.postId': postId, + }; + + // customer said hi ====================== + await saveWebhookResponse.getOrCreateConversation({ + findSelector: filter, + status: CONVERSATION_STATUSES.NEW, + senderId, + facebookData, + content: 'hi', + }); + + // must be created new conversation, new message + expect(await Conversations.find().count()).toBe(1); + expect(await ConversationMessages.find().count()).toBe(1); + + let conversation = await Conversations.findOne({}); + expect(conversation.status).toBe(CONVERSATION_STATUSES.NEW); + + // customer commented on above converstaion =========== + await saveWebhookResponse.getOrCreateConversation({ + findSelector: filter, + status: CONVERSATION_STATUSES.NEW, + senderId, + facebookData, + content: 'hey', + }); + + // must not be created new conversation, new message + expect(await Conversations.find().count()).toBe(1); + expect(await ConversationMessages.find().count()).toBe(2); + + // close converstaion + await Conversations.update({}, { $set: { status: CONVERSATION_STATUSES.CLOSED } }); + + // customer commented on closed converstaion =========== + await saveWebhookResponse.getOrCreateConversation({ + findSelector: filter, + status: CONVERSATION_STATUSES.NEW, + senderId, + facebookData, + conntet: 'hi again', + }); + + // must not be created new conversation, new message + expect(await Conversations.find().count()).toBe(1); + + // must be opened + conversation = await Conversations.findOne({ _id: conversation._id }); + expect(conversation.status).toBe(CONVERSATION_STATUSES.OPEN); + expect(await ConversationMessages.find().count()).toBe(3); + + // new post =========== + filter.postId = '34424242444242'; + + await saveWebhookResponse.getOrCreateConversation({ + findSelector: filter, + status: CONVERSATION_STATUSES.NEW, + senderId, + facebookData, + content: 'new sender hi', + }); + + // must be created new conversation, new message + expect(await Conversations.find().count()).toBe(2); + expect(await ConversationMessages.find().count()).toBe(4); + + // unwrap getOrCreateCustomer + saveWebhookResponse.getOrCreateCustomer.restore(); + }); +}); From 1583416c7671800f64d02095e756a8a22ba0a8e0 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 1 Nov 2017 16:36:17 +0800 Subject: [PATCH 127/318] Add facebook reply tests --- src/__tests__/social/facebook.reply.test.js | 111 ++++++++++++++++++++ src/social/facebook.js | 8 +- 2 files changed, 116 insertions(+), 3 deletions(-) create mode 100755 src/__tests__/social/facebook.reply.test.js diff --git a/src/__tests__/social/facebook.reply.test.js b/src/__tests__/social/facebook.reply.test.js new file mode 100755 index 000000000..3a7d8626d --- /dev/null +++ b/src/__tests__/social/facebook.reply.test.js @@ -0,0 +1,111 @@ +/* eslint-env jest */ + +import sinon from 'sinon'; +import { connect, disconnect } from '../../db/connection'; +import { graphRequest, facebookReply } from '../../social/facebook'; +import { Integrations, Conversations, ConversationMessages } from '../../db/models'; +import { integrationFactory, conversationFactory } from '../../db/factories'; +import { FACEBOOK_DATA_KINDS } from '../../data/constants'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('facebook integration: reply', () => { + const senderId = 2242424244; + let integration; + const pageId = '2252525525'; + + beforeEach(async () => { + // mock settings + process.env.FACEBOOK = JSON.stringify([ + { + id: 'id', + name: 'name', + accessToken: 'access_token', + }, + ]); + + // create integration + integration = await integrationFactory({ + facebookData: { + appId: 'id', + pageIds: [pageId], + }, + }); + + // mock get page access token + sinon.stub(graphRequest, 'get').callsFake(() => ({ + access_token: 'page_access_token', + })); + }); + + afterEach(async () => { + // unwraps the spy + graphRequest.get.restore(); + graphRequest.post.restore(); + + // clear previous data + await Conversations.remove({}); + await Integrations.remove({}); + await ConversationMessages.remove(); + }); + + it('messenger', async () => { + const conversation = await conversationFactory({ + integrationId: integration._id, + facebookData: { + kind: FACEBOOK_DATA_KINDS.MESSENGER, + pageId: pageId, + senderId: senderId, + }, + }); + + const text = 'to messenger'; + + // mock post messenger reply + const stub = sinon.stub(graphRequest, 'post').callsFake(() => {}); + + // reply + await facebookReply(conversation, text); + + // check + expect(stub.calledWith('me/messages', 'page_access_token')).toBe(true); + }); + + it('feed', async () => { + const conversation = await conversationFactory({ + integrationId: integration._id, + facebookData: { + kind: FACEBOOK_DATA_KINDS.FEED, + senderId: 'senderId', + pageId: 'pageId', + postId: 'postId', + }, + }); + + const text = 'comment'; + const messageId = '242424242'; + + // mock post messenger reply + const gpStub = sinon.stub(graphRequest, 'post').callsFake(() => ({ + id: 'commentId', + })); + + // mock message update + const mongoStub = sinon.stub(ConversationMessages, 'update').callsFake(() => {}); + + // reply + await facebookReply(conversation, text, messageId); + + // check graph request + expect(gpStub.calledWith('postId/comments', 'page_access_token')).toBe(true); + + // check mongo update + expect( + mongoStub.calledWith({ _id: messageId }, { $set: { 'facebookData.commentId': 'commentId' } }), + ).toBe(true); + + // unwrap stub + ConversationMessages.update.restore(); + }); +}); diff --git a/src/social/facebook.js b/src/social/facebook.js index eca0a1ef4..69f12861f 100755 --- a/src/social/facebook.js +++ b/src/social/facebook.js @@ -402,9 +402,11 @@ export const receiveWebhookResponse = async (app, data) => { export const facebookReply = async (conversation, text, messageId) => { const { FACEBOOK } = process.env; - const app = JSON.parse(FACEBOOK).find( - a => a.id === conversation.integration().facebookData.appId, - ); + const integration = await Integrations.findOne({ + _id: conversation.integrationId, + }); + + const app = JSON.parse(FACEBOOK).find(a => a.id === integration.facebookData.appId); // page access token const response = graphRequest.get( From 5fe2e41811bcf9a7d76da76c2893a7750ad99f36 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 1 Nov 2017 17:11:47 +0800 Subject: [PATCH 128/318] Add facebook save response tests --- .../social/facebook.saveResponse.test.js | 300 ++++++++++++++++++ src/db/models/Conversations.js | 50 ++- 2 files changed, 349 insertions(+), 1 deletion(-) create mode 100755 src/__tests__/social/facebook.saveResponse.test.js diff --git a/src/__tests__/social/facebook.saveResponse.test.js b/src/__tests__/social/facebook.saveResponse.test.js new file mode 100755 index 000000000..de4c0d7ae --- /dev/null +++ b/src/__tests__/social/facebook.saveResponse.test.js @@ -0,0 +1,300 @@ +/* eslint-env jest */ + +import sinon from 'sinon'; +import { connect, disconnect } from '../../db/connection'; +import { graphRequest, SaveWebhookResponse } from '../../social/facebook'; +import { Conversations, Customers, ConversationMessages } from '../../db/models'; +import { integrationFactory } from '../../db/factories'; +import { CONVERSATION_STATUSES, FACEBOOK_DATA_KINDS } from '../../data/constants'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('facebook integration: save webhook response', () => { + let senderId = 2242424244; + const pageId = '2252525525'; + const postId = '242422242424244'; + const recipientId = '242422242424244'; + + let saveWebhookResponse; + let integration; + + beforeEach(async () => { + integration = await integrationFactory({ + facebookData: { + appId: '242424242422', + pageIds: [pageId], + }, + }); + + sinon.stub(graphRequest, 'get').callsFake(path => { + // mock get page access token + if (path.includes('/?fields=access_token')) { + return { + access_token: '244242442442', + }; + } + + // mock get post object + if (path === postId) { + return { + id: postId, + }; + } + + // mock get user info + return { + name: 'Dombo Gombo', + }; + }); + + saveWebhookResponse = new SaveWebhookResponse('access_token', integration, {}); + }); + + afterEach(async () => { + graphRequest.get.restore(); // unwraps the spy + + // clear previous datas + await Conversations.remove({}); + await Customers.remove({}); + await ConversationMessages.remove({}); + }); + + it('via messenger event', async () => { + // first time ======================== + + expect(await Conversations.find().count()).toBe(0); // 0 conversations + expect(await Customers.find().count()).toBe(0); // 0 customers + expect(await ConversationMessages.find().count()).toBe(0); // 0 messages + + senderId = '2242424244'; + let messageText = 'from messenger'; + + const attachments = [ + { + type: 'image', + payload: { + url: 'attachment_url', + }, + }, + ]; + + // customer says from messenger via messenger + saveWebhookResponse.data = { + object: 'page', + entry: [ + { + id: pageId, + messaging: [ + { + sender: { id: senderId }, + recipient: { id: recipientId }, + message: { + text: messageText, + attachments, + }, + }, + ], + }, + ], + }; + await saveWebhookResponse.start(); + + expect(await Conversations.find().count()).toBe(1); // 1 conversation + expect(await Customers.find().count()).toBe(1); // 1 customer + expect(await ConversationMessages.find().count()).toBe(1); // 1 message + + let conversation = await Conversations.findOne(); + const customer = await Customers.findOne(); + const message = await ConversationMessages.findOne(); + + // check conversation field values + expect(conversation.integrationId).toBe(integration._id); + expect(conversation.customerId).toBe(customer._id); + expect(conversation.status).toBe(CONVERSATION_STATUSES.NEW); + expect(conversation.content).toBe(messageText); + expect(conversation.facebookData.kind).toBe(FACEBOOK_DATA_KINDS.MESSENGER); + expect(conversation.facebookData.senderId).toBe(senderId); + expect(conversation.facebookData.recipientId).toBe(recipientId); + expect(conversation.facebookData.pageId).toBe(pageId); + + // check customer field values + expect(customer.integrationId).toBe(integration._id); + expect(customer.name).toBe('Dombo Gombo'); // from mocked get info above + expect(customer.facebookData.id).toBe(senderId); + + // check message field values + expect(message.conversationId).toBe(conversation._id); + expect(message.customerId).toBe(customer._id); + expect(message.internal).toBe(false); + expect(message.content).toBe(messageText); + expect(message.attachments).toEqual([{ type: 'image', url: 'attachment_url' }]); + + // second time ======================== + + // customer says hi via messenger again + messageText = 'hi'; + + saveWebhookResponse.data = { + object: 'page', + entry: [ + { + id: pageId, + messaging: [ + { + sender: { id: senderId }, + recipient: { id: recipientId }, + + message: { + text: messageText, + }, + }, + ], + }, + ], + }; + await saveWebhookResponse.start(); + + // must not be created new conversation + expect(await Conversations.find().count()).toBe(1); + + // must not be created new customer + expect(await Customers.find().count()).toBe(1); + + // must be created new message + expect(await ConversationMessages.find().count()).toBe(2); + + // check conversation field updates + conversation = await Conversations.findOne(); + expect(conversation.readUserIds.length).toBe(0); + + const newMessage = await ConversationMessages.findOne({ _id: { $ne: message._id } }); + + // check message fields + expect(newMessage.conversationId).toBe(conversation._id); + expect(newMessage.customerId).toBe(customer._id); + expect(newMessage.internal).toBe(false); + expect(newMessage.content).toBe(messageText); + }); + + it('via feed event', async () => { + // first time ======================== + + expect(await Conversations.find().count()).toBe(0); // 0 conversations + expect(await Customers.find().count()).toBe(0); // 0 customers + expect(await ConversationMessages.find().count()).toBe(0); // 0 messages + + let messageText = 'wall post'; + const link = 'link_url'; + const commentId = '2424242422242424244'; + + // customer posted `wall post` on our wall + saveWebhookResponse.data = { + object: 'page', + entry: [ + { + id: pageId, + changes: [ + { + value: { + verb: 'add', + item: 'post', + post_id: postId, + comment_id: commentId, + sender_id: senderId, + message: messageText, + link, + }, + }, + ], + }, + ], + }; + await saveWebhookResponse.start(); + + expect(await Conversations.find().count()).toBe(1); // 1 conversation + expect(await Customers.find().count()).toBe(1); // 1 customer + expect(await ConversationMessages.find().count()).toBe(1); // 1 message + + let conversation = await Conversations.findOne(); + const customer = await Customers.findOne(); + const message = await ConversationMessages.findOne(); + + // check conversation field values + expect(conversation.integrationId).toBe(integration._id); + expect(conversation.customerId).toBe(customer._id); + expect(conversation.status).toBe(CONVERSATION_STATUSES.NEW); + expect(conversation.content).toBe(messageText); + expect(conversation.facebookData.kind).toBe(FACEBOOK_DATA_KINDS.FEED); + expect(conversation.facebookData.postId).toBe(postId); + expect(conversation.facebookData.pageId).toBe(pageId); + + // check customer field values + expect(customer.integrationId).toBe(integration._id); + expect(customer.name).toBe('Dombo Gombo'); // from mocked get info above + expect(customer.facebookData.id).toBe(senderId); + + // check message field values + expect(message.conversationId).toBe(conversation._id); + expect(message.customerId).toBe(customer._id); + expect(message.internal).toBe(false); + expect(message.content).toBe(messageText); + expect(message.facebookData.toJSON()).toEqual({ item: 'post', senderId, link }); + + // second time ======================== + + // customer commented hi on above post again + messageText = 'hi'; + + saveWebhookResponse.data = { + object: 'page', + entry: [ + { + id: pageId, + changes: [ + { + value: { + verb: 'add', + item: 'comment', + reaction_type: 'haha', + post_id: postId, + comment_id: commentId, + sender_id: senderId, + message: messageText, + }, + }, + ], + }, + ], + }; + await saveWebhookResponse.start(); + + // must not be created new conversation + expect(await Conversations.find().count()).toBe(1); + + // must not be created new customer + expect(await Customers.find().count()).toBe(1); + + // must be created new message + expect(await ConversationMessages.find().count()).toBe(2); + + // check conversation field updates + conversation = await Conversations.findOne(); + expect(conversation.readUserIds.length).toBe(0); + + const newMessage = await ConversationMessages.findOne({ _id: { $ne: message._id } }); + + // check message fields + expect(newMessage.conversationId).toBe(conversation._id); + expect(newMessage.customerId).toBe(customer._id); + expect(newMessage.internal).toBe(false); + expect(newMessage.content).toBe(messageText); + expect(newMessage.attachments).toBe(undefined); + + expect(newMessage.facebookData.toJSON()).toEqual({ + item: 'comment', + senderId, + reactionType: 'haha', + }); + }); +}); diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index b4df6d0e2..3870bbfef 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -76,6 +76,54 @@ const FacebookSchema = mongoose.Schema( { _id: false }, ); +const MessagesFacebookSchema = mongoose.Schema( + { + commentId: { + type: String, + optional: true, + }, + + // comment, reaction, etc ... + item: { + type: String, + optional: true, + }, + + // when share photo + photoId: { + type: String, + optional: true, + }, + + // when share video + videoId: { + type: String, + optional: true, + }, + + link: { + type: String, + optional: true, + }, + + reactionType: { + type: String, + optional: true, + }, + + senderId: { + type: String, + optional: true, + }, + + senderName: { + type: String, + optional: true, + }, + }, + { _id: false }, +); + // Conversation schema const ConversationSchema = mongoose.Schema({ _id: { type: String, unique: true, default: () => Random.id() }, @@ -353,7 +401,7 @@ const MessageSchema = mongoose.Schema({ isCustomerRead: Boolean, engageData: Object, formWidgetData: Object, - facebookData: FacebookSchema, + facebookData: MessagesFacebookSchema, }); class Message { From 887ed6138c7304df6040f5faac582291fa9ee5c4 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 1 Nov 2017 17:26:02 +0800 Subject: [PATCH 129/318] Fix some tests --- src/db/factories.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/db/factories.js b/src/db/factories.js index 30d258ebb..404a2041f 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -219,9 +219,10 @@ export const integrationFactory = async (params = {}) => { const doc = { name: faker.random.word(), kind, - brandId: Random.id(), + brandId: params.brandId || Random.id(), formId: Random.id(), messengerData: { welcomeMessage: 'welcome', notifyCustomer: true }, + twitterData: params.twitterData || {}, facebookData: params.facebookData || {}, formData: params.formData === 'form' From 4e09c51a6bbf001935892de1a5365d1b3a023a6b Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 1 Nov 2017 17:28:50 +0800 Subject: [PATCH 130/318] Fix form tests --- src/db/factories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/factories.js b/src/db/factories.js index 404a2041f..344f62f63 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -220,7 +220,7 @@ export const integrationFactory = async (params = {}) => { name: faker.random.word(), kind, brandId: params.brandId || Random.id(), - formId: Random.id(), + formId: params.formId || Random.id(), messengerData: { welcomeMessage: 'welcome', notifyCustomer: true }, twitterData: params.twitterData || {}, facebookData: params.facebookData || {}, From f05b738bdc53d3f521ddabc84b7d515ad2405d88 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 1 Nov 2017 17:35:29 +0800 Subject: [PATCH 131/318] Split conversation model --- src/db/models/ConversationMessages.js | 196 ++++++++++++++++++++++++++ src/db/models/Conversations.js | 193 +------------------------ src/db/models/Integrations.js | 4 +- src/db/models/index.js | 3 +- 4 files changed, 202 insertions(+), 194 deletions(-) create mode 100644 src/db/models/ConversationMessages.js diff --git a/src/db/models/ConversationMessages.js b/src/db/models/ConversationMessages.js new file mode 100644 index 000000000..b17de8a0e --- /dev/null +++ b/src/db/models/ConversationMessages.js @@ -0,0 +1,196 @@ +import strip from 'strip'; +import mongoose from 'mongoose'; +import Random from 'meteor-random'; +import { Conversations } from './'; + +const FacebookSchema = mongoose.Schema( + { + commentId: { + type: String, + optional: true, + }, + + // comment, reaction, etc ... + item: { + type: String, + optional: true, + }, + + // when share photo + photoId: { + type: String, + optional: true, + }, + + // when share video + videoId: { + type: String, + optional: true, + }, + + link: { + type: String, + optional: true, + }, + + reactionType: { + type: String, + optional: true, + }, + + senderId: { + type: String, + optional: true, + }, + + senderName: { + type: String, + optional: true, + }, + }, + { _id: false }, +); + +const MessageSchema = mongoose.Schema({ + _id: { type: String, unique: true, default: () => Random.id() }, + content: String, + attachments: Object, + mentionedUserIds: [String], + conversationId: String, + internal: Boolean, + customerId: String, + userId: String, + createdAt: Date, + isCustomerRead: Boolean, + engageData: Object, + formWidgetData: Object, + facebookData: FacebookSchema, +}); + +class Message { + /** + * Create a message + * @param {Object} messageObj object + * @return {Promise} Newly created message object + */ + static async createMessage(doc) { + const message = await this.create({ + ...doc, + createdAt: new Date(), + }); + + const messageCount = await this.find({ + conversationId: message.conversationId, + }).count(); + + await Conversations.update({ _id: message.conversationId }, { $set: { messageCount } }); + + // add created user to participators + await Conversations.addParticipatedUsers(message.conversationId, message.userId); + + // add mentioned users to participators + for (let userId of message.mentionedUserIds) { + await Conversations.addParticipatedUsers(message.conversationId, userId); + } + + return message; + } + + /** + * Create a conversation + * @param {Object} doc - conversation message fields + * @param {Object} user object + * @return {Promise} Newly created conversation object + */ + static async addMessage(doc, userId) { + const conversation = await Conversations.findOne({ _id: doc.conversationId }); + + if (!conversation) throw new Error(`Conversation not found with id ${doc.conversationId}`); + + // normalize content, attachments + const content = doc.content || ''; + const attachments = doc.attachments || []; + + doc.content = content; + doc.attachments = attachments; + + // if there is no attachments and no content then throw content required error + if (attachments.length === 0 && !strip(content)) throw new Error('Content is required'); + + // setting conversation's content to last message + await this.update({ _id: doc.conversationId }, { $set: { content } }); + + return this.createMessage({ ...doc, userId }); + } + + /** + * Remove a messages + * @param {Object} selector + * @return {Promise} Deleted messages info + */ + static async removeMessages(selector) { + const messages = await this.find(selector); + const result = await this.remove(selector); + + for (let message of messages) { + const messageCount = await Messages.find({ + conversationId: message.conversationId, + }).count(); + + await Conversations.update({ _id: message.conversationId }, { $set: { messageCount } }); + } + + return result; + } + + /** + * User's last non answered question + * @param {String} conversationId + * @return {Promise} message object + */ + static getNonAsnweredMessage(conversationId) { + return this.findOne({ + conversationId: conversationId, + customerId: { $exists: true }, + }).sort({ createdAt: -1 }); + } + + /** + * Get admin messages + * @param {String} conversationId + * @return {Promise} messages + */ + static getAdminMessages(conversationId) { + return this.find({ + conversationId: conversationId, + userId: { $exists: true }, + isCustomerRead: false, + + // exclude internal notes + internal: false, + }).sort({ createdAt: 1 }); + } + + /** + * Mark sent messages as read + * @param {String} conversationId + * @return {Promise} updated messages info + */ + static markSentAsReadMessages(conversationId) { + return this.update( + { + conversationId: conversationId, + userId: { $exists: true }, + isCustomerRead: { $exists: false }, + }, + { $set: { isCustomerRead: true } }, + { multi: true }, + ); + } +} + +MessageSchema.loadClass(Message); + +const Messages = mongoose.model('conversation_messages', MessageSchema); + +export default Messages; diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index 3870bbfef..0d384706c 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -1,5 +1,3 @@ -import strip from 'strip'; - import mongoose from 'mongoose'; import Random from 'meteor-random'; import { CONVERSATION_STATUSES, FACEBOOK_DATA_KINDS } from '../../data/constants'; @@ -76,54 +74,6 @@ const FacebookSchema = mongoose.Schema( { _id: false }, ); -const MessagesFacebookSchema = mongoose.Schema( - { - commentId: { - type: String, - optional: true, - }, - - // comment, reaction, etc ... - item: { - type: String, - optional: true, - }, - - // when share photo - photoId: { - type: String, - optional: true, - }, - - // when share video - videoId: { - type: String, - optional: true, - }, - - link: { - type: String, - optional: true, - }, - - reactionType: { - type: String, - optional: true, - }, - - senderId: { - type: String, - optional: true, - }, - - senderName: { - type: String, - optional: true, - }, - }, - { _id: false }, -); - // Conversation schema const ConversationSchema = mongoose.Schema({ _id: { type: String, unique: true, default: () => Random.id() }, @@ -386,146 +336,7 @@ class Conversation { } ConversationSchema.loadClass(Conversation); -export const Conversations = mongoose.model('conversations', ConversationSchema); - -const MessageSchema = mongoose.Schema({ - _id: { type: String, unique: true, default: () => Random.id() }, - content: String, - attachments: Object, - mentionedUserIds: [String], - conversationId: String, - internal: Boolean, - customerId: String, - userId: String, - createdAt: Date, - isCustomerRead: Boolean, - engageData: Object, - formWidgetData: Object, - facebookData: MessagesFacebookSchema, -}); - -class Message { - /** - * Create a message - * @param {Object} messageObj object - * @return {Promise} Newly created message object - */ - static async createMessage(doc) { - const message = await this.create({ - ...doc, - createdAt: new Date(), - }); - - const messageCount = await this.find({ - conversationId: message.conversationId, - }).count(); - - await Conversations.update({ _id: message.conversationId }, { $set: { messageCount } }); - - // add created user to participators - await Conversations.addParticipatedUsers(message.conversationId, message.userId); - - // add mentioned users to participators - for (let userId of message.mentionedUserIds) { - await Conversations.addParticipatedUsers(message.conversationId, userId); - } - - return message; - } - - /** - * Create a conversation - * @param {Object} doc - conversation message fields - * @param {Object} user object - * @return {Promise} Newly created conversation object - */ - static async addMessage(doc, userId) { - const conversation = await Conversations.findOne({ _id: doc.conversationId }); - - if (!conversation) throw new Error(`Conversation not found with id ${doc.conversationId}`); - - // normalize content, attachments - const content = doc.content || ''; - const attachments = doc.attachments || []; - - doc.content = content; - doc.attachments = attachments; - - // if there is no attachments and no content then throw content required error - if (attachments.length === 0 && !strip(content)) throw new Error('Content is required'); - - // setting conversation's content to last message - await this.update({ _id: doc.conversationId }, { $set: { content } }); - - return this.createMessage({ ...doc, userId }); - } - - /** - * Remove a messages - * @param {Object} selector - * @return {Promise} Deleted messages info - */ - static async removeMessages(selector) { - const messages = await this.find(selector); - const result = await this.remove(selector); - - for (let message of messages) { - const messageCount = await Messages.find({ - conversationId: message.conversationId, - }).count(); - - await Conversations.update({ _id: message.conversationId }, { $set: { messageCount } }); - } - - return result; - } - - /** - * User's last non answered question - * @param {String} conversationId - * @return {Promise} message object - */ - static getNonAsnweredMessage(conversationId) { - return this.findOne({ - conversationId: conversationId, - customerId: { $exists: true }, - }).sort({ createdAt: -1 }); - } - - /** - * Get admin messages - * @param {String} conversationId - * @return {Promise} messages - */ - static getAdminMessages(conversationId) { - return this.find({ - conversationId: conversationId, - userId: { $exists: true }, - isCustomerRead: false, - - // exclude internal notes - internal: false, - }).sort({ createdAt: 1 }); - } - - /** - * Mark sent messages as read - * @param {String} conversationId - * @return {Promise} updated messages info - */ - static markSentAsReadMessages(conversationId) { - return this.update( - { - conversationId: conversationId, - userId: { $exists: true }, - isCustomerRead: { $exists: false }, - }, - { $set: { isCustomerRead: true } }, - { multi: true }, - ); - } -} -MessageSchema.loadClass(Message); +const Conversations = mongoose.model('conversations', ConversationSchema); -export const Messages = mongoose.model('conversation_messages', MessageSchema); +export default Conversations; diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index 3709ed29e..93a5a16f3 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -1,7 +1,7 @@ import mongoose from 'mongoose'; import 'mongoose-type-email'; import Random from 'meteor-random'; -import { Messages, Conversations } from './Conversations'; +import { ConversationMessages, Conversations } from './'; import { Customers } from './'; import { KIND_CHOICES, @@ -330,7 +330,7 @@ class Integration { }); // Remove messages - await Messages.remove({ conversationId: { $in: conversationIds } }); + await ConversationMessages.remove({ conversationId: { $in: conversationIds } }); // Remove conversations await Conversations.remove({ integrationId: _id }); diff --git a/src/db/models/index.js b/src/db/models/index.js index 92206bfa7..469390f3b 100644 --- a/src/db/models/index.js +++ b/src/db/models/index.js @@ -12,7 +12,8 @@ import InternalNotes from './InternalNotes'; import Customers from './Customers'; import Companies from './Companies'; import Segments from './Segments'; -import { Conversations, Messages as ConversationMessages } from './Conversations'; +import Conversations from './Conversations'; +import ConversationMessages from './ConversationMessages'; import { KnowledgeBaseArticles, KnowledgeBaseCategories, From c60099a1701cda14144192c77aa7209e5285a96a Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 1 Nov 2017 18:18:59 +0800 Subject: [PATCH 132/318] Add some comments --- src/db/models/ConversationMessages.js | 2 +- src/social/facebook.js | 56 ++++++++++++++++----------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/db/models/ConversationMessages.js b/src/db/models/ConversationMessages.js index b17de8a0e..1da141694 100644 --- a/src/db/models/ConversationMessages.js +++ b/src/db/models/ConversationMessages.js @@ -97,7 +97,7 @@ class Message { } /** - * Create a conversation + * Create a conversation message * @param {Object} doc - conversation message fields * @param {Object} user object * @return {Promise} Newly created conversation object diff --git a/src/social/facebook.js b/src/social/facebook.js index 69f12861f..b9906d3ff 100755 --- a/src/social/facebook.js +++ b/src/social/facebook.js @@ -37,7 +37,9 @@ export const graphRequest = { }; /* - * get list of pages that authorized user owns + * Get list of pages that authorized user owns + * @param {String} accessToken - App access token + * @return {[Object]} - page list */ export const getPageList = accessToken => { const response = graphRequest.get('/me/accounts?limit=100', accessToken); @@ -49,8 +51,12 @@ export const getPageList = accessToken => { }; /* - * save webhook response + * Save webhook response * create conversation, customer, message using transmitted data + * + * @param {String} userAccessToken - User access token + * @param {Object} integration - Integration object + * @param {Object} data - Facebook webhook response */ export class SaveWebhookResponse { @@ -93,7 +99,7 @@ export class SaveWebhookResponse { } /* - * via page messenger + * Via page messenger */ async viaMessengerEvent(entry) { for (let messagingEvent of entry.messaging) { @@ -105,7 +111,7 @@ export class SaveWebhookResponse { } /* - * wall post + * Wall post */ async viaFeedEvent(entry) { for (let event of entry.changes) { @@ -115,7 +121,9 @@ export class SaveWebhookResponse { } /* - * common get or create conversation helper using both in messenger and feed + * Common get or create conversation helper using both in messenger and feed + * @param {Object} params - Parameters doc + * @return newly create message object */ async getOrCreateConversation(params) { // extract params @@ -150,20 +158,9 @@ export class SaveWebhookResponse { conversation = await Conversations.findOne({ _id: conversationId }); - // update conversation + // reopen conversation } else { - await Conversations.update( - { _id: conversation._id }, - { - $set: { - // reset read history - readUserIds: [], - - // if closed, reopen it - status: CONVERSATION_STATUSES.OPEN, - }, - }, - ); + conversation = await Conversations.reopen(conversation._id); } // create new message @@ -177,7 +174,8 @@ export class SaveWebhookResponse { } /* - * get or create new conversation by feed info + * Get or create new conversation by feed info + * @param {Object} value - Webhook response item */ async getOrCreateConversationByFeed(value) { const commentId = value.comment_id; @@ -277,7 +275,9 @@ export class SaveWebhookResponse { } /* - * get or create new conversation by page messenger + * Get or create new conversation by page messenger + * @param {Object} event - Webhook response item + * @return Newly created message object */ async getOrCreateConversationByMessenger(event) { const senderId = event.sender.id; @@ -323,7 +323,9 @@ export class SaveWebhookResponse { } /* - * get or create customer using facebook data + * Get or create customer using facebook data + * @param {String} fbUserId - Facebook user id + * @return Previous or newly created customer object */ async getOrCreateCustomer(fbUserId) { const integrationId = this.integration._id; @@ -358,6 +360,9 @@ export class SaveWebhookResponse { }); } + /* + * Create new message + */ async createMessage({ conversation, userId, content, attachments, facebookData }) { if (conversation) { // create new message @@ -378,7 +383,9 @@ export class SaveWebhookResponse { } /* - * receive per app webhook response + * Receive per app webhook response + * @param {Object} app - Apps configuration item from .env + * @param {Object} data - Webhook response */ export const receiveWebhookResponse = async (app, data) => { const selector = { @@ -397,7 +404,10 @@ export const receiveWebhookResponse = async (app, data) => { }; /* - * post reply to page conversation or comment to wall post + * Post reply to page conversation or comment to wall post + * @param {Object} conversation - Conversation object + * @param {Sting} text - Reply content + * @param {String} messageId - Conversation message id */ export const facebookReply = async (conversation, text, messageId) => { const { FACEBOOK } = process.env; From c23bf56480da638a047d66dac9d03fe48a6a288a Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 00:02:49 +0800 Subject: [PATCH 133/318] Test some invalid values; --- .../facebook.conversationByFeed.test.js | 65 +++++++++++++------ .../facebook.getOrCreateConversation.test.js | 10 ++- src/social/facebook.js | 12 ++-- 3 files changed, 61 insertions(+), 26 deletions(-) diff --git a/src/__tests__/social/facebook.conversationByFeed.test.js b/src/__tests__/social/facebook.conversationByFeed.test.js index 90582ae4b..a631f85d2 100755 --- a/src/__tests__/social/facebook.conversationByFeed.test.js +++ b/src/__tests__/social/facebook.conversationByFeed.test.js @@ -4,26 +4,13 @@ import sinon from 'sinon'; import { connect, disconnect } from '../../db/connection'; import { graphRequest, SaveWebhookResponse } from '../../social/facebook'; import { Conversations, ConversationMessages } from '../../db/models'; -import { integrationFactory } from '../../db/factories'; +import { integrationFactory, conversationMessageFactory } from '../../db/factories'; import { CONVERSATION_STATUSES } from '../../data/constants'; beforeAll(() => connect()); afterAll(() => disconnect()); describe('facebook integration: get or create conversation by feed info', () => { - beforeEach(() => { - // mock all requests - sinon.stub(graphRequest, 'get').callsFake(path => { - if (path.includes('/?fields=access_token')) { - return { - access_token: '244242442442', - }; - } - - return {}; - }); - }); - afterEach(async () => { // clear await Conversations.remove({}); @@ -53,13 +40,53 @@ describe('facebook integration: get or create conversation by feed info', () => // must be 0 conversations expect(await Conversations.find().count()).toBe(0); - await saveWebhookResponse.getOrCreateConversationByFeed({ - verb: 'add', - sender_id: senderId, - post_id: postId, - message: 'hi all', + const value = { sender_id: senderId }; + + // check invalid verb + value.verb = 'edit'; + expect(await saveWebhookResponse.getOrCreateConversationByFeed(value)).toBe(null); + + // ignore likes + value.verb = 'add'; + value.item = 'like'; + expect(await saveWebhookResponse.getOrCreateConversationByFeed(value)).toBe(null); + + // already saved comments ========== + await conversationMessageFactory({ facebookData: { commentId: 1 } }); + + value.item = null; + value.comment_id = 1; + + expect(await saveWebhookResponse.getOrCreateConversationByFeed(value)).toBe(null); + + // no message + await ConversationMessages.remove({}); + value.message = ''; + expect(await saveWebhookResponse.getOrCreateConversationByFeed(value)).toBe(null); + + // access token expired + value.link = 'link'; + sinon.stub(graphRequest, 'get').callsFake(() => 'Error processing https request'); + expect(await saveWebhookResponse.getOrCreateConversationByFeed(value)).toBe(null); + graphRequest.get.restore(); + + // successful ============== + // mock external requests + sinon.stub(graphRequest, 'get').callsFake(path => { + if (path.includes('/?fields=access_token')) { + return { + access_token: '244242442442', + }; + } + + return {}; }); + value.post_id = postId; + value.message = 'hi'; + + await saveWebhookResponse.getOrCreateConversationByFeed(value); + expect(await Conversations.find().count()).toBe(1); // 1 conversation const conversation = await Conversations.findOne(); diff --git a/src/__tests__/social/facebook.getOrCreateConversation.test.js b/src/__tests__/social/facebook.getOrCreateConversation.test.js index 38ee8382b..aeb1bd1e0 100644 --- a/src/__tests__/social/facebook.getOrCreateConversation.test.js +++ b/src/__tests__/social/facebook.getOrCreateConversation.test.js @@ -33,9 +33,17 @@ describe('facebook integration: get or create conversation', () => { const integration = await integrationFactory(); const saveWebhookResponse = new SaveWebhookResponse('access_token', integration, {}); + saveWebhookResponse.currentPageId = pageId; - // mock getOrCreateCustomer + // checking non exising page response ======= + saveWebhookResponse.data = { object: 'page', entry: [{}] }; + + expect(await saveWebhookResponse.start()).toBe(null); + + saveWebhookResponse.data = {}; + + // mock getOrCreateCustomer ========== sinon.stub(saveWebhookResponse, 'getOrCreateCustomer').callsFake(() => customerId); // check initial states diff --git a/src/social/facebook.js b/src/social/facebook.js index b9906d3ff..e46a0eb94 100755 --- a/src/social/facebook.js +++ b/src/social/facebook.js @@ -79,7 +79,7 @@ export class SaveWebhookResponse { for (let entry of data.entry) { // check receiving page is in integration's page list if (!integration.facebookData.pageIds.includes(entry.id)) { - return; + return null; } // set current page @@ -182,12 +182,12 @@ export class SaveWebhookResponse { // collect only added actions if (value.verb !== 'add') { - return; + return null; } // ignore duplicated action when like if (value.verb === 'add' && value.item === 'like') { - return; + return null; } // if this is already saved then ignore it @@ -195,7 +195,7 @@ export class SaveWebhookResponse { commentId && (await ConversationMessages.findOne({ 'facebookData.commentId': commentId })) ) { - return; + return null; } const senderName = value.sender_name; @@ -215,7 +215,7 @@ export class SaveWebhookResponse { // when situations like checkin, there will be no text and no link // if so ignore it if (!messageText) { - return; + return null; } // value.post_id is returning different value even though same post @@ -231,7 +231,7 @@ export class SaveWebhookResponse { // acess token expired if (response === 'Error processing https request') { - return; + return null; } // get post object From 51ea3eaf6ac4af644d2faca36fc95cbb726537a6 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 00:27:33 +0800 Subject: [PATCH 134/318] Add facebook receiveWebHook response test --- src/__tests__/social/facebook.test.js | 24 ++++++++++++++++++++++++ src/social/facebook.js | 1 + 2 files changed, 25 insertions(+) create mode 100644 src/__tests__/social/facebook.test.js diff --git a/src/__tests__/social/facebook.test.js b/src/__tests__/social/facebook.test.js new file mode 100644 index 000000000..90e4eda24 --- /dev/null +++ b/src/__tests__/social/facebook.test.js @@ -0,0 +1,24 @@ +/* eslint-env jest */ + +import { connect, disconnect } from '../../db/connection'; +import { receiveWebhookResponse } from '../../social/facebook'; +import { Integrations } from '../../db/models'; +import { integrationFactory } from '../../db/factories'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('facebook integration common tests', () => { + afterEach(async () => { + // clear + await Integrations.remove({}); + }); + + it('receive web hook response', async () => { + const app = { id: 1 }; + + await integrationFactory({ kind: 'facebook', facebookData: { appId: app.id } }); + + await receiveWebhookResponse(app, {}); + }); +}); diff --git a/src/social/facebook.js b/src/social/facebook.js index e46a0eb94..baa90ae7e 100755 --- a/src/social/facebook.js +++ b/src/social/facebook.js @@ -433,6 +433,7 @@ export const facebookReply = async (conversation, text, messageId) => { recipient: { id: conversation.facebookData.senderId }, message: { text }, }, + /* istanbul ignore next */ () => {}, ); } From 0ca5f2deaff2fcfdffb190330c44a5e0b836103f Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 00:31:37 +0800 Subject: [PATCH 135/318] Add get page list test --- src/__tests__/social/facebook.test.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/__tests__/social/facebook.test.js b/src/__tests__/social/facebook.test.js index 90e4eda24..064d466f2 100644 --- a/src/__tests__/social/facebook.test.js +++ b/src/__tests__/social/facebook.test.js @@ -1,7 +1,8 @@ /* eslint-env jest */ +import sinon from 'sinon'; import { connect, disconnect } from '../../db/connection'; -import { receiveWebhookResponse } from '../../social/facebook'; +import { graphRequest, getPageList, receiveWebhookResponse } from '../../social/facebook'; import { Integrations } from '../../db/models'; import { integrationFactory } from '../../db/factories'; @@ -9,9 +10,17 @@ beforeAll(() => connect()); afterAll(() => disconnect()); describe('facebook integration common tests', () => { + const pages = [{ id: '1', name: 'page1' }]; + + beforeEach(() => { + // mock all requests + sinon.stub(graphRequest, 'get').callsFake(() => ({ data: pages })); + }); + afterEach(async () => { // clear await Integrations.remove({}); + graphRequest.get.restore(); // unwraps the spy }); it('receive web hook response', async () => { @@ -21,4 +30,8 @@ describe('facebook integration common tests', () => { await receiveWebhookResponse(app, {}); }); + + it('get page list', async () => { + expect(getPageList()).toEqual(pages); + }); }); From 090d32bf1eebe1ccad6ae65b1ec60f26ceafd828 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 11:26:38 +0800 Subject: [PATCH 136/318] Add facebook tracker --- .../facebook.conversationByFeed.test.js | 3 +- .../facebook.getOrCreateConversation.test.js | 3 +- src/__tests__/social/facebook.reply.test.js | 3 +- .../social/facebook.saveResponse.test.js | 3 +- src/__tests__/social/facebook.test.js | 22 +++++++++----- src/social/facebook.js | 30 +------------------ src/social/facebookTracker.js | 30 +++++++++++++++++++ 7 files changed, 54 insertions(+), 40 deletions(-) create mode 100644 src/social/facebookTracker.js diff --git a/src/__tests__/social/facebook.conversationByFeed.test.js b/src/__tests__/social/facebook.conversationByFeed.test.js index a631f85d2..396b6a325 100755 --- a/src/__tests__/social/facebook.conversationByFeed.test.js +++ b/src/__tests__/social/facebook.conversationByFeed.test.js @@ -2,7 +2,8 @@ import sinon from 'sinon'; import { connect, disconnect } from '../../db/connection'; -import { graphRequest, SaveWebhookResponse } from '../../social/facebook'; +import { SaveWebhookResponse } from '../../social/facebook'; +import { graphRequest } from '../../social/facebookTracker'; import { Conversations, ConversationMessages } from '../../db/models'; import { integrationFactory, conversationMessageFactory } from '../../db/factories'; import { CONVERSATION_STATUSES } from '../../data/constants'; diff --git a/src/__tests__/social/facebook.getOrCreateConversation.test.js b/src/__tests__/social/facebook.getOrCreateConversation.test.js index aeb1bd1e0..ccd78a4f4 100644 --- a/src/__tests__/social/facebook.getOrCreateConversation.test.js +++ b/src/__tests__/social/facebook.getOrCreateConversation.test.js @@ -2,7 +2,8 @@ import sinon from 'sinon'; import { connect, disconnect } from '../../db/connection'; -import { graphRequest, SaveWebhookResponse } from '../../social/facebook'; +import { SaveWebhookResponse } from '../../social/facebook'; +import { graphRequest } from '../../social/facebookTracker'; import { Conversations, ConversationMessages } from '../../db/models'; import { integrationFactory, customerFactory } from '../../db/factories'; import { CONVERSATION_STATUSES, FACEBOOK_DATA_KINDS } from '../../data/constants'; diff --git a/src/__tests__/social/facebook.reply.test.js b/src/__tests__/social/facebook.reply.test.js index 3a7d8626d..49ff58686 100755 --- a/src/__tests__/social/facebook.reply.test.js +++ b/src/__tests__/social/facebook.reply.test.js @@ -2,7 +2,8 @@ import sinon from 'sinon'; import { connect, disconnect } from '../../db/connection'; -import { graphRequest, facebookReply } from '../../social/facebook'; +import { facebookReply } from '../../social/facebook'; +import { graphRequest } from '../../social/facebookTracker'; import { Integrations, Conversations, ConversationMessages } from '../../db/models'; import { integrationFactory, conversationFactory } from '../../db/factories'; import { FACEBOOK_DATA_KINDS } from '../../data/constants'; diff --git a/src/__tests__/social/facebook.saveResponse.test.js b/src/__tests__/social/facebook.saveResponse.test.js index de4c0d7ae..4a54a2869 100755 --- a/src/__tests__/social/facebook.saveResponse.test.js +++ b/src/__tests__/social/facebook.saveResponse.test.js @@ -2,7 +2,8 @@ import sinon from 'sinon'; import { connect, disconnect } from '../../db/connection'; -import { graphRequest, SaveWebhookResponse } from '../../social/facebook'; +import { SaveWebhookResponse } from '../../social/facebook'; +import { graphRequest } from '../../social/facebookTracker'; import { Conversations, Customers, ConversationMessages } from '../../db/models'; import { integrationFactory } from '../../db/factories'; import { CONVERSATION_STATUSES, FACEBOOK_DATA_KINDS } from '../../data/constants'; diff --git a/src/__tests__/social/facebook.test.js b/src/__tests__/social/facebook.test.js index 064d466f2..b43ab7556 100644 --- a/src/__tests__/social/facebook.test.js +++ b/src/__tests__/social/facebook.test.js @@ -2,7 +2,8 @@ import sinon from 'sinon'; import { connect, disconnect } from '../../db/connection'; -import { graphRequest, getPageList, receiveWebhookResponse } from '../../social/facebook'; +import { getPageList, receiveWebhookResponse } from '../../social/facebook'; +import { graphRequest } from '../../social/facebookTracker'; import { Integrations } from '../../db/models'; import { integrationFactory } from '../../db/factories'; @@ -12,15 +13,9 @@ afterAll(() => disconnect()); describe('facebook integration common tests', () => { const pages = [{ id: '1', name: 'page1' }]; - beforeEach(() => { - // mock all requests - sinon.stub(graphRequest, 'get').callsFake(() => ({ data: pages })); - }); - afterEach(async () => { // clear await Integrations.remove({}); - graphRequest.get.restore(); // unwraps the spy }); it('receive web hook response', async () => { @@ -32,6 +27,19 @@ describe('facebook integration common tests', () => { }); it('get page list', async () => { + sinon.stub(graphRequest, 'get').callsFake(() => ({ data: pages })); + expect(getPageList()).toEqual(pages); + + graphRequest.get.restore(); // unwraps the spy + }); + + it('graph request', async () => { + sinon.stub(graphRequest, 'base').callsFake(() => {}); + + graphRequest.get(); + graphRequest.post(); + + graphRequest.base.restore(); // unwraps the spy }); }); diff --git a/src/social/facebook.js b/src/social/facebook.js index baa90ae7e..444700682 100755 --- a/src/social/facebook.js +++ b/src/social/facebook.js @@ -1,4 +1,3 @@ -import graph from 'fbgraph'; import { Integrations, Conversations, ConversationMessages, Customers } from '../db/models'; import { @@ -7,34 +6,7 @@ import { FACEBOOK_DATA_KINDS, } from '../data/constants'; -/* - * Common graph api request wrapper - * catchs auth token or other type of exceptions - */ -export const graphRequest = { - base(method, path, accessToken, ...otherParams) { - // set access token - graph.setAccessToken(accessToken); - - try { - // TODO - return graph[method](otherParams); - - // catch session expired or some other error - } catch (e) { - console.log(e.message); // eslint-disable-line no-console - return e.message; - } - }, - - get(...args) { - return this.base('get', ...args); - }, - - post(...args) { - return this.base('post', ...args); - }, -}; +import { graphRequest } from './facebookTracker'; /* * Get list of pages that authorized user owns diff --git a/src/social/facebookTracker.js b/src/social/facebookTracker.js new file mode 100644 index 000000000..2d9d31b52 --- /dev/null +++ b/src/social/facebookTracker.js @@ -0,0 +1,30 @@ +import graph from 'fbgraph'; + +/* + * Common graph api request wrapper + * catchs auth token or other type of exceptions + */ +export const graphRequest = { + base(method, path, accessToken, ...otherParams) { + // set access token + graph.setAccessToken(accessToken); + + try { + // TODO + return graph[method](otherParams); + + // catch session expired or some other error + } catch (e) { + console.log(e.message); // eslint-disable-line no-console + return e.message; + } + }, + + get(...args) { + return this.base('get', ...args); + }, + + post(...args) { + return this.base('post', ...args); + }, +}; From e000bb161a17f16ec8996020a81ae98e59345c1e Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 12:56:35 +0800 Subject: [PATCH 137/318] Add createUser, updateUser --- src/__tests__/userDb.test.js | 82 ++++++++++++++++++++++++++++++++ src/data/constants.js | 5 ++ src/db/factories.js | 14 +++--- src/db/models/Users.js | 91 +++++++++++++++++++++++++++++++++++- 4 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 src/__tests__/userDb.test.js diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js new file mode 100644 index 000000000..4e026311c --- /dev/null +++ b/src/__tests__/userDb.test.js @@ -0,0 +1,82 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { Users } from '../db/models'; +import { userFactory } from '../db/factories'; +import bcrypt from 'bcrypt'; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); + +describe('User db utils', () => { + let _user; + + beforeEach(async () => { + // Creating test data + _user = await userFactory({ email: 'info@erxes.io', isOwner: true }); + }); + + afterEach(async () => { + // Clearing test data + await Users.remove({}); + }); + + test('Create user', async () => { + const testPassword = 'test'; + + const userObj = await Users.createUser({ + ..._user._doc, + details: _user._doc.details, + password: testPassword, + }); + + expect(userObj).toBeDefined(); + expect(userObj.username).toBe(_user.username); + expect(userObj.email).toBe(_user.email); + expect(userObj.role).toBe(_user.role); + expect(bcrypt.compare(testPassword, userObj.password)).toBeTruthy(); + expect(userObj.details.position).toBe(_user.details.position); + expect(userObj.details.twitterUsername).toBe(_user.details.twitterUsername); + expect(userObj.details.fullName).toBe(_user.details.fullName); + expect(userObj.details.avatar).toBe(_user.details.avatar); + }); + + test('Update user: owner required', async () => { + const user = await userFactory(); + + expect.assertions(1); + + try { + await Users.updateUser(user._id, {}); + } catch (e) { + expect(e.message).toBe('Permission denied'); + } + }); + + test('Update user', async () => { + const updateDoc = await userFactory(); + + const testPassword = 'updatedPass'; + + // try using exisiting one + await Users.updateUser(_user._id, { + email: updateDoc.email, + username: updateDoc.username, + password: testPassword, + details: updateDoc._doc.details, + }); + + const userObj = await Users.findOne({ _id: _user._id }); + + expect(userObj.username).toBe(updateDoc.username); + expect(userObj.email).toBe(updateDoc.email); + expect(userObj.role).toBe(userObj.role); + expect(bcrypt.compare(testPassword, userObj.password)).toBeTruthy(); + expect(userObj.details.position).toBe(updateDoc.details.position); + expect(userObj.details.twitterUsername).toBe(updateDoc.details.twitterUsername); + expect(userObj.details.fullName).toBe(updateDoc.details.fullName); + expect(userObj.details.avatar).toBe(updateDoc.details.avatar); + }); +}); diff --git a/src/data/constants.js b/src/data/constants.js index 66f785af9..15c26fbca 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -150,3 +150,8 @@ export const SEGMENT_CONTENT_TYPES = { COMPANY: 'company', ALL_LIST: ['customer', 'company'], }; + +export const ROLES = { + ADMIN: 'admin', + CONTRIBUTOR: 'contributor', +}; diff --git a/src/db/factories.js b/src/db/factories.js index 344f62f63..6de4fbbff 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -25,16 +25,16 @@ import { export const userFactory = (params = {}) => { const user = new Users({ - username: params.username || faker.random.word(), + username: params.username || faker.internet.userName(), details: { fullName: params.fullName || faker.random.word(), + avatar: params.avatar || faker.image.imageUrl(), + twitterUsername: params.twitterUsername || faker.internet.userName(), + position: params.position || 'admin', }, - emails: [ - { - address: params.email || faker.internet.email(), - verified: true, - }, - ], + email: params.email || faker.internet.email(), + role: params.role || 'contributor', + isOwner: params.isOwner || false, }); return user.save(); diff --git a/src/db/models/Users.js b/src/db/models/Users.js index e3e8d3e16..f94c383c1 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -1,6 +1,33 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +import bcrypt from 'bcrypt'; +import { ROLES } from '../../data/constants'; +const SALT_WORK_FACTOR = 10; + +// Detail schema +const DetailSchema = mongoose.Schema( + { + avatar: String, + fullName: String, + position: String, + twitterUsername: String, + + // channels to invite + channelIds: { + type: [String], + optional: true, + }, + + signatures: { + brandId: String, + signature: String, + }, + }, + { _id: false }, +); + +// User schema const UserSchema = mongoose.Schema({ _id: { type: String, @@ -11,10 +38,70 @@ const UserSchema = mongoose.Schema({ password: String, resetPasswordToken: String, resetPasswordExpires: Date, - details: Object, - email: String, + role: { + type: String, + enum: [ROLES.ADMIN, ROLES.CONTRIBUTOR], + }, + details: DetailSchema, + isOwner: Boolean, + email: { + type: String, + lowercase: true, + unique: true, + match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 'Please fill a valid email address'], + }, }); +class User { + /** + * Create new user + * @param {Object} doc - user fields + * @return {Promise} newly created user object + */ + static async createUser({ username, email, password, role, details }) { + return this.create({ + username, + email, + role, + details, + // hash password + password: await this.generatePassword(password), + }); + } + + /** + * Update user information + * @param {String} userId + * @param {Object} doc - user fields + * @return {Promise} updated user info + */ + static async updateUser(_id, { username, email, password, role, details }) { + const user = await Users.findOne({ _id }); + + const doc = { username, email, password, role, details }; + + // change password + if (password) { + doc.password = await this.generatePassword(password); + } + + // only owner allowed to edit + if (!user.isOwner) { + throw new Error('Permission denied'); + } + + await this.update({ _id }, { $set: doc }); + + return this.findOne({ _id }); + } + + static generatePassword(password) { + return bcrypt.hash(password, SALT_WORK_FACTOR); + } +} + +UserSchema.loadClass(User); + const Users = mongoose.model('users', UserSchema); export default Users; From 8724f2b569af5d2523080e8b342b0dfbe58254f0 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 13:05:27 +0800 Subject: [PATCH 138/318] Add remove user --- src/__tests__/userDb.test.js | 21 ++++++++------------- src/db/models/Users.js | 21 ++++++++++++++------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index 4e026311c..315ff560f 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -15,7 +15,7 @@ describe('User db utils', () => { beforeEach(async () => { // Creating test data - _user = await userFactory({ email: 'info@erxes.io', isOwner: true }); + _user = await userFactory({ email: 'info@erxes.io' }); }); afterEach(async () => { @@ -43,18 +43,6 @@ describe('User db utils', () => { expect(userObj.details.avatar).toBe(_user.details.avatar); }); - test('Update user: owner required', async () => { - const user = await userFactory(); - - expect.assertions(1); - - try { - await Users.updateUser(user._id, {}); - } catch (e) { - expect(e.message).toBe('Permission denied'); - } - }); - test('Update user', async () => { const updateDoc = await userFactory(); @@ -79,4 +67,11 @@ describe('User db utils', () => { expect(userObj.details.fullName).toBe(updateDoc.details.fullName); expect(userObj.details.avatar).toBe(updateDoc.details.avatar); }); + + test('Remove user', async () => { + await Users.removeUser(_user._id); + + // ensure removed + expect(await Users.find().count()).toBe(0); + }); }); diff --git a/src/db/models/Users.js b/src/db/models/Users.js index f94c383c1..adb2d5613 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -76,8 +76,6 @@ class User { * @return {Promise} updated user info */ static async updateUser(_id, { username, email, password, role, details }) { - const user = await Users.findOne({ _id }); - const doc = { username, email, password, role, details }; // change password @@ -85,16 +83,25 @@ class User { doc.password = await this.generatePassword(password); } - // only owner allowed to edit - if (!user.isOwner) { - throw new Error('Permission denied'); - } - await this.update({ _id }, { $set: doc }); return this.findOne({ _id }); } + /* + * Remove user + * @param {String} _id - User id + * @return {Promise} - remove method response + */ + static async removeUser(_id) { + return Users.remove({ _id }); + } + + /* + * Generates new password hash using plan text password + * @param {String} password - Plan text password + * @return hashed password + */ static generatePassword(password) { return bcrypt.hash(password, SALT_WORK_FACTOR); } From 9aa4361f2916783e2a905948f55f70b2ecbd768c Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 13:59:43 +0800 Subject: [PATCH 139/318] Add editProfile --- .../facebook.conversationByFeed.test.js | 2 +- .../facebook.getOrCreateConversation.test.js | 2 +- src/__tests__/social/facebook.reply.test.js | 4 +- .../social/facebook.saveResponse.test.js | 4 +- src/__tests__/social/facebook.test.js | 6 +-- src/__tests__/social/twitter.test.js | 12 +++--- src/__tests__/social/twitterTracker.test.js | 6 +-- src/__tests__/userDb.test.js | 40 +++++++++++++++++-- src/db/models/Users.js | 36 ++++++++++++++--- 9 files changed, 84 insertions(+), 28 deletions(-) diff --git a/src/__tests__/social/facebook.conversationByFeed.test.js b/src/__tests__/social/facebook.conversationByFeed.test.js index 396b6a325..db2a036b1 100755 --- a/src/__tests__/social/facebook.conversationByFeed.test.js +++ b/src/__tests__/social/facebook.conversationByFeed.test.js @@ -20,7 +20,7 @@ describe('facebook integration: get or create conversation by feed info', () => graphRequest.get.restore(); // unwraps the spy }); - it('admin posts', async () => { + test('admin posts', async () => { const senderId = 'DFDFDEREREEFFFD'; const postId = 'DFJDFJDIF'; diff --git a/src/__tests__/social/facebook.getOrCreateConversation.test.js b/src/__tests__/social/facebook.getOrCreateConversation.test.js index ccd78a4f4..e788668c5 100644 --- a/src/__tests__/social/facebook.getOrCreateConversation.test.js +++ b/src/__tests__/social/facebook.getOrCreateConversation.test.js @@ -28,7 +28,7 @@ describe('facebook integration: get or create conversation', () => { await ConversationMessages.remove({}); }); - it('get or create conversation', async () => { + test('get or create conversation', async () => { const postId = '32242442442'; const customerId = await customerFactory(); const integration = await integrationFactory(); diff --git a/src/__tests__/social/facebook.reply.test.js b/src/__tests__/social/facebook.reply.test.js index 49ff58686..0f6a76e6e 100755 --- a/src/__tests__/social/facebook.reply.test.js +++ b/src/__tests__/social/facebook.reply.test.js @@ -51,7 +51,7 @@ describe('facebook integration: reply', () => { await ConversationMessages.remove(); }); - it('messenger', async () => { + test('messenger', async () => { const conversation = await conversationFactory({ integrationId: integration._id, facebookData: { @@ -73,7 +73,7 @@ describe('facebook integration: reply', () => { expect(stub.calledWith('me/messages', 'page_access_token')).toBe(true); }); - it('feed', async () => { + test('feed', async () => { const conversation = await conversationFactory({ integrationId: integration._id, facebookData: { diff --git a/src/__tests__/social/facebook.saveResponse.test.js b/src/__tests__/social/facebook.saveResponse.test.js index 4a54a2869..9e03a3caa 100755 --- a/src/__tests__/social/facebook.saveResponse.test.js +++ b/src/__tests__/social/facebook.saveResponse.test.js @@ -61,7 +61,7 @@ describe('facebook integration: save webhook response', () => { await ConversationMessages.remove({}); }); - it('via messenger event', async () => { + test('via messenger event', async () => { // first time ======================== expect(await Conversations.find().count()).toBe(0); // 0 conversations @@ -178,7 +178,7 @@ describe('facebook integration: save webhook response', () => { expect(newMessage.content).toBe(messageText); }); - it('via feed event', async () => { + test('via feed event', async () => { // first time ======================== expect(await Conversations.find().count()).toBe(0); // 0 conversations diff --git a/src/__tests__/social/facebook.test.js b/src/__tests__/social/facebook.test.js index b43ab7556..2f1b677d3 100644 --- a/src/__tests__/social/facebook.test.js +++ b/src/__tests__/social/facebook.test.js @@ -18,7 +18,7 @@ describe('facebook integration common tests', () => { await Integrations.remove({}); }); - it('receive web hook response', async () => { + test('receive web hook response', async () => { const app = { id: 1 }; await integrationFactory({ kind: 'facebook', facebookData: { appId: app.id } }); @@ -26,7 +26,7 @@ describe('facebook integration common tests', () => { await receiveWebhookResponse(app, {}); }); - it('get page list', async () => { + test('get page list', async () => { sinon.stub(graphRequest, 'get').callsFake(() => ({ data: pages })); expect(getPageList()).toEqual(pages); @@ -34,7 +34,7 @@ describe('facebook integration common tests', () => { graphRequest.get.restore(); // unwraps the spy }); - it('graph request', async () => { + test('graph request', async () => { sinon.stub(graphRequest, 'base').callsFake(() => {}); graphRequest.get(); diff --git a/src/__tests__/social/twitter.test.js b/src/__tests__/social/twitter.test.js index cd16fd9ec..90a42872e 100755 --- a/src/__tests__/social/twitter.test.js +++ b/src/__tests__/social/twitter.test.js @@ -40,7 +40,7 @@ describe('twitter integration', () => { await Customers.remove({}); }); - it('common', async () => { + test('common', async () => { const tweetId = 2424244244; // create conversation @@ -71,7 +71,7 @@ describe('twitter integration', () => { expect(conversation.status).toEqual(CONVERSATION_STATUSES.OPEN); }); - it('direct message', async () => { + test('direct message', async () => { const senderId = 2424424242; const recipientId = 92442424424242; @@ -149,7 +149,7 @@ describe('twitter integration', () => { await Customers.remove({}); }); - it('direct message', async () => { + test('direct message', async () => { const text = 'reply'; const senderId = 242424242; @@ -178,7 +178,7 @@ describe('twitter integration', () => { ).toBe(true); }); - it('tweet', async () => { + test('tweet', async () => { const text = 'reply'; const tweetIdStr = '242424242'; const screenName = 'test'; @@ -222,7 +222,7 @@ describe('twitter integration', () => { await Customers.remove({}); }); - it('mention', async () => { + test('mention', async () => { let tweetText = '@test hi'; const tweetId = 242424242424; const tweetIdStr = '242424242424'; @@ -315,7 +315,7 @@ describe('twitter integration', () => { expect(newMessage.content).toBe(tweetText); }); - it('direct message', async () => { + test('direct message', async () => { // try using non existing integration expect(await getOrCreateDirectMessageConversation({}, { _id: 'dffdfd' })).toBe(null); diff --git a/src/__tests__/social/twitterTracker.test.js b/src/__tests__/social/twitterTracker.test.js index 9e4927cd7..98c0b1331 100644 --- a/src/__tests__/social/twitterTracker.test.js +++ b/src/__tests__/social/twitterTracker.test.js @@ -37,7 +37,7 @@ describe('twitter integration tracker', () => { await ConversationMessages.remove({}); }); - it('check delete integration', async () => { + test('check delete integration', async () => { const response = await receiveTimeLineResponse({ _id: 'DFAFDFSD', twitterData: {}, @@ -46,7 +46,7 @@ describe('twitter integration tracker', () => { expect(response).toBe(null); }); - it('receive reply', async () => { + test('receive reply', async () => { // non existing conversation ========= await receiveTimeLineResponse(_integration, data); expect(await ConversationMessages.count()).toBe(0); @@ -59,7 +59,7 @@ describe('twitter integration tracker', () => { expect(await ConversationMessages.count()).toBe(1); }); - it('user mentions', async () => { + test('user mentions', async () => { data.in_reply_to_status_id = null; data.entities.user_mentions = [{ id: 1 }]; diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index 315ff560f..1fe163362 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -23,12 +23,25 @@ describe('User db utils', () => { await Users.remove({}); }); + test('Create user: twitter handler duplication', async () => { + expect.assertions(1); + + try { + await Users.createUser({ + password: 'password', + details: { twitterUsername: _user.details.twitterUsername }, + }); + } catch (e) { + expect(e.message).toBe('Duplicated twitter username'); + } + }); + test('Create user', async () => { const testPassword = 'test'; const userObj = await Users.createUser({ ..._user._doc, - details: _user._doc.details, + details: { ..._user.details.toJSON(), twitterUsername: 'twitter' }, password: testPassword, }); @@ -38,7 +51,7 @@ describe('User db utils', () => { expect(userObj.role).toBe(_user.role); expect(bcrypt.compare(testPassword, userObj.password)).toBeTruthy(); expect(userObj.details.position).toBe(_user.details.position); - expect(userObj.details.twitterUsername).toBe(_user.details.twitterUsername); + expect(userObj.details.twitterUsername).toBe('twitter'); expect(userObj.details.fullName).toBe(_user.details.fullName); expect(userObj.details.avatar).toBe(_user.details.avatar); }); @@ -53,7 +66,7 @@ describe('User db utils', () => { email: updateDoc.email, username: updateDoc.username, password: testPassword, - details: updateDoc._doc.details, + details: { ...updateDoc._doc.details.toJSON(), twitterUsername: 'tw' }, }); const userObj = await Users.findOne({ _id: _user._id }); @@ -63,7 +76,7 @@ describe('User db utils', () => { expect(userObj.role).toBe(userObj.role); expect(bcrypt.compare(testPassword, userObj.password)).toBeTruthy(); expect(userObj.details.position).toBe(updateDoc.details.position); - expect(userObj.details.twitterUsername).toBe(updateDoc.details.twitterUsername); + expect(userObj.details.twitterUsername).toBe('tw'); expect(userObj.details.fullName).toBe(updateDoc.details.fullName); expect(userObj.details.avatar).toBe(updateDoc.details.avatar); }); @@ -74,4 +87,23 @@ describe('User db utils', () => { // ensure removed expect(await Users.find().count()).toBe(0); }); + + test('Edit profile', async () => { + const updateDoc = await userFactory(); + + await Users.editProfile(_user._id, { + email: updateDoc.email, + username: updateDoc.username, + details: updateDoc._doc.details, + }); + + const userObj = await Users.findOne({ _id: _user._id }); + + expect(userObj.username).toBe(updateDoc.username); + expect(userObj.email).toBe(updateDoc.email); + expect(userObj.details.position).toBe(updateDoc.details.position); + expect(userObj.details.twitterUsername).toBe(updateDoc.details.twitterUsername); + expect(userObj.details.fullName).toBe(updateDoc.details.fullName); + expect(userObj.details.avatar).toBe(updateDoc.details.avatar); + }); }); diff --git a/src/db/models/Users.js b/src/db/models/Users.js index adb2d5613..deb4da422 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -13,12 +13,6 @@ const DetailSchema = mongoose.Schema( position: String, twitterUsername: String, - // channels to invite - channelIds: { - type: [String], - optional: true, - }, - signatures: { brandId: String, signature: String, @@ -59,6 +53,8 @@ class User { * @return {Promise} newly created user object */ static async createUser({ username, email, password, role, details }) { + await this.checkDuplications({ twitterUsername: details.twitterUsername }); + return this.create({ username, email, @@ -76,6 +72,8 @@ class User { * @return {Promise} updated user info */ static async updateUser(_id, { username, email, password, role, details }) { + await this.checkDuplications({ twitterUsername: details.twitterUsername }); + const doc = { username, email, password, role, details }; // change password @@ -88,6 +86,18 @@ class User { return this.findOne({ _id }); } + /* + * Update user profile + * @param {String} _id - User id + * @param {Object} doc - User profile information + * @return {Promise} - Updated user + */ + static async editProfile(_id, { username, email, details }) { + await this.update({ _id }, { $set: { username, email, details } }); + + return this.findOne({ _id }); + } + /* * Remove user * @param {String} _id - User id @@ -97,6 +107,20 @@ class User { return Users.remove({ _id }); } + /* + * Check duplications + */ + static async checkDuplications({ userId, twitterUsername }) { + const previousEntry = await Users.findOne({ + _id: { $ne: userId }, + 'details.twitterUsername': twitterUsername, + }); + + if (previousEntry) { + throw new Error('Duplicated twitter username'); + } + } + /* * Generates new password hash using plan text password * @param {String} password - Plan text password From c6fb7afa12508f70787dcf168f893fb56aa61077 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 14:20:30 +0800 Subject: [PATCH 140/318] Add configEmailSignatures --- src/__tests__/userDb.test.js | 8 ++++++++ src/db/models/Users.js | 22 +++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index 1fe163362..71720e679 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -106,4 +106,12 @@ describe('User db utils', () => { expect(userObj.details.fullName).toBe(updateDoc.details.fullName); expect(userObj.details.avatar).toBe(updateDoc.details.avatar); }); + + test('Config email signature', async () => { + const signature = { brandId: 'brandId', signature: 'signature' }; + + const user = await Users.configEmailSignatures(_user._id, [signature]); + + expect(user.details.emailSignatures[0].toJSON()).toEqual(signature); + }); }); diff --git a/src/db/models/Users.js b/src/db/models/Users.js index deb4da422..a019d2dec 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -5,6 +5,11 @@ import { ROLES } from '../../data/constants'; const SALT_WORK_FACTOR = 10; +const EmailSignatureSchema = mongoose.Schema( + { brandId: String, signature: String }, + { _id: false }, +); + // Detail schema const DetailSchema = mongoose.Schema( { @@ -12,11 +17,7 @@ const DetailSchema = mongoose.Schema( fullName: String, position: String, twitterUsername: String, - - signatures: { - brandId: String, - signature: String, - }, + emailSignatures: [EmailSignatureSchema], }, { _id: false }, ); @@ -98,6 +99,17 @@ class User { return this.findOne({ _id }); } + /* + * Update email signatures + * @param {[Object]} signatures - Email signatures + * @return {Promise} - Updated user + */ + static async configEmailSignatures(_id, signatures) { + await this.update({ _id }, { $set: { 'details.emailSignatures': signatures } }); + + return this.findOne({ _id }); + } + /* * Remove user * @param {String} _id - User id From 6f666128a760a2cc6173a9cce61d18227868152a Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 14:25:42 +0800 Subject: [PATCH 141/318] Add configGetNotificationsByEmail --- src/__tests__/userDb.test.js | 6 ++++++ src/db/models/Users.js | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index 71720e679..4e20f696f 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -114,4 +114,10 @@ describe('User db utils', () => { expect(user.details.emailSignatures[0].toJSON()).toEqual(signature); }); + + test('Config get notifications by email', async () => { + const user = await Users.configGetNotificationByEmail(_user._id, true); + + expect(user.details.getNotificationByEmail).toEqual(true); + }); }); diff --git a/src/db/models/Users.js b/src/db/models/Users.js index a019d2dec..bed7937f1 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -17,6 +17,7 @@ const DetailSchema = mongoose.Schema( fullName: String, position: String, twitterUsername: String, + getNotificationByEmail: Boolean, emailSignatures: [EmailSignatureSchema], }, { _id: false }, @@ -101,6 +102,7 @@ class User { /* * Update email signatures + * @param {String} _id - User id * @param {[Object]} signatures - Email signatures * @return {Promise} - Updated user */ @@ -110,6 +112,18 @@ class User { return this.findOne({ _id }); } + /* + * Config get notifications by emmail + * @param {String} _id - User id + * @param {[Object]} isAllowed - is allowed + * @return {Promise} - Updated user + */ + static async configGetNotificationByEmail(_id, isAllowed) { + await this.update({ _id }, { $set: { 'details.getNotificationByEmail': isAllowed } }); + + return this.findOne({ _id }); + } + /* * Remove user * @param {String} _id - User id From ead631fe37ea988b6197f9c140a60171ddb9725f Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 14:44:00 +0800 Subject: [PATCH 142/318] Add resetPassword --- src/__tests__/userDb.test.js | 42 ++++++++++++++++++++++++++++++++++++ src/auth.js | 35 ------------------------------ src/db/models/Users.js | 36 +++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 35 deletions(-) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index 4e20f696f..0432eb2bc 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -120,4 +120,46 @@ describe('User db utils', () => { expect(user.details.getNotificationByEmail).toEqual(true); }); + + test('Reset password', async () => { + expect.assertions(5); + + // token expired ============== + try { + await Users.resetPassword({ token: '', newPassword: '' }); + } catch (e) { + expect(e.message).toBe('Password reset token is invalid or has expired.'); + } + + // invalid password ================= + const today = new Date(); + const tomorrow = new Date(); + tomorrow.setDate(today.getDate() + 1); + + await Users.update( + { _id: _user._id }, + { + $set: { + resetPasswordToken: 'token', + resetPasswordExpires: tomorrow, + }, + }, + ); + + try { + await Users.resetPassword({ token: 'token', newPassword: '' }); + } catch (e) { + expect(e.message).toBe('Password is required.'); + } + + // valid + const user = await Users.resetPassword({ + token: 'token', + newPassword: 'password', + }); + + expect(user.resetPasswordToken).toBe(null); + expect(user.resetPasswordExpires).toBe(null); + expect(bcrypt.compare('password', user.password)).toBeTruthy(); + }); }); diff --git a/src/auth.js b/src/auth.js index 8ee6e1478..ab4b29a7e 100644 --- a/src/auth.js +++ b/src/auth.js @@ -131,40 +131,6 @@ export const forgotPassword = async ({ email }) => { return link; }; -/* - * Resets user password by given token & password - * @param {String} token - User's temporary token for reset password - * @param {String} newPassword - New password - * @return {Promise} - Update user response - */ -export const resetPassword = async ({ token, newPassword }) => { - // find user by token - const user = await Users.findOne({ - resetPasswordToken: token, - resetPasswordExpires: { - $gt: Date.now(), - }, - }); - - if (!user) { - throw new Error('Password reset token is invalid or has expired.'); - } - - if (!newPassword) { - throw new Error('Password is required.'); - } - - // set new password - return Users.findByIdAndUpdate( - { _id: user._id }, - { - password: bcrypt.hashSync(newPassword, 10), - resetPasswordToken: undefined, - resetPasswordExpires: undefined, - }, - ); -}; - /* * Finds user object by passed tokens * @param {Object} req - Request object @@ -206,5 +172,4 @@ export const userMiddleware = async (req, res, next) => { export default { login, forgotPassword, - resetPassword, }; diff --git a/src/db/models/Users.js b/src/db/models/Users.js index bed7937f1..814e68bba 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -155,6 +155,42 @@ class User { static generatePassword(password) { return bcrypt.hash(password, SALT_WORK_FACTOR); } + + /* + * Resets user password by given token & password + * @param {String} token - User's temporary token for reset password + * @param {String} newPassword - New password + * @return {Promise} - Updated user information + */ + static async resetPassword({ token, newPassword }) { + // find user by token + const user = await this.findOne({ + resetPasswordToken: token, + resetPasswordExpires: { + $gt: Date.now(), + }, + }); + + if (!user) { + throw new Error('Password reset token is invalid or has expired.'); + } + + if (!newPassword) { + throw new Error('Password is required.'); + } + + // set new password + await this.findByIdAndUpdate( + { _id: user._id }, + { + password: bcrypt.hashSync(newPassword, 10), + resetPasswordToken: undefined, + resetPasswordExpires: undefined, + }, + ); + + return this.findOne({ _id: user._id }); + } } UserSchema.loadClass(User); From 843718c4df7a86d280bf37b11815aec8d7471b45 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 15:42:39 +0800 Subject: [PATCH 143/318] Add forgot password --- src/__tests__/userDb.test.js | 17 +++++++++++++ src/auth.js | 49 ------------------------------------ src/db/models/Users.js | 30 ++++++++++++++++++++++ 3 files changed, 47 insertions(+), 49 deletions(-) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index 0432eb2bc..afb61af4f 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -162,4 +162,21 @@ describe('User db utils', () => { expect(user.resetPasswordExpires).toBe(null); expect(bcrypt.compare('password', user.password)).toBeTruthy(); }); + + test('Forgot password', async () => { + expect.assertions(3); + + // invalid email ============== + try { + await Users.forgotPassword('test@yahoo.com'); + } catch (e) { + expect(e.message).toBe('Invalid email'); + } + + // valid + const user = await Users.forgotPassword(_user.email); + + expect(user.resetPasswordToken).toBeDefined(); + expect(user.resetPasswordExpires).toBeDefined(); + }); }); diff --git a/src/auth.js b/src/auth.js index ab4b29a7e..ed55c45d3 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,8 +1,6 @@ import jwt from 'jsonwebtoken'; import bcrypt from 'bcrypt'; -import crypto from 'crypto'; import { Users } from './db/models'; -import { sendEmail } from './data/utils'; const SECRET = 'dfjklsafjjekjtejifjidfjsfd'; @@ -85,52 +83,6 @@ export const login = async ({ email, password }) => { }; }; -/* - * Sends reset password link to found user's email - * @param {String} email - Registered user's email - * @return {String} link - Reset password link - */ -export const forgotPassword = async ({ email }) => { - // find user - const user = await Users.findOne({ email }); - - if (!user) { - throw new Error('Invalid email'); - } - - // create the random token - const buffer = await crypto.randomBytes(20); - const token = buffer.toString('hex'); - - // save token & expiration date - await Users.findByIdAndUpdate( - { _id: user._id }, - { - resetPasswordToken: token, - resetPasswordExpires: Date.now() + 86400000, - }, - ); - - // send email ============== - const { COMPANY_EMAIL_FROM, MAIN_APP_DOMAIN } = process.env; - - const link = `${MAIN_APP_DOMAIN}/reset-password?token=${token}`; - - sendEmail({ - toEmails: [email], - fromEmail: COMPANY_EMAIL_FROM, - title: 'Reset password', - template: { - name: 'base', - data: { - content: link, - }, - }, - }); - - return link; -}; - /* * Finds user object by passed tokens * @param {Object} req - Request object @@ -171,5 +123,4 @@ export const userMiddleware = async (req, res, next) => { export default { login, - forgotPassword, }; diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 814e68bba..ce7b879ca 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -1,6 +1,7 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; import bcrypt from 'bcrypt'; +import crypto from 'crypto'; import { ROLES } from '../../data/constants'; const SALT_WORK_FACTOR = 10; @@ -191,6 +192,35 @@ class User { return this.findOne({ _id: user._id }); } + + /* + * Sends reset password link to found user's email + * @param {String} email - Registered user's email + * @return {Promise} - Updated user object + */ + static async forgotPassword(email) { + // find user + const user = await this.findOne({ email }); + + if (!user) { + throw new Error('Invalid email'); + } + + // create the random token + const buffer = await crypto.randomBytes(20); + const token = buffer.toString('hex'); + + // save token & expiration date + await this.findByIdAndUpdate( + { _id: user._id }, + { + resetPasswordToken: token, + resetPasswordExpires: Date.now() + 86400000, + }, + ); + + return this.findOne({ _id: user._id }); + } } UserSchema.loadClass(User); From e793a02dac97fd2ced28c6374eedb6a426b16343 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 16:09:52 +0800 Subject: [PATCH 144/318] Add login --- src/__tests__/userDb.test.js | 27 +++++++++++ src/auth.js | 90 +----------------------------------- src/db/factories.js | 1 + src/db/models/Users.js | 84 +++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 88 deletions(-) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index afb61af4f..7adf4e8c7 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -179,4 +179,31 @@ describe('User db utils', () => { expect(user.resetPasswordToken).toBeDefined(); expect(user.resetPasswordExpires).toBeDefined(); }); + + test('Login', async () => { + expect.assertions(4); + + // invalid email ============== + try { + await Users.login({ email: 'test@yahoo.com' }); + } catch (e) { + expect(e.message).toBe('Invalid login'); + } + + // invalid password ============== + try { + await Users.login({ email: _user.email, password: 'pass' }); + } catch (e) { + expect(e.message).toBe('Invalid login'); + } + + // valid + const { token, refreshToken } = await Users.login({ + email: _user.email, + password: 'Dombo@123', + }); + + expect(token).toBeDefined(); + expect(refreshToken).toBeDefined(); + }); }); diff --git a/src/auth.js b/src/auth.js index ed55c45d3..55e48e7a9 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,88 +1,6 @@ import jwt from 'jsonwebtoken'; -import bcrypt from 'bcrypt'; import { Users } from './db/models'; -const SECRET = 'dfjklsafjjekjtejifjidfjsfd'; - -/* - * Creates regular and refresh tokens using given user information - * @param {Object} _user - User object - * @param {String} secret - Token secret - * @return [String] - list of tokens - */ -const createTokens = async (_user, secret) => { - const user = { _id: _user._id, email: _user.email, details: _user.details }; - - const createToken = await jwt.sign({ user }, secret, { expiresIn: '20m' }); - - const createRefreshToken = await jwt.sign({ user }, secret, { expiresIn: '7d' }); - - return [createToken, createRefreshToken]; -}; - -/* - * Renews tokens - * @param {String} token - * @param {String} refreshToken - * @return {Object} renewed tokens with user - */ -export const refreshTokens = async (token, refreshToken) => { - let _id = null; - - try { - // validate refresh token - const { user } = jwt.verify(refreshToken, SECRET); - - _id = user._id; - - // if refresh token is expired then force to login - } catch (e) { - return {}; - } - - const user = await Users.findOne({ _id }); - - // recreate tokens - const [newToken, newRefreshToken] = await createTokens(user, SECRET); - - return { - token: newToken, - refreshToken: newRefreshToken, - user, - }; -}; - -/* - * Validates user credentials and generates tokens - * @param {Object} args - * @param {String} args.email - User email - * @param {String} args.password - User password - * @return {Object} - generated tokens - */ -export const login = async ({ email, password }) => { - const user = await Users.findOne({ email }); - - if (!user) { - // user with provided email not found - throw new Error('Invalid login'); - } - - const valid = await bcrypt.compare(password, user.password); - - if (!valid) { - // bad password - throw new Error('Invalid login'); - } - - // create tokens - const [token, refreshToken] = await createTokens(user, SECRET); - - return { - token, - refreshToken, - }; -}; - /* * Finds user object by passed tokens * @param {Object} req - Request object @@ -95,7 +13,7 @@ export const userMiddleware = async (req, res, next) => { if (token) { try { // verify user token and retrieve stored user information - const { user } = jwt.verify(token, SECRET); + const { user } = jwt.verify(token, Users.getSecret()); // save user in request req.user = user; @@ -105,7 +23,7 @@ export const userMiddleware = async (req, res, next) => { const refreshToken = req.headers['x-refresh-token']; // create new tokens using refresh token & refresh token - const newTokens = await refreshTokens(token, refreshToken); + const newTokens = await Users.refreshTokens(token, refreshToken); if (newTokens.token && newTokens.refreshToken) { res.set('Access-Control-Expose-Headers', 'x-token, x-refresh-token'); @@ -120,7 +38,3 @@ export const userMiddleware = async (req, res, next) => { next(); }; - -export default { - login, -}; diff --git a/src/db/factories.js b/src/db/factories.js index 6de4fbbff..6b1f4a9e4 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -34,6 +34,7 @@ export const userFactory = (params = {}) => { }, email: params.email || faker.internet.email(), role: params.role || 'contributor', + password: params.password || '$2a$12$eStwXbJ03luTm2826cbkWu57PnUA4Whk.KVOClc1P2kqcZTtsMK/i', isOwner: params.isOwner || false, }); diff --git a/src/db/models/Users.js b/src/db/models/Users.js index ce7b879ca..8d7780131 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -2,6 +2,7 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; import bcrypt from 'bcrypt'; import crypto from 'crypto'; +import jwt from 'jsonwebtoken'; import { ROLES } from '../../data/constants'; const SALT_WORK_FACTOR = 10; @@ -50,6 +51,10 @@ const UserSchema = mongoose.Schema({ }); class User { + static getSecret() { + return 'dfjklsafjjekjtejifjidfjsfd'; + } + /** * Create new user * @param {Object} doc - user fields @@ -221,6 +226,85 @@ class User { return this.findOne({ _id: user._id }); } + + /* + * Creates regular and refresh tokens using given user information + * @param {Object} _user - User object + * @param {String} secret - Token secret + * @return [String] - list of tokens + */ + static async createTokens(_user, secret) { + const user = { _id: _user._id, email: _user.email, details: _user.details }; + + const createToken = await jwt.sign({ user }, secret, { expiresIn: '20m' }); + + const createRefreshToken = await jwt.sign({ user }, secret, { expiresIn: '7d' }); + + return [createToken, createRefreshToken]; + } + + /* + * Renews tokens + * @param {String} token + * @param {String} refreshToken + * @return {Object} renewed tokens with user + */ + static async refreshTokens(token, refreshToken) { + let _id = null; + + try { + // validate refresh token + const { user } = jwt.verify(refreshToken, this.getSecret()); + + _id = user._id; + + // if refresh token is expired then force to login + } catch (e) { + return {}; + } + + const user = await Users.findOne({ _id }); + + // recreate tokens + const [newToken, newRefreshToken] = await this.createTokens(user, this.getSecret()); + + return { + token: newToken, + refreshToken: newRefreshToken, + user, + }; + } + + /* + * Validates user credentials and generates tokens + * @param {Object} args + * @param {String} args.email - User email + * @param {String} args.password - User password + * @return {Object} - generated tokens + */ + static async login({ email, password }) { + const user = await Users.findOne({ email }); + + if (!user) { + // user with provided email not found + throw new Error('Invalid login'); + } + + const valid = await bcrypt.compare(password, user.password); + + if (!valid) { + // bad password + throw new Error('Invalid login'); + } + + // create tokens + const [token, refreshToken] = await this.createTokens(user, this.getSecret()); + + return { + token, + refreshToken, + }; + } } UserSchema.loadClass(User); From c1d63d688325ade55d3b6d3ebc29ea6509f3fe27 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 16:17:35 +0800 Subject: [PATCH 145/318] Add refreshTokens tests --- src/__tests__/userDb.test.js | 18 ++++++++++++++++++ src/db/models/Users.js | 3 +-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index 7adf4e8c7..75f9bf52b 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -4,6 +4,7 @@ import { connect, disconnect } from '../db/connection'; import { Users } from '../db/models'; import { userFactory } from '../db/factories'; +import jwt from 'jsonwebtoken'; import bcrypt from 'bcrypt'; beforeAll(() => connect()); @@ -206,4 +207,21 @@ describe('User db utils', () => { expect(token).toBeDefined(); expect(refreshToken).toBeDefined(); }); + + test('Refresh tokens', async () => { + expect.assertions(3); + + // invalid refresh token + expect(await Users.refreshTokens('invalid')).toEqual({}); + + // valid ============== + const prevRefreshToken = await jwt.sign({ user: _user }, Users.getSecret(), { + expiresIn: '7d', + }); + + const { token, refreshToken } = await Users.refreshTokens(prevRefreshToken); + + expect(token).toBeDefined(); + expect(refreshToken).toBeDefined(); + }); }); diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 8d7780131..2c4351e4a 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -245,11 +245,10 @@ class User { /* * Renews tokens - * @param {String} token * @param {String} refreshToken * @return {Object} renewed tokens with user */ - static async refreshTokens(token, refreshToken) { + static async refreshTokens(refreshToken) { let _id = null; try { From 01b5d87a509db526c85a78c1f256851e0bd08e3c Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 16:26:21 +0800 Subject: [PATCH 146/318] Fix auth mutations --- src/__tests__/userDb.test.js | 3 ++- src/auth.js | 2 +- src/data/resolvers/mutations/users.js | 30 ++++++++++++++++++++++----- src/db/models/Users.js | 4 ++-- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index 75f9bf52b..b1b4eed82 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -175,7 +175,8 @@ describe('User db utils', () => { } // valid - const user = await Users.forgotPassword(_user.email); + await Users.forgotPassword(_user.email); + const user = await Users.findOne({ email: _user.email }); expect(user.resetPasswordToken).toBeDefined(); expect(user.resetPasswordExpires).toBeDefined(); diff --git a/src/auth.js b/src/auth.js index 55e48e7a9..28a8ff1ab 100644 --- a/src/auth.js +++ b/src/auth.js @@ -23,7 +23,7 @@ export const userMiddleware = async (req, res, next) => { const refreshToken = req.headers['x-refresh-token']; // create new tokens using refresh token & refresh token - const newTokens = await Users.refreshTokens(token, refreshToken); + const newTokens = await Users.refreshTokens(refreshToken); if (newTokens.token && newTokens.refreshToken) { res.set('Access-Control-Expose-Headers', 'x-token, x-refresh-token'); diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index 2e34e4b4d..d89c8c3a2 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -1,15 +1,35 @@ -import auth from '../../../auth'; +import { Users } from '../../../db/models'; +import { sendEmail } from '../../../data/utils'; export default { login(root, args) { - return auth.login(args); + return Users.login(args); }, - forgotPassword(root, args) { - return auth.forgotPassword(args); + async forgotPassword(root, { email }) { + const token = await Users.forgotPassword(email); + + // send email ============== + const { COMPANY_EMAIL_FROM, MAIN_APP_DOMAIN } = process.env; + + const link = `${MAIN_APP_DOMAIN}/reset-password?token=${token}`; + + sendEmail({ + toEmails: [email], + fromEmail: COMPANY_EMAIL_FROM, + title: 'Reset password', + template: { + name: 'base', + data: { + content: link, + }, + }, + }); + + return link; }, resetPassword(root, args) { - return auth.resetPassword(args); + return Users.resetPassword(args); }, }; diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 2c4351e4a..1a75696fd 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -201,7 +201,7 @@ class User { /* * Sends reset password link to found user's email * @param {String} email - Registered user's email - * @return {Promise} - Updated user object + * @return {String} - Generated token */ static async forgotPassword(email) { // find user @@ -224,7 +224,7 @@ class User { }, ); - return this.findOne({ _id: user._id }); + return token; } /* From 06373c46007c96554ebb7fbda46bd296cd22688b Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 16:37:00 +0800 Subject: [PATCH 147/318] Some test fix --- src/__tests__/userMutations.test.js | 14 +++++++------- src/db/models/Users.js | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js index 3091c12a2..57ca58cf1 100644 --- a/src/__tests__/userMutations.test.js +++ b/src/__tests__/userMutations.test.js @@ -1,37 +1,37 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ -import auth from '../auth'; +import { Users } from '../db/models'; import usersMutations from '../data/resolvers/mutations/users'; describe('User mutations', () => { test('Login', async () => { - auth.login = jest.fn(); + Users.login = jest.fn(); const doc = { email: 'test@erxes.io', password: 'password' }; await usersMutations.login({}, doc); - expect(auth.login).toBeCalledWith(doc); + expect(Users.login).toBeCalledWith(doc); }); test('Forgot password', async () => { - auth.forgotPassword = jest.fn(); + Users.forgotPassword = jest.fn(); const doc = { email: 'test@erxes.io' }; await usersMutations.forgotPassword({}, doc); - expect(auth.forgotPassword).toBeCalledWith(doc); + expect(Users.forgotPassword).toBeCalledWith(doc.email); }); test('Reset password', async () => { - auth.resetPassword = jest.fn(); + Users.resetPassword = jest.fn(); const doc = { token: '2424920429402', newPassword: 'newPassword' }; await usersMutations.resetPassword({}, doc); - expect(auth.resetPassword).toBeCalledWith(doc); + expect(Users.resetPassword).toBeCalledWith(doc); }); }); diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 1a75696fd..72507f70f 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -21,6 +21,7 @@ const DetailSchema = mongoose.Schema( twitterUsername: String, getNotificationByEmail: Boolean, emailSignatures: [EmailSignatureSchema], + starredConversationIds: [String], }, { _id: false }, ); From df9dd7f5edd5b74f96e45888f8625abbc34bbaae Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 17:41:53 +0800 Subject: [PATCH 148/318] Remove some fields from user detail --- src/__tests__/conversationDb.test.js | 10 +++------- src/__tests__/userDb.test.js | 4 ++-- src/db/models/Conversations.js | 18 ++---------------- src/db/models/Users.js | 12 ++++++------ 4 files changed, 13 insertions(+), 31 deletions(-) diff --git a/src/__tests__/conversationDb.test.js b/src/__tests__/conversationDb.test.js index 48f09dcfc..f3da61113 100644 --- a/src/__tests__/conversationDb.test.js +++ b/src/__tests__/conversationDb.test.js @@ -156,7 +156,7 @@ describe('Conversation db', () => { test('Conversation star', async () => { const user = await Conversations.starConversation([_conversation._id], _user._id); - expect(user.details.starredConversationIds[0]).toBe(_conversation._id); + expect(user.starredConversationIds[0]).toBe(_conversation._id); }); test('Conversation unstar', async () => { @@ -165,17 +165,13 @@ describe('Conversation db', () => { // star first before unstar await Users.update( { _id: _user.id }, - { - $addToSet: { - 'details.starredConversationIds': { $each: ids }, - }, - }, + { $addToSet: { starredConversationIds: { $each: ids } } }, ); // unstar const user = await Conversations.unstarConversation(ids, _user._id); - expect(user.details.starredConversationIds.length).toBe(0); + expect(user.starredConversationIds.length).toBe(0); }); test('Toggle participated users in conversation ', async () => { diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index b1b4eed82..ebe78c928 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -113,13 +113,13 @@ describe('User db utils', () => { const user = await Users.configEmailSignatures(_user._id, [signature]); - expect(user.details.emailSignatures[0].toJSON()).toEqual(signature); + expect(user.emailSignatures[0].toJSON()).toEqual(signature); }); test('Config get notifications by email', async () => { const user = await Users.configGetNotificationByEmail(_user._id, true); - expect(user.details.getNotificationByEmail).toEqual(true); + expect(user.getNotificationByEmail).toEqual(true); }); test('Reset password', async () => { diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index 0d384706c..3e53f3753 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -214,14 +214,7 @@ class Conversation { static async starConversation(_ids, userId) { await this.checkExistanceConversations(_ids); - await Users.update( - { _id: userId }, - { - $addToSet: { - 'details.starredConversationIds': { $each: _ids }, - }, - }, - ); + await Users.update({ _id: userId }, { $addToSet: { starredConversationIds: { $each: _ids } } }); return Users.findOne({ _id: userId }); } @@ -236,14 +229,7 @@ class Conversation { // check conversations existance await this.checkExistanceConversations(_ids); - await Users.update( - { _id: userId }, - { - $pull: { - 'details.starredConversationIds': { $in: _ids }, - }, - }, - ); + await Users.update({ _id: userId }, { $pull: { starredConversationIds: { $in: _ids } } }); return Users.findOne({ _id: userId }); } diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 72507f70f..d4b03aaf6 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -19,9 +19,6 @@ const DetailSchema = mongoose.Schema( fullName: String, position: String, twitterUsername: String, - getNotificationByEmail: Boolean, - emailSignatures: [EmailSignatureSchema], - starredConversationIds: [String], }, { _id: false }, ); @@ -41,7 +38,6 @@ const UserSchema = mongoose.Schema({ type: String, enum: [ROLES.ADMIN, ROLES.CONTRIBUTOR], }, - details: DetailSchema, isOwner: Boolean, email: { type: String, @@ -49,6 +45,10 @@ const UserSchema = mongoose.Schema({ unique: true, match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 'Please fill a valid email address'], }, + getNotificationByEmail: Boolean, + emailSignatures: [EmailSignatureSchema], + starredConversationIds: [String], + details: DetailSchema, }); class User { @@ -114,7 +114,7 @@ class User { * @return {Promise} - Updated user */ static async configEmailSignatures(_id, signatures) { - await this.update({ _id }, { $set: { 'details.emailSignatures': signatures } }); + await this.update({ _id }, { $set: { emailSignatures: signatures } }); return this.findOne({ _id }); } @@ -126,7 +126,7 @@ class User { * @return {Promise} - Updated user */ static async configGetNotificationByEmail(_id, isAllowed) { - await this.update({ _id }, { $set: { 'details.getNotificationByEmail': isAllowed } }); + await this.update({ _id }, { $set: { getNotificationByEmail: isAllowed } }); return this.findOne({ _id }); } From faff20b017dca084f107ab971096bd420cd45b1f Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 2 Nov 2017 18:27:48 +0800 Subject: [PATCH 149/318] Fix, refactor knowledge base topic --- src/data/resolvers/mutations/knowledgeBase.js | 16 +++++++++------- src/data/schema/knowledgeBase.js | 12 ++++++------ src/db/models/KnowledgeBase.js | 1 + 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/data/resolvers/mutations/knowledgeBase.js b/src/data/resolvers/mutations/knowledgeBase.js index de8dcfbb2..fbc6a689c 100644 --- a/src/data/resolvers/mutations/knowledgeBase.js +++ b/src/data/resolvers/mutations/knowledgeBase.js @@ -8,13 +8,14 @@ export default { /** * Create topic document * @param {Object} root - * @param {KnowledgeBaseTopic} doc - KnowledgeBaseTopic object - * @param {Object} object3 - Graphql input data + * @param {Object} object2 - Graphql input data + * @param {KnowledgeBaseTopic} object2.doc - KnowledgeBaseTopic object + * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - returns Promise resolving created document * @throws {Error} - throws Error('Login required') if user object is not supplied */ - knowledgeBaseTopicsAdd(root, doc, { user }) { + knowledgeBaseTopicsAdd(root, { doc }, { user }) { if (!user) { throw new Error('Login required'); } @@ -25,19 +26,20 @@ export default { /** * Update topic document * @param {Object} root - * @param {KnowledgeBaseTopic} doc - KnowledgeBaseTopic object + * @param {Object} object2 - Graphql input data + * @param {KnowledgeBaseTopic} object2.doc - KnowledgeBaseTopic object * @param {string} doc._id - KnowledgeBaseTopic document id - * @param {Object} object3 - Graphql input data + * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - returns Promise resolving modified document * @throws {Error} - throws Error('Login required') if user object is not supplied */ - knowledgeBaseTopicsEdit(root, { _id, ...fields }, { user }) { + knowledgeBaseTopicsEdit(root, { _id, doc }, { user }) { if (!user) { throw new Error('Login required'); } - return KnowledgeBaseTopics.updateDoc(_id, fields, user._id); + return KnowledgeBaseTopics.updateDoc(_id, doc, user._id); }, /** diff --git a/src/data/schema/knowledgeBase.js b/src/data/schema/knowledgeBase.js index d1db407ce..51c2a243e 100644 --- a/src/data/schema/knowledgeBase.js +++ b/src/data/schema/knowledgeBase.js @@ -72,15 +72,15 @@ export const queries = ` `; export const mutations = ` - knowledgeBaseTopicsAdd(doc: KnowledgeBaseTopicDoc): KnowledgeBaseTopic - knowledgeBaseTopicsEdit(_id: String!): Boolean + knowledgeBaseTopicsAdd(doc: KnowledgeBaseTopicDoc!): KnowledgeBaseTopic + knowledgeBaseTopicsEdit(_id: String!, doc: KnowledgeBaseTopicDoc!): KnowledgeBaseTopic knowledgeBaseTopicsRemove(_id: String!): Boolean - knowledgeBaseCategoriesAdd(doc: KnowledgeBaseCategoryDoc): KnowledgeBaseCategory - knowledgeBaseCategoriesEdit(_id: String!): Boolean + knowledgeBaseCategoriesAdd(doc: KnowledgeBaseCategoryDoc!): KnowledgeBaseCategory + knowledgeBaseCategoriesEdit(_id: String!, doc: KnowledgeBaseCategoryDoc!): KnowledgeBaseCategory knowledgeBaseCategoriesRemove(_id: String!): Boolean - knowledgeBaseArticlesAdd(doc: KnowledgeBaseArticleDoc): KnowledgeBaseArticle - knowledgeBaseArticlesEdit(_id: String!): Boolean + knowledgeBaseArticlesAdd(doc: KnowledgeBaseArticleDoc!): KnowledgeBaseArticle + knowledgeBaseArticlesEdit(_id: String!, doc: KnowledgeBaseArticleDoc!): KnowledgeBaseArticle knowledgeBaseArticlesRemove(_id: String!): Boolean `; diff --git a/src/db/models/KnowledgeBase.js b/src/db/models/KnowledgeBase.js index fc4e92bfd..0544d6228 100644 --- a/src/db/models/KnowledgeBase.js +++ b/src/db/models/KnowledgeBase.js @@ -231,6 +231,7 @@ const TopicSchema = mongoose.Schema({ brandId: { type: String, required: true, + validate: /\S+/, }, categoryIds: { type: [String], From efb601943f3fa4f99f23591fc5538703443bd209 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 2 Nov 2017 20:32:24 +0800 Subject: [PATCH 150/318] Refator, fix knoledge base category mutations --- src/data/resolvers/mutations/knowledgeBase.js | 48 ++++++++++--------- src/db/models/KnowledgeBase.js | 5 +- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/data/resolvers/mutations/knowledgeBase.js b/src/data/resolvers/mutations/knowledgeBase.js index fbc6a689c..43a035daa 100644 --- a/src/data/resolvers/mutations/knowledgeBase.js +++ b/src/data/resolvers/mutations/knowledgeBase.js @@ -27,8 +27,8 @@ export default { * Update topic document * @param {Object} root * @param {Object} object2 - Graphql input data + * @param {string} object2._id - KnowledgeBaseTopic document id * @param {KnowledgeBaseTopic} object2.doc - KnowledgeBaseTopic object - * @param {string} doc._id - KnowledgeBaseTopic document id * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - returns Promise resolving modified document @@ -45,9 +45,9 @@ export default { /** * Remove topic document * @param {Object} root - * @param {Object} doc - KnowledgeBaseTopic object - * @param {string} doc._id - KnowledgeBaseTopic document id - * @param {Object} object3 - Graphql input data + * @param {Object} object2 - Graphql input data + * @param {string} object2._id - KnowledgeBaseTopic document id + * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} * @throws {Error} - throws Error('Login required') if user object is not supplied @@ -63,13 +63,14 @@ export default { /** * Create category document * @param {Object} root - * @param {KnowledgeBaseCategory} doc - KnowledgeBaseCategory object - * @param {Object} object3 - Graphql input data + * @param {Object} object2 - Graphql input data + * @param {KnowledgeBaseCategory} object2.doc - KnowledgeBaseCategory object + * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - returns Promise resolving created document * @throws {Error} - throws Error('Login required') if user object is not supplied */ - knowledgeBaseCategoriesAdd(root, doc, { user }) { + knowledgeBaseCategoriesAdd(root, { doc }, { user }) { if (!user) { throw new Error('Login required'); } @@ -80,19 +81,20 @@ export default { /** * Update category document * @param {Object} root - * @param {KnowledgeBaseCategory} doc - KnowledgeBaseCategory object - * @param {string} doc._id - KnowledgeBaseCategory document id - * @param {Object} object3 - Graphql input data + * @param {Object} object2 - Graphql input data + * @param {string} object2._id - KnowledgeBaseCategory document id + * @param {KnowledgeBaseCategory} object2.doc - KnowledgeBaseCategory object + * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - returns Promise resolving modified document * @throws {Error} - throws Error('Login required') if user object is not supplied */ - knowledgeBaseCategoriesEdit(root, { _id, ...fields }, { user }) { + knowledgeBaseCategoriesEdit(root, { _id, doc }, { user }) { if (!user) { throw new Error('Login required'); } - return KnowledgeBaseCategories.updateDoc(_id, fields, user._id); + return KnowledgeBaseCategories.updateDoc(_id, doc, user._id); }, /** @@ -116,13 +118,14 @@ export default { /** * Create article document * @param {Object} root - * @param {KnowledgeBaseArticle} doc - KnowledgeBasecategory object - * @param {Object} object3 - Graphql input data + * @param {Object} object2 - Graphql input data + * @param {KnowledgeBaseArticle} object2.doc - KnowledgeBaseCategory object + * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - returns Promise resolving created document * @throws {Error} - throws Error('Login required') if user object is not supplied */ - knowledgeBaseArticlesAdd(root, doc, { user }) { + knowledgeBaseArticlesAdd(root, { doc }, { user }) { if (!user) { throw new Error('Login required'); } @@ -133,26 +136,27 @@ export default { /** * Update article document * @param {Object} root - * @param {KnowledgeBaseArticle} doc - KnowledgeBaseArticle object - * @param {string} doc._id - KnowledgeBaseArticle document id - * @param {Object} object3 - Graphql input data + * @param {Object} object2 - Graphql input data + * @param {string} object2._id - KnowledgeBaseArticle document id + * @param {KnowledgeBaseArticle} object2.doc - KnowledgeBaseArticle object + * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - returns Promise resolving modified document * @throws {Error} - throws Error('Login required') if user object is not supplied */ - knowledgeBaseArticlesEdit(root, { _id, ...fields }, { user }) { + knowledgeBaseArticlesEdit(root, { _id, doc }, { user }) { if (!user) { throw new Error('Login required'); } - return KnowledgeBaseArticles.updateDoc(_id, fields, user._id); + return KnowledgeBaseArticles.updateDoc(_id, doc, user._id); }, /** * Remove article document * @param {Object} root - * @param {Object} doc - KnowledgeBaseArticle object - * @param {string} doc._id - KnowledgeBaseArticle document id + * @param {Object} object2 - KnowledgeBaseArticle object + * @param {string} object2._id - KnowledgeBaseArticle document id * @param {Object} object3 - Graphql input data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} diff --git a/src/db/models/KnowledgeBase.js b/src/db/models/KnowledgeBase.js index 0544d6228..e43bad416 100644 --- a/src/db/models/KnowledgeBase.js +++ b/src/db/models/KnowledgeBase.js @@ -155,7 +155,10 @@ const CategorySchema = mongoose.Schema({ type: String, required: true, }, - description: String, + description: { + type: String, + required: false, + }, articleIds: { type: [String], required: false, From 9e0b049744d1a4f1d7981b9a345d48438b05ae64 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 2 Nov 2017 20:39:58 +0800 Subject: [PATCH 151/318] Update knowledge base tests --- src/__tests__/knowledgeBaseMutations.test.js | 22 +++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/__tests__/knowledgeBaseMutations.test.js b/src/__tests__/knowledgeBaseMutations.test.js index 136c1e860..036c1b585 100644 --- a/src/__tests__/knowledgeBaseMutations.test.js +++ b/src/__tests__/knowledgeBaseMutations.test.js @@ -38,15 +38,17 @@ describe('mutations', () => { KnowledgeBaseTopics.createDoc = jest.fn(); const doc = { - title: 'Test topic title', - description: 'Test topic description', - categoryIds: ['fakeCategoryId'], - brandId: 'fakeBrandId', + doc: { + title: 'Test topic title', + description: 'Test topic description', + categoryIds: ['fakeCategoryId'], + brandId: 'fakeBrandId', + }, }; knowledgeBaseMutations.knowledgeBaseTopicsAdd(null, doc, { user: _user }); - expect(KnowledgeBaseTopics.createDoc).toBeCalledWith(doc, _user._id); + expect(KnowledgeBaseTopics.createDoc).toBeCalledWith(doc.doc, _user._id); expect(KnowledgeBaseTopics.createDoc.mock.calls.length).toBe(1); KnowledgeBaseTopics.createDoc.mockRestore(); @@ -64,7 +66,7 @@ describe('mutations', () => { const updateDoc = { _id: 'fakeTopicId', - ...doc, + doc, }; knowledgeBaseMutations.knowledgeBaseTopicsEdit(null, updateDoc, { user: _user }); @@ -100,7 +102,7 @@ describe('mutations', () => { brandId: 'fakeBrandId', }; - knowledgeBaseMutations.knowledgeBaseCategoriesAdd(null, doc, { user: _user }); + knowledgeBaseMutations.knowledgeBaseCategoriesAdd(null, { doc }, { user: _user }); expect(KnowledgeBaseCategories.createDoc).toBeCalledWith(doc, _user._id); expect(KnowledgeBaseCategories.createDoc.mock.calls.length).toBe(1); @@ -120,7 +122,7 @@ describe('mutations', () => { const updateDoc = { _id: 'fakeCategoryId', - ...doc, + doc, }; knowledgeBaseMutations.knowledgeBaseCategoriesEdit(null, updateDoc, { user: _user }); @@ -160,7 +162,7 @@ describe('mutations', () => { status: 'Test article status', }; - knowledgeBaseMutations.knowledgeBaseArticlesAdd(null, doc, { user: _user }); + knowledgeBaseMutations.knowledgeBaseArticlesAdd(null, { doc }, { user: _user }); expect(KnowledgeBaseArticles.createDoc).toBeCalledWith(doc, _user._id); expect(KnowledgeBaseArticles.createDoc.mock.calls.length).toBe(1); @@ -180,7 +182,7 @@ describe('mutations', () => { const updateDoc = { _id: 'fakeArticleId', - ...doc, + doc, }; knowledgeBaseMutations.knowledgeBaseArticlesEdit(null, updateDoc, { user: _user }); From e6ba445a49e18f8ee6ada042df2988d480e20fee Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 2 Nov 2017 21:47:31 +0800 Subject: [PATCH 152/318] Add minor changes to knowledge base articles schema --- src/data/schema/knowledgeBase.js | 2 +- src/db/models/KnowledgeBase.js | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/data/schema/knowledgeBase.js b/src/data/schema/knowledgeBase.js index 51c2a243e..8ed989599 100644 --- a/src/data/schema/knowledgeBase.js +++ b/src/data/schema/knowledgeBase.js @@ -14,7 +14,7 @@ export const types = ` input KnowledgeBaseArticleDoc { title: String! summary: String - content: String + content: String! status: String! } diff --git a/src/db/models/KnowledgeBase.js b/src/db/models/KnowledgeBase.js index e43bad416..901020da4 100644 --- a/src/db/models/KnowledgeBase.js +++ b/src/db/models/KnowledgeBase.js @@ -82,10 +82,7 @@ const ArticleSchema = mongoose.Schema({ type: String, required: true, }, - summary: { - type: String, - required: true, - }, + summary: String, content: { type: String, required: true, From 18339a21238c8518f42f38b60691e0967043a560 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 2 Nov 2017 22:25:54 +0800 Subject: [PATCH 153/318] Add userAdd mutation --- src/__tests__/userMutations.test.js | 50 +++++++++++++++++++++++++++ src/data/resolvers/mutations/users.js | 12 +++++++ src/data/schema/user.js | 20 ++++++++++- src/db/models/Channels.js | 21 +++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js index 57ca58cf1..fa9676850 100644 --- a/src/__tests__/userMutations.test.js +++ b/src/__tests__/userMutations.test.js @@ -5,6 +5,8 @@ import { Users } from '../db/models'; import usersMutations from '../data/resolvers/mutations/users'; describe('User mutations', () => { + const user = { _id: 'DFAFDFDFD' }; + test('Login', async () => { Users.login = jest.fn(); @@ -34,4 +36,52 @@ describe('User mutations', () => { expect(Users.resetPassword).toBeCalledWith(doc); }); + + test('Login required checks', async () => { + const checkLogin = async (fn, args) => { + try { + await fn({}, args, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }; + + expect.assertions(1); + + // users add + checkLogin(usersMutations.usersAdd, {}); + }); + + test('Users add: wrong password confirmation', async () => { + expect.assertions(1); + + const doc = { + password: 'password', + passwordConfirmation: 'wrong', + }; + + try { + await usersMutations.usersAdd({}, doc, { user }); + } catch (e) { + expect(e.message).toBe('Incorrect password confirmation'); + } + }); + + test('Users add', async () => { + const user = { _id: 'DFAFDFDFD' }; + + Users.createUser = jest.fn(); + + const doc = { + username: 'username', + password: 'password', + email: 'info@erxes.io', + role: 'admin', + details: {}, + }; + + await usersMutations.usersAdd({}, { ...doc, passwordConfirmation: 'password' }, { user }); + + expect(Users.createUser).toBeCalledWith(doc); + }); }); diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index d89c8c3a2..8dbc71b62 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -32,4 +32,16 @@ export default { resetPassword(root, args) { return Users.resetPassword(args); }, + + usersAdd(root, args, { user }) { + const { username, password, passwordConfirmation, email, role, details } = args; + + if (!user) throw new Error('Login required'); + + if (password !== passwordConfirmation) { + throw new Error('Incorrect password confirmation'); + } + + return Users.createUser({ username, password, email, role, details }); + }, }; diff --git a/src/data/schema/user.js b/src/data/schema/user.js index 2e8c9acad..adfb47160 100644 --- a/src/data/schema/user.js +++ b/src/data/schema/user.js @@ -1,9 +1,17 @@ export const types = ` + input UserDetails { + avatar: String + fullName: String + position: String + twitterUsername: String + } + type User { _id: String! username: String + email: String + role: String details: JSON - emails: JSON } type AuthPayload { @@ -23,4 +31,14 @@ export const mutations = ` login(email: String!, password: String!): AuthPayload! forgotPassword(email: String!): String! resetPassword(token: String!, newPassword: String!): String + + usersAdd( + username: String!, + email: String!, + role: String! + details: UserDetails, + channelIds: [String], + password: String!, + passwordConfirmation: String! + ): User `; diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index eefa1a896..55278e3f7 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -81,6 +81,27 @@ class Channel { return this.findOne({}); } + /* + * Update user's channels + * @param {[String]} channelIds - User's all involved channels + * @param {String} userId - User id + */ + static async updateUserChannels(channelIds, userId) { + // remove from previous channels + await this.update( + { memberIds: { $in: [userId] } }, + { $pull: { memberIds: userId } }, + { multi: true }, + ); + + // add to given channels + await this.update( + { _id: { $in: channelIds } }, + { $push: { memberIds: userId } }, + { multi: true }, + ); + } + /** * Removes a channel document * @param {string} _id - Channel id From 07be37a96f60c0c2df80e926b3fe008627443766 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 3 Nov 2017 10:59:01 +0800 Subject: [PATCH 154/318] Replace all ALL_LIST to ALL --- src/data/constants.js | 19 +++++++++---------- src/data/resolvers/queries/conversations.js | 2 +- src/data/resolvers/queries/customers.js | 2 +- src/data/resolvers/queries/insights.js | 2 +- src/db/models/Conversations.js | 4 ++-- src/db/models/Engages.js | 6 +++--- src/db/models/Integrations.js | 4 ++-- src/db/models/InternalNotes.js | 2 +- src/db/models/Segments.js | 2 +- src/db/models/Tags.js | 2 +- 10 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/data/constants.js b/src/data/constants.js index 122f0bfb8..025026e14 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -5,7 +5,7 @@ export const CONVERSATION_STATUSES = { NEW: 'new', OPEN: 'open', CLOSED: 'closed', - ALL_LIST: ['new', 'open', 'closed'], + ALL: ['new', 'open', 'closed'], }; export const INTEGRATION_KIND_CHOICES = { @@ -20,7 +20,7 @@ export const TAG_TYPES = { CONVERSATION: 'conversation', CUSTOMER: 'customer', ENGAGE_MESSAGE: 'engageMessage', - ALL_LIST: ['conversation', 'customer', 'engageMessage'], + ALL: ['conversation', 'customer', 'engageMessage'], }; export const FACEBOOK_DATA_KINDS = { @@ -33,27 +33,27 @@ export const MESSENGER_KINDS = { CHAT: 'chat', NOTE: 'note', POST: 'post', - ALL_LIST: ['chat', 'note', 'post'], + ALL: ['chat', 'note', 'post'], }; export const SENT_AS_CHOICES = { BADGE: 'badge', SNIPPET: 'snippet', FULL_MESSAGE: 'fullMessage', - ALL_LIST: ['badge', 'snippet', 'fullMessage'], + ALL: ['badge', 'snippet', 'fullMessage'], }; export const MESSAGE_KINDS = { AUTO: 'auto', VISITOR_AUTO: 'visitorAuto', MANUAL: 'manual', - ALL_LIST: ['auto', 'visitorAuto', 'manual'], + ALL: ['auto', 'visitorAuto', 'manual'], }; export const METHODS = { MESSENGER: 'messenger', EMAIL: 'email', - ALL_LIST: ['messenger', 'email'], + ALL: ['messenger', 'email'], }; export const FORM_LOAD_TYPES = { @@ -75,7 +75,6 @@ export const KIND_CHOICES = { FORM: 'form', TWITTER: 'twitter', FACEBOOK: 'facebook', - ALL_LIST: ['messenger', 'form', 'twitter', 'facebook'], ALL: ['messenger', 'form', 'twitter', 'facebook'], }; @@ -136,19 +135,19 @@ export const FIELD_CONTENT_TYPES = { FORM: 'form', CUSTOMER: 'customer', COMPANY: 'company', - ALL_LIST: ['form', 'customer', 'company'], + ALL: ['form', 'customer', 'company'], }; export const INTERNAL_NOTE_CONTENT_TYPES = { CUSTOMER: 'customer', COMPANY: 'company', - ALL_LIST: ['customer', 'company'], + ALL: ['customer', 'company'], }; export const SEGMENT_CONTENT_TYPES = { CUSTOMER: 'customer', COMPANY: 'company', - ALL_LIST: ['customer', 'company'], + ALL: ['customer', 'company'], }; export const ROLES = { diff --git a/src/data/resolvers/queries/conversations.js b/src/data/resolvers/queries/conversations.js index 516b0179b..d5acd463d 100644 --- a/src/data/resolvers/queries/conversations.js +++ b/src/data/resolvers/queries/conversations.js @@ -119,7 +119,7 @@ export default { ); // by integration type - for (let intT of INTEGRATION_KIND_CHOICES.ALL_LIST) { + for (let intT of INTEGRATION_KIND_CHOICES.ALL) { response.byIntegrationTypes[intT] = await count( Object.assign({}, queries.default, await qb.integrationTypeFilter(intT)), ); diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index e28efc174..93a52801e 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -100,7 +100,7 @@ export default { } // Count customers by integration - for (let kind of INTEGRATION_KIND_CHOICES.ALL_LIST) { + for (let kind of INTEGRATION_KIND_CHOICES.ALL) { const integrations = await Integrations.find({ kind }); counts.byIntegrationType[kind] = await count({ diff --git a/src/data/resolvers/queries/insights.js b/src/data/resolvers/queries/insights.js index 0e524e6f8..aa9174d92 100644 --- a/src/data/resolvers/queries/insights.js +++ b/src/data/resolvers/queries/insights.js @@ -37,7 +37,7 @@ export default { const insights = []; // count conversations by each integration kind - for (let kind of INTEGRATION_KIND_CHOICES.ALL_LIST) { + for (let kind of INTEGRATION_KIND_CHOICES.ALL) { const integrationIds = await Integrations.find({ ...integrationSelector, kind }).select( '_id', ); diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index 3e53f3753..bb49f740b 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -50,7 +50,7 @@ const FacebookSchema = mongoose.Schema( { kind: { type: String, - enum: FACEBOOK_DATA_KINDS.ALL_LIST, + enum: FACEBOOK_DATA_KINDS.ALL, }, senderName: { type: String, @@ -91,7 +91,7 @@ const ConversationSchema = mongoose.Schema({ status: { type: String, required: true, - enum: CONVERSATION_STATUSES.ALL_LIST, + enum: CONVERSATION_STATUSES.ALL, }, messageCount: Number, tagIds: [String], diff --git a/src/db/models/Engages.js b/src/db/models/Engages.js index a78fce4ed..e30bb1bbb 100644 --- a/src/db/models/Engages.js +++ b/src/db/models/Engages.js @@ -27,11 +27,11 @@ const MessengerSchema = mongoose.Schema({ brandId: String, kind: { type: String, - enum: MESSENGER_KINDS.ALL_LIST, + enum: MESSENGER_KINDS.ALL, }, sentAs: { type: String, - enum: SENT_AS_CHOICES.ALL_LIST, + enum: SENT_AS_CHOICES.ALL, }, content: String, rules: [RuleSchema], @@ -46,7 +46,7 @@ const EngageMessageSchema = mongoose.Schema({ fromUserId: String, method: { type: String, - enum: METHODS.ALL_LIST, + enum: METHODS.ALL, }, isDraft: Boolean, isLive: Boolean, diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index 93a5a16f3..2f6c922ce 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -47,11 +47,11 @@ const FormDataSchema = mongoose.Schema( { loadType: { type: String, - enum: FORM_LOAD_TYPES.ALL_LIST, + enum: FORM_LOAD_TYPES.ALL, }, successAction: { type: String, - enum: FORM_SUCCESS_ACTIONS.ALL_LIST, + enum: FORM_SUCCESS_ACTIONS.ALL, }, fromEmail: { type: String, diff --git a/src/db/models/InternalNotes.js b/src/db/models/InternalNotes.js index 13f14e2cf..76d399607 100644 --- a/src/db/models/InternalNotes.js +++ b/src/db/models/InternalNotes.js @@ -13,7 +13,7 @@ const InternalNoteSchema = mongoose.Schema({ }, contentType: { type: String, - enum: INTERNAL_NOTE_CONTENT_TYPES.ALL_LIST, + enum: INTERNAL_NOTE_CONTENT_TYPES.ALL, }, contentTypeId: String, content: { diff --git a/src/db/models/Segments.js b/src/db/models/Segments.js index ba00ba1c6..5fb4354f8 100644 --- a/src/db/models/Segments.js +++ b/src/db/models/Segments.js @@ -29,7 +29,7 @@ const SegmentSchema = mongoose.Schema({ }, contentType: { type: String, - enum: SEGMENT_CONTENT_TYPES.ALL_LIST, + enum: SEGMENT_CONTENT_TYPES.ALL, }, name: String, description: String, diff --git a/src/db/models/Tags.js b/src/db/models/Tags.js index 6f8326c83..0d334223a 100644 --- a/src/db/models/Tags.js +++ b/src/db/models/Tags.js @@ -13,7 +13,7 @@ const TagSchema = mongoose.Schema({ name: String, type: { type: String, - enum: TAG_TYPES.ALL_LIST, + enum: TAG_TYPES.ALL, }, colorCode: String, createdAt: Date, From 5fc329cd1f0d3650307325c52826ee9aa3d20674 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 3 Nov 2017 11:21:41 +0800 Subject: [PATCH 155/318] Add updateUserChannels method --- src/__tests__/channelDb.test.js | 26 +++++++++++++++++++++++++- src/db/models/Channels.js | 4 +++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/__tests__/channelDb.test.js b/src/__tests__/channelDb.test.js index 61e615890..3679e11aa 100644 --- a/src/__tests__/channelDb.test.js +++ b/src/__tests__/channelDb.test.js @@ -2,7 +2,7 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { userFactory, integrationFactory } from '../db/factories'; +import { userFactory, integrationFactory, channelFactory } from '../db/factories'; import { Channels, Users, Integrations } from '../db/models'; import toBeType from 'jest-tobetype'; @@ -184,3 +184,27 @@ describe('test createdAtModifier', () => { expect(updatedChannel.createdAt).toEqual(createdAt); }); }); + +describe('db utils', () => { + let _user; + let _channel; + + beforeEach(async () => { + _user = await userFactory({}); + _channel = await channelFactory({ memberIds: ['DFAFDSFDDFAS'] }); + }); + + afterEach(async () => { + await Users.remove({}); + await Channels.remove({}); + }); + + test('updateUserChannels', async () => { + const updatedChannels = await Channels.updateUserChannels([_channel._id], _user._id); + + const updatedChannel = updatedChannels.pop(); + + expect(updatedChannel.memberIds).toContain('DFAFDSFDDFAS'); + expect(updatedChannel.memberIds).toContain(_user._id); + }); +}); diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index 55278e3f7..6d6a3f113 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -84,7 +84,7 @@ class Channel { /* * Update user's channels * @param {[String]} channelIds - User's all involved channels - * @param {String} userId - User id + * @param {Promise} - Updated channels */ static async updateUserChannels(channelIds, userId) { // remove from previous channels @@ -100,6 +100,8 @@ class Channel { { $push: { memberIds: userId } }, { multi: true }, ); + + return this.find({ _id: { $in: channelIds } }); } /** From 8d41ce4f631326d65328d541933df3b3651ca62e Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 3 Nov 2017 12:09:47 +0800 Subject: [PATCH 156/318] Complete userAdd --- src/__tests__/userDb.test.js | 1 + src/__tests__/userMutations.test.js | 32 +++++++++++++-- src/data/resolvers/mutations/users.js | 56 ++++++++++++++++++++++++--- 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index ebe78c928..aa2ac9127 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -47,6 +47,7 @@ describe('User db utils', () => { }); expect(userObj).toBeDefined(); + expect(userObj._id).toBeDefined(); expect(userObj.username).toBe(_user.username); expect(userObj.email).toBe(_user.email); expect(userObj.role).toBe(_user.role); diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js index fa9676850..39819d00a 100644 --- a/src/__tests__/userMutations.test.js +++ b/src/__tests__/userMutations.test.js @@ -1,8 +1,9 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ -import { Users } from '../db/models'; +import { Users, Channels } from '../db/models'; import usersMutations from '../data/resolvers/mutations/users'; +import utils from '../data/utils'; describe('User mutations', () => { const user = { _id: 'DFAFDFDFD' }; @@ -69,8 +70,11 @@ describe('User mutations', () => { test('Users add', async () => { const user = { _id: 'DFAFDFDFD' }; + const channelIds = ['DFAFSDFDSAF', 'DFFADSFDSFD']; - Users.createUser = jest.fn(); + Users.createUser = jest.fn(() => ({ _id: '_id' })); + Channels.updateUserChannels = jest.fn(); + const spyEmail = jest.spyOn(utils, 'sendEmail'); const doc = { username: 'username', @@ -80,8 +84,30 @@ describe('User mutations', () => { details: {}, }; - await usersMutations.usersAdd({}, { ...doc, passwordConfirmation: 'password' }, { user }); + await usersMutations.usersAdd( + {}, + { ...doc, passwordConfirmation: 'password', channelIds }, + { user }, + ); + // create user call expect(Users.createUser).toBeCalledWith(doc); + + // update user channels call + expect(Channels.updateUserChannels).toBeCalledWith(channelIds, '_id'); + + // send email call + expect(spyEmail).toBeCalledWith({ + toEmails: [doc.email], + fromEmail: process.env.COMPANY_EMAIL_FROM, + subject: 'Invitation info', + template: { + name: 'invitation', + data: { + username: doc.username, + password: doc.password, + }, + }, + }); }); }); diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index 8dbc71b62..d0eede67f 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -1,11 +1,23 @@ -import { Users } from '../../../db/models'; -import { sendEmail } from '../../../data/utils'; +import { Users, Channels } from '../../../db/models'; +import utils from '../../../data/utils'; export default { + /* + * Login + * @param {String} email - User email + * @param {String} password - User password + * @return tokens.token - Token to use authenticate against graphql endpoints + * @return tokens.refreshToken - Token to use refresh expired token + */ login(root, args) { return Users.login(args); }, + /* + * Send forgot password email + * @param {String} email - Email to send link + * @return {String} - Recover link + */ async forgotPassword(root, { email }) { const token = await Users.forgotPassword(email); @@ -14,7 +26,7 @@ export default { const link = `${MAIN_APP_DOMAIN}/reset-password?token=${token}`; - sendEmail({ + utils.sendEmail({ toEmails: [email], fromEmail: COMPANY_EMAIL_FROM, title: 'Reset password', @@ -29,12 +41,23 @@ export default { return link; }, + /* + * Reset password + * @param {String} token - Temporary token to find user + * @param {String} newPassword - New password to set + * @return {Promise} - Updated user object + */ resetPassword(root, args) { return Users.resetPassword(args); }, - usersAdd(root, args, { user }) { - const { username, password, passwordConfirmation, email, role, details } = args; + /* + * Create new user + * @param {Object} args - User doc + * @return {Promise} - Newly created user + */ + async usersAdd(root, args, { user }) { + const { username, password, passwordConfirmation, email, role, channelIds, details } = args; if (!user) throw new Error('Login required'); @@ -42,6 +65,27 @@ export default { throw new Error('Incorrect password confirmation'); } - return Users.createUser({ username, password, email, role, details }); + const createdUser = await Users.createUser({ username, password, email, role, details }); + + // add new user to channels + await Channels.updateUserChannels(channelIds, createdUser._id); + + // send email ================ + const { COMPANY_EMAIL_FROM } = process.env; + + utils.sendEmail({ + toEmails: [email], + fromEmail: COMPANY_EMAIL_FROM, + subject: 'Invitation info', + template: { + name: 'invitation', + data: { + username, + password, + }, + }, + }); + + return createdUser; }, }; From 4e82c7d943efc33a9d5b13e0236dbe658411eff5 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 3 Nov 2017 12:39:19 +0800 Subject: [PATCH 157/318] Add users edit --- src/__tests__/userMutations.test.js | 44 +++++++++++++++++++++++++-- src/data/resolvers/mutations/users.js | 32 +++++++++++++++++++ src/data/schema/user.js | 22 ++++++++------ src/db/models/Users.js | 5 ++- 4 files changed, 89 insertions(+), 14 deletions(-) diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js index 39819d00a..385992b00 100644 --- a/src/__tests__/userMutations.test.js +++ b/src/__tests__/userMutations.test.js @@ -47,14 +47,17 @@ describe('User mutations', () => { } }; - expect.assertions(1); + expect.assertions(2); // users add checkLogin(usersMutations.usersAdd, {}); + + // users edit + checkLogin(usersMutations.usersEdit, {}); }); - test('Users add: wrong password confirmation', async () => { - expect.assertions(1); + test('Users add & edit: wrong password confirmation', async () => { + expect.assertions(2); const doc = { password: 'password', @@ -66,6 +69,12 @@ describe('User mutations', () => { } catch (e) { expect(e.message).toBe('Incorrect password confirmation'); } + + try { + await usersMutations.usersEdit({}, doc, { user }); + } catch (e) { + expect(e.message).toBe('Incorrect password confirmation'); + } }); test('Users add', async () => { @@ -110,4 +119,33 @@ describe('User mutations', () => { }, }); }); + + test('Users edit', async () => { + const creatingUser = { _id: 'DFAFDFDFD' }; + const channelIds = ['DFAFSDFDSAF', 'DFFADSFDSFD']; + + Users.updateUser = jest.fn(); + Channels.updateUserChannels = jest.fn(); + + const userId = 'DFAFDSFSDFDSF'; + const doc = { + username: 'username', + password: 'password', + email: 'info@erxes.io', + role: 'admin', + details: {}, + }; + + await usersMutations.usersEdit( + {}, + { ...doc, _id: userId, passwordConfirmation: 'password', channelIds }, + { user: creatingUser }, + ); + + // update user call + expect(Users.updateUser).toBeCalledWith(userId, doc); + + // update user channels call + expect(Channels.updateUserChannels).toBeCalledWith(channelIds, userId); + }); }); diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index d0eede67f..3b01cd327 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -88,4 +88,36 @@ export default { return createdUser; }, + + /* + * Update user + * @param {Object} args - User doc + * @return {Promise} - Newly created user + */ + async usersEdit(root, args, { user }) { + const { + _id, + username, + password, + passwordConfirmation, + email, + role, + channelIds, + details, + } = args; + + if (!user) throw new Error('Login required'); + + if (password && password !== passwordConfirmation) { + throw new Error('Incorrect password confirmation'); + } + + // TODO check isOwner + const updatedUser = await Users.updateUser(_id, { username, password, email, role, details }); + + // add new user to channels + await Channels.updateUserChannels(channelIds, _id); + + return updatedUser; + }, }; diff --git a/src/data/schema/user.js b/src/data/schema/user.js index adfb47160..f3ad87246 100644 --- a/src/data/schema/user.js +++ b/src/data/schema/user.js @@ -27,18 +27,20 @@ export const queries = ` currentUser: User `; +const commonParams = ` + username: String!, + email: String!, + role: String! + details: UserDetails, + channelIds: [String], + password: String!, + passwordConfirmation: String! +`; + export const mutations = ` login(email: String!, password: String!): AuthPayload! forgotPassword(email: String!): String! resetPassword(token: String!, newPassword: String!): String - - usersAdd( - username: String!, - email: String!, - role: String! - details: UserDetails, - channelIds: [String], - password: String!, - passwordConfirmation: String! - ): User + usersAdd(${commonParams}): User + usersEdit(_id: String!, ${commonParams}): User `; diff --git a/src/db/models/Users.js b/src/db/models/Users.js index d4b03aaf6..264d2f957 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -81,7 +81,10 @@ class User { * @return {Promise} updated user info */ static async updateUser(_id, { username, email, password, role, details }) { - await this.checkDuplications({ twitterUsername: details.twitterUsername }); + await this.checkDuplications({ + userId: _id, + twitterUsername: details.twitterUsername, + }); const doc = { username, email, password, role, details }; From c507c84b3c8553ed0187e2cb44dbe78afa0fda5a Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 12:40:32 +0800 Subject: [PATCH 158/318] Add utils for basic permissions --- src/auth.js | 1 + src/data/resolvers/queries/knowledgeBase.js | 11 +++++++- src/data/resolvers/queries/users.js | 28 ++++++++++++++++++--- src/data/resolvers/queries/utils.js | 18 +++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 src/data/resolvers/queries/utils.js diff --git a/src/auth.js b/src/auth.js index 28a8ff1ab..9f80d42d3 100644 --- a/src/auth.js +++ b/src/auth.js @@ -36,5 +36,6 @@ export const userMiddleware = async (req, res, next) => { } } + console.log('middleware req.user: ', req.user); next(); }; diff --git a/src/data/resolvers/queries/knowledgeBase.js b/src/data/resolvers/queries/knowledgeBase.js index df3d57aa4..d98ba9a99 100644 --- a/src/data/resolvers/queries/knowledgeBase.js +++ b/src/data/resolvers/queries/knowledgeBase.js @@ -4,7 +4,9 @@ import { KnowledgeBaseArticles, } from '../../../db/models'; -export default { +import { BasicPermissions } from './utils'; + +const knowledgeBase = { /** * Article list * @param {Object} args @@ -12,6 +14,7 @@ export default { * @return {Promise} sorted article list */ knowledgeBaseArticles(root, { limit }) { + console.log('aaaa'); const articles = KnowledgeBaseArticles.find({}); const sort = { createdDate: -1 }; @@ -19,6 +22,7 @@ export default { return articles.sort(sort).limit(limit); } + console.log('bbbb'); return articles.sort(sort); }, @@ -112,3 +116,8 @@ export default { return KnowledgeBaseTopics.find({}).count(); }, }; + +BasicPermissions.setPermissionsForList(knowledgeBase, 'knowledgeBaseArticles'); +console.log('knowledgeBase.knowledgeBaseArticles: ', knowledgeBase.knowledgeBaseArticles); + +export default knowledgeBase; diff --git a/src/data/resolvers/queries/users.js b/src/data/resolvers/queries/users.js index ddf6367e1..d03b833ed 100644 --- a/src/data/resolvers/queries/users.js +++ b/src/data/resolvers/queries/users.js @@ -1,13 +1,16 @@ import { Users } from '../../../db/models'; +import { BasicPermissions } from './utils'; -export default { +const users = { /** * Users list * @param {Object} args * @param {Integer} args.limit + * @param {Object} object3 - Graphql middleware data + * @param {Object} object3.user - User making this request * @return {Promise} sorted and filtered users objects */ - users(root, { limit }) { + users(root, { limit }, { user }) { const users = Users.find({}); const sort = { username: 1 }; @@ -22,17 +25,29 @@ export default { * Get one user * @param {Object} args * @param {String} args._id + * @param {Object} object3 - Graphql middleware data + * @param {Object} object3.user - User making this request * @return {Promise} found user */ - userDetail(root, { _id }) { + userDetail(root, { _id }, { user }) { + if (!user) { + return {}; + } + return Users.findOne({ _id }); }, /** * Get all users count. We will use it in pager + * @param {Object} object3 - Graphql middleware data + * @param {Object} object3.user - User making this request * @return {Promise} total count */ - usersTotalCount() { + usersTotalCount(root, object2, { user }) { + if (!user) { + return 0; + } + return Users.find({}).count(); }, @@ -44,3 +59,8 @@ export default { return user; }, }; + +BasicPermissions.setPermissionsForList(users, 'users'); +console.log('users.users: ', users.users); + +export default users; diff --git a/src/data/resolvers/queries/utils.js b/src/data/resolvers/queries/utils.js new file mode 100644 index 000000000..6e58f1e05 --- /dev/null +++ b/src/data/resolvers/queries/utils.js @@ -0,0 +1,18 @@ +export class BasicPermissions { + static setPermissionsForList(cls, methodName) { + const oldMethod = cls[methodName]; + + cls[methodName] = (root, object2, { user }) => { + console.log('user: ', user); + if (!user) { + return []; + } + + return oldMethod(root, object2, { user }); + }; + } +} + +export default { + BasicPermissions, +}; From 37105aed159a0d5b8eea835aac916a5dcb619fe3 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 13:39:33 +0800 Subject: [PATCH 159/318] Test requireLogin on channel mutations --- src/data/permissions.js | 21 +++++++++++++++++++++ src/data/resolvers/mutations/channels.js | 7 ++++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/data/permissions.js diff --git a/src/data/permissions.js b/src/data/permissions.js new file mode 100644 index 000000000..4c8a92d74 --- /dev/null +++ b/src/data/permissions.js @@ -0,0 +1,21 @@ +export const requireLogin = (cls, methodName) => { + const oldMethod = cls[methodName]; + + console.log('oldMethod: ', oldMethod); + console.log('cls: ', cls); + console.log('methodName: ', methodName); + + cls[methodName] = (root, object2, { user }) => { + if (!user) { + throw new Error('Login required'); + } + + return oldMethod(root, object2, { user }); + }; + console.log('cls[methodName]: ', cls[methodName]); + console.log('cls2: ', cls); +}; + +export default { + requireLogin, +}; diff --git a/src/data/resolvers/mutations/channels.js b/src/data/resolvers/mutations/channels.js index 9c9341500..87cb21a26 100644 --- a/src/data/resolvers/mutations/channels.js +++ b/src/data/resolvers/mutations/channels.js @@ -1,6 +1,7 @@ import { MODULES } from '../../constants'; import { Channels } from '../../../db/models'; import utils from '../../utils'; +import permissions from '../../permissions'; /** * Send notification to all members of this channel except the sender @@ -25,7 +26,7 @@ export const sendChannelNotifications = async channel => { }); }; -export default { +const channelMutations = { /** * Create a new channel and send notifications to its members bar the creator * @param {Object} root @@ -97,3 +98,7 @@ export default { return _id; }, }; + +permissions.requireLogin(channelMutations, 'channelsAdd'); + +export default channelMutations; From 9a756694c5ba524d3200a3522a371bc862a3bc30 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 3 Nov 2017 13:48:06 +0800 Subject: [PATCH 160/318] Add usersRemove --- src/__tests__/userMutations.test.js | 48 +++++++++++++++++++++------ src/data/resolvers/mutations/users.js | 20 ++++++++++- src/db/models/Users.js | 2 +- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js index 385992b00..e11e01536 100644 --- a/src/__tests__/userMutations.test.js +++ b/src/__tests__/userMutations.test.js @@ -1,19 +1,30 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ +import { connect, disconnect } from '../db/connection'; import { Users, Channels } from '../db/models'; -import usersMutations from '../data/resolvers/mutations/users'; +import { userFactory } from '../db/factories'; +import userMutations from '../data/resolvers/mutations/users'; import utils from '../data/utils'; +beforeAll(() => connect()); + +afterAll(() => disconnect()); + describe('User mutations', () => { const user = { _id: 'DFAFDFDFD' }; + afterEach(async () => { + // Clearing test data + await Users.remove({}); + }); + test('Login', async () => { Users.login = jest.fn(); const doc = { email: 'test@erxes.io', password: 'password' }; - await usersMutations.login({}, doc); + await userMutations.login({}, doc); expect(Users.login).toBeCalledWith(doc); }); @@ -23,7 +34,7 @@ describe('User mutations', () => { const doc = { email: 'test@erxes.io' }; - await usersMutations.forgotPassword({}, doc); + await userMutations.forgotPassword({}, doc); expect(Users.forgotPassword).toBeCalledWith(doc.email); }); @@ -33,7 +44,7 @@ describe('User mutations', () => { const doc = { token: '2424920429402', newPassword: 'newPassword' }; - await usersMutations.resetPassword({}, doc); + await userMutations.resetPassword({}, doc); expect(Users.resetPassword).toBeCalledWith(doc); }); @@ -47,13 +58,16 @@ describe('User mutations', () => { } }; - expect.assertions(2); + expect.assertions(3); // users add - checkLogin(usersMutations.usersAdd, {}); + checkLogin(userMutations.usersAdd, {}); // users edit - checkLogin(usersMutations.usersEdit, {}); + checkLogin(userMutations.usersEdit, {}); + + // users remove + checkLogin(userMutations.usersRemove, {}); }); test('Users add & edit: wrong password confirmation', async () => { @@ -65,13 +79,13 @@ describe('User mutations', () => { }; try { - await usersMutations.usersAdd({}, doc, { user }); + await userMutations.usersAdd({}, doc, { user }); } catch (e) { expect(e.message).toBe('Incorrect password confirmation'); } try { - await usersMutations.usersEdit({}, doc, { user }); + await userMutations.usersEdit({}, doc, { user }); } catch (e) { expect(e.message).toBe('Incorrect password confirmation'); } @@ -93,7 +107,7 @@ describe('User mutations', () => { details: {}, }; - await usersMutations.usersAdd( + await userMutations.usersAdd( {}, { ...doc, passwordConfirmation: 'password', channelIds }, { user }, @@ -136,7 +150,7 @@ describe('User mutations', () => { details: {}, }; - await usersMutations.usersEdit( + await userMutations.usersEdit( {}, { ...doc, _id: userId, passwordConfirmation: 'password', channelIds }, { user: creatingUser }, @@ -148,4 +162,16 @@ describe('User mutations', () => { // update user channels call expect(Channels.updateUserChannels).toBeCalledWith(channelIds, userId); }); + + test('Users remove: can not delete owner', async () => { + expect.assertions(1); + + const owner = await userFactory({ isOwner: true }); + + try { + await userMutations.usersRemove({}, { _id: owner._id }, { user }); + } catch (e) { + expect(e.message).toBe('Can not remove owner'); + } + }); }); diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index 3b01cd327..fdd6209df 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -92,7 +92,7 @@ export default { /* * Update user * @param {Object} args - User doc - * @return {Promise} - Newly created user + * @return {Promise} - Updated user */ async usersEdit(root, args, { user }) { const { @@ -120,4 +120,22 @@ export default { return updatedUser; }, + + /* + * Remove user + * @param {String} _id - User _id + * @return {Promise} - Remove user response + */ + async usersRemove(root, { _id }, { user }) { + if (!user) throw new Error('Login required'); + + const userToRemove = await Users.findOne({ _id }); + + // can not remove owner + if (userToRemove.isOwner) { + throw new Error('Can not remove owner'); + } + + return Users.removeUser(_id); + }, }; diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 264d2f957..9a292ace8 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -139,7 +139,7 @@ class User { * @param {String} _id - User id * @return {Promise} - remove method response */ - static async removeUser(_id) { + static removeUser(_id) { return Users.remove({ _id }); } From e53df57398c24ece97b175f9affa6e98e0d31f85 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 3 Nov 2017 14:34:41 +0800 Subject: [PATCH 161/318] Complete usersRemove --- src/__tests__/userMutations.test.js | 39 ++++++++++++++++++++++++++- src/data/resolvers/mutations/users.js | 9 +++++++ src/data/schema/user.js | 1 + 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js index e11e01536..a7013bf59 100644 --- a/src/__tests__/userMutations.test.js +++ b/src/__tests__/userMutations.test.js @@ -3,7 +3,7 @@ import { connect, disconnect } from '../db/connection'; import { Users, Channels } from '../db/models'; -import { userFactory } from '../db/factories'; +import { userFactory, channelFactory } from '../db/factories'; import userMutations from '../data/resolvers/mutations/users'; import utils from '../data/utils'; @@ -17,6 +17,7 @@ describe('User mutations', () => { afterEach(async () => { // Clearing test data await Users.remove({}); + await Channels.remove({}); }); test('Login', async () => { @@ -174,4 +175,40 @@ describe('User mutations', () => { expect(e.message).toBe('Can not remove owner'); } }); + + test('Users remove: can not remove user who created some channels', async () => { + expect.assertions(1); + + const userToRemove = await userFactory({}); + await channelFactory({ userId: userToRemove._id }); + + try { + await userMutations.usersRemove({}, { _id: userToRemove._id }, { user }); + } catch (e) { + expect(e.message).toBe('You cannot delete this user. This user belongs other channel.'); + } + }); + + test('Users remove: can not remove user who involved some channels', async () => { + expect.assertions(1); + + const userToRemove = await userFactory({}); + await channelFactory({ memberIds: ['DFAFSFDSFDS', userToRemove._id] }); + + try { + await userMutations.usersRemove({}, { _id: userToRemove._id }, { user }); + } catch (e) { + expect(e.message).toBe('You cannot delete this user. This user belongs other channel.'); + } + }); + + test('Users remove: successful', async () => { + const removeUser = await userFactory({}); + const removeUserId = removeUser._id; + + await userMutations.usersRemove({}, { _id: removeUserId }, { user }); + + // ensure removed + expect(await Users.findOne({ _id: removeUserId })).toBe(null); + }); }); diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index fdd6209df..7cab511ea 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -136,6 +136,15 @@ export default { throw new Error('Can not remove owner'); } + // if the user involved in any channel then can not delete this user + if ((await Channels.find({ userId: userToRemove._id }).count()) > 0) { + throw new Error('You cannot delete this user. This user belongs other channel.'); + } + + if ((await Channels.find({ memberIds: { $in: [userToRemove._id] } }).count()) > 0) { + throw new Error('You cannot delete this user. This user belongs other channel.'); + } + return Users.removeUser(_id); }, }; diff --git a/src/data/schema/user.js b/src/data/schema/user.js index f3ad87246..83780898f 100644 --- a/src/data/schema/user.js +++ b/src/data/schema/user.js @@ -43,4 +43,5 @@ export const mutations = ` resetPassword(token: String!, newPassword: String!): String usersAdd(${commonParams}): User usersEdit(_id: String!, ${commonParams}): User + usersRemove(_id: String): String `; From f16bd7eb08611674b825d4c54ee570da5d83ac98 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 15:53:32 +0800 Subject: [PATCH 162/318] Add knowledge base use authentication --- src/auth.js | 1 - src/data/index.js | 2 +- src/data/permissions.js | 42 +++++++++++-- src/data/resolvers/mutations/channels.js | 36 +++-------- src/data/resolvers/mutations/knowledgeBase.js | 61 ++++--------------- src/data/resolvers/queries/knowledgeBase.js | 14 ++--- src/data/resolvers/queries/users.js | 22 ++----- src/data/resolvers/queries/utils.js | 18 ------ src/index.js | 4 ++ 9 files changed, 72 insertions(+), 128 deletions(-) delete mode 100644 src/data/resolvers/queries/utils.js diff --git a/src/auth.js b/src/auth.js index 9f80d42d3..28a8ff1ab 100644 --- a/src/auth.js +++ b/src/auth.js @@ -36,6 +36,5 @@ export const userMiddleware = async (req, res, next) => { } } - console.log('middleware req.user: ', req.user); next(); }; diff --git a/src/data/index.js b/src/data/index.js index f74730478..5e8b2adac 100644 --- a/src/data/index.js +++ b/src/data/index.js @@ -1,6 +1,6 @@ import { makeExecutableSchema } from 'graphql-tools'; import resolvers from './resolvers'; -import { types, queries, mutations, subscriptions } from './schema'; +import { types, queries, mutations, subscriptions, permissions } from './schema'; export default makeExecutableSchema({ typeDefs: [types, queries, mutations, subscriptions], diff --git a/src/data/permissions.js b/src/data/permissions.js index 4c8a92d74..686b5a3dd 100644 --- a/src/data/permissions.js +++ b/src/data/permissions.js @@ -1,21 +1,51 @@ export const requireLogin = (cls, methodName) => { + console.log('bbb'); const oldMethod = cls[methodName]; - console.log('oldMethod: ', oldMethod); - console.log('cls: ', cls); - console.log('methodName: ', methodName); - cls[methodName] = (root, object2, { user }) => { if (!user) { throw new Error('Login required'); } + console.log('ccc'); return oldMethod(root, object2, { user }); }; - console.log('cls[methodName]: ', cls[methodName]); - console.log('cls2: ', cls); }; +export const moduleRequireLogin = mdl => { + for (let method in mdl) { + requireLogin(mdl, method); + } +}; + +// import mutations from './resolvers/mutations'; +// import queries from './resolvers/queries'; +// import knowledgeBaseQueries from './resolvers/queries/knowledgeBase'; +// import knowledgeBaseMutations from './resolvers/mutations/knowledgeBase'; + +// export const setAuthPermissions = () => { +// console.log('setAuthPermissions'); +// for (let mutation in mutations) { +// requireLogin(mutations, mutation); +// } +// +// for (let query in queries) { +// requireLogin(queries, query); +// } +// +// for (let query in knowledgeBaseQueries) { +// requireLogin(knowledgeBaseQueries, query); +// } +// +// for (let query in knowledgeBaseMutations) { +// requireLogin(knowledgeBaseMutations, query); +// } +// } + +// setAuthPermissions(); + export default { requireLogin, + moduleRequireLogin, + // setAuthPermissions, }; diff --git a/src/data/resolvers/mutations/channels.js b/src/data/resolvers/mutations/channels.js index 87cb21a26..733de3026 100644 --- a/src/data/resolvers/mutations/channels.js +++ b/src/data/resolvers/mutations/channels.js @@ -1,7 +1,6 @@ import { MODULES } from '../../constants'; import { Channels } from '../../../db/models'; import utils from '../../utils'; -import permissions from '../../permissions'; /** * Send notification to all members of this channel except the sender @@ -26,7 +25,7 @@ export const sendChannelNotifications = async channel => { }); }; -const channelMutations = { +export default { /** * Create a new channel and send notifications to its members bar the creator * @param {Object} root @@ -38,13 +37,8 @@ const channelMutations = { * @param {Object} object3 - Graphql input data * @param {Object|string} user - User making this action * @return {Promise} return Promise resolving created Channel document - * @throws {Error} throws Error('Login required') if user is not logged in */ async channelsAdd(root, doc, { user }) { - if (!user) { - throw new Error('Login required'); - } - const channel = await Channels.createChannel(doc, user); await sendChannelNotifications(channel); @@ -55,22 +49,17 @@ const channelMutations = { /** * Update channel data * @param {Object} root - * @param {string} doc - Channel object - * @param {string} doc._id - Channel id - * @param {string} doc.name - Channel name - * @param {string} doc.description - Channel description - * @param {string[]} doc.memberIds - Members assigned to this channel - * @param {string[]} doc.integrationIds - Integration related to this channel + * @param {string} object2 - Channel object + * @param {string} object2._id - Channel id + * @param {string} object2.name - Channel name + * @param {string} object2.description - Channel description + * @param {string[]} object2.memberIds - Members assigned to this channel + * @param {string[]} object2.integrationIds - Integration related to this channel * @param {Object} object3 - Graphql input data * @param {Object|string} object3.user - user making this action * @return {Promise} return Promise resolving the updated Channel document - * @throws {Error} throws Error('Login required') if user is not logged in */ - async channelsEdit(root, { _id, ...doc }, { user }) { - if (!user) { - throw new Error('Login required'); - } - + async channelsEdit(root, { _id, ...doc }) { const channel = Channels.updateChannel(_id, doc); await sendChannelNotifications(channel); @@ -86,19 +75,10 @@ const channelMutations = { * @param {string} object3 - Middleware data * @param {Object|String} object3.user - User making this action * @return {Promise} - * @throws {Error} throws Error('Login required') if user is not logged in */ async channelsRemove(root, { _id }, { user }) { - if (!user) { - throw new Error('Login required'); - } - await Channels.removeChannel(_id); return _id; }, }; - -permissions.requireLogin(channelMutations, 'channelsAdd'); - -export default channelMutations; diff --git a/src/data/resolvers/mutations/knowledgeBase.js b/src/data/resolvers/mutations/knowledgeBase.js index 43a035daa..82f4f384c 100644 --- a/src/data/resolvers/mutations/knowledgeBase.js +++ b/src/data/resolvers/mutations/knowledgeBase.js @@ -4,7 +4,11 @@ import { KnowledgeBaseArticles, } from '../../../db/models'; -export default { +import { moduleRequireLogin } from '../../permissions'; + +console.log('moduleRequireLogin: ', moduleRequireLogin); + +const knowledgeBaseMutations = { /** * Create topic document * @param {Object} root @@ -13,13 +17,8 @@ export default { * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - returns Promise resolving created document - * @throws {Error} - throws Error('Login required') if user object is not supplied */ knowledgeBaseTopicsAdd(root, { doc }, { user }) { - if (!user) { - throw new Error('Login required'); - } - return KnowledgeBaseTopics.createDoc(doc, user._id); }, @@ -32,13 +31,8 @@ export default { * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - returns Promise resolving modified document - * @throws {Error} - throws Error('Login required') if user object is not supplied */ knowledgeBaseTopicsEdit(root, { _id, doc }, { user }) { - if (!user) { - throw new Error('Login required'); - } - return KnowledgeBaseTopics.updateDoc(_id, doc, user._id); }, @@ -50,13 +44,8 @@ export default { * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - * @throws {Error} - throws Error('Login required') if user object is not supplied */ - knowledgeBaseTopicsRemove(root, { _id }, { user }) { - if (!user) { - throw new Error('Login required'); - } - + knowledgeBaseTopicsRemove(root, { _id }) { return KnowledgeBaseTopics.removeDoc(_id); }, @@ -68,13 +57,8 @@ export default { * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - returns Promise resolving created document - * @throws {Error} - throws Error('Login required') if user object is not supplied */ knowledgeBaseCategoriesAdd(root, { doc }, { user }) { - if (!user) { - throw new Error('Login required'); - } - return KnowledgeBaseCategories.createDoc(doc, user._id); }, @@ -87,13 +71,8 @@ export default { * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - returns Promise resolving modified document - * @throws {Error} - throws Error('Login required') if user object is not supplied */ knowledgeBaseCategoriesEdit(root, { _id, doc }, { user }) { - if (!user) { - throw new Error('Login required'); - } - return KnowledgeBaseCategories.updateDoc(_id, doc, user._id); }, @@ -105,13 +84,8 @@ export default { * @param {Object} object3 - Graphql input data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - * @throws {Error} - throws Error('Login required') if user object is not supplied */ - knowledgeBaseCategoriesRemove(root, { _id }, { user }) { - if (!user) { - throw new Error('Login required'); - } - + knowledgeBaseCategoriesRemove(root, { _id }) { return KnowledgeBaseCategories.removeDoc(_id); }, @@ -123,13 +97,8 @@ export default { * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - returns Promise resolving created document - * @throws {Error} - throws Error('Login required') if user object is not supplied */ knowledgeBaseArticlesAdd(root, { doc }, { user }) { - if (!user) { - throw new Error('Login required'); - } - return KnowledgeBaseArticles.createDoc(doc, user._id); }, @@ -142,13 +111,8 @@ export default { * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - returns Promise resolving modified document - * @throws {Error} - throws Error('Login required') if user object is not supplied */ knowledgeBaseArticlesEdit(root, { _id, doc }, { user }) { - if (!user) { - throw new Error('Login required'); - } - return KnowledgeBaseArticles.updateDoc(_id, doc, user._id); }, @@ -160,13 +124,12 @@ export default { * @param {Object} object3 - Graphql input data * @param {Object} object3.user - User object supplied by middleware * @return {Promise} - * @throws {Error} - throws Error('Login required') if user object is not supplied */ - knowledgeBaseArticlesRemove(root, { _id }, { user }) { - if (!user) { - throw new Error('Login required'); - } - + knowledgeBaseArticlesRemove(root, { _id }) { return KnowledgeBaseArticles.removeDoc(_id); }, }; + +moduleRequireLogin(knowledgeBaseMutations); + +export default knowledgeBaseMutations; diff --git a/src/data/resolvers/queries/knowledgeBase.js b/src/data/resolvers/queries/knowledgeBase.js index d98ba9a99..2a5a03ad8 100644 --- a/src/data/resolvers/queries/knowledgeBase.js +++ b/src/data/resolvers/queries/knowledgeBase.js @@ -4,9 +4,10 @@ import { KnowledgeBaseArticles, } from '../../../db/models'; -import { BasicPermissions } from './utils'; +import { moduleRequireLogin } from '../../permissions'; +console.log('moduleRequireLogin: ', moduleRequireLogin); -const knowledgeBase = { +const knowledgeBaseQueries = { /** * Article list * @param {Object} args @@ -14,7 +15,8 @@ const knowledgeBase = { * @return {Promise} sorted article list */ knowledgeBaseArticles(root, { limit }) { - console.log('aaaa'); + console.log('aaa'); + const articles = KnowledgeBaseArticles.find({}); const sort = { createdDate: -1 }; @@ -22,7 +24,6 @@ const knowledgeBase = { return articles.sort(sort).limit(limit); } - console.log('bbbb'); return articles.sort(sort); }, @@ -117,7 +118,6 @@ const knowledgeBase = { }, }; -BasicPermissions.setPermissionsForList(knowledgeBase, 'knowledgeBaseArticles'); -console.log('knowledgeBase.knowledgeBaseArticles: ', knowledgeBase.knowledgeBaseArticles); +moduleRequireLogin(knowledgeBaseQueries); -export default knowledgeBase; +export default knowledgeBaseQueries; diff --git a/src/data/resolvers/queries/users.js b/src/data/resolvers/queries/users.js index d03b833ed..25d900f74 100644 --- a/src/data/resolvers/queries/users.js +++ b/src/data/resolvers/queries/users.js @@ -1,7 +1,6 @@ import { Users } from '../../../db/models'; -import { BasicPermissions } from './utils'; -const users = { +export default { /** * Users list * @param {Object} args @@ -10,7 +9,7 @@ const users = { * @param {Object} object3.user - User making this request * @return {Promise} sorted and filtered users objects */ - users(root, { limit }, { user }) { + users(root, { limit }) { const users = Users.find({}); const sort = { username: 1 }; @@ -29,11 +28,7 @@ const users = { * @param {Object} object3.user - User making this request * @return {Promise} found user */ - userDetail(root, { _id }, { user }) { - if (!user) { - return {}; - } - + userDetail(root, { _id }) { return Users.findOne({ _id }); }, @@ -43,11 +38,7 @@ const users = { * @param {Object} object3.user - User making this request * @return {Promise} total count */ - usersTotalCount(root, object2, { user }) { - if (!user) { - return 0; - } - + usersTotalCount() { return Users.find({}).count(); }, @@ -59,8 +50,3 @@ const users = { return user; }, }; - -BasicPermissions.setPermissionsForList(users, 'users'); -console.log('users.users: ', users.users); - -export default users; diff --git a/src/data/resolvers/queries/utils.js b/src/data/resolvers/queries/utils.js deleted file mode 100644 index 6e58f1e05..000000000 --- a/src/data/resolvers/queries/utils.js +++ /dev/null @@ -1,18 +0,0 @@ -export class BasicPermissions { - static setPermissionsForList(cls, methodName) { - const oldMethod = cls[methodName]; - - cls[methodName] = (root, object2, { user }) => { - console.log('user: ', user); - if (!user) { - return []; - } - - return oldMethod(root, object2, { user }); - }; - } -} - -export default { - BasicPermissions, -}; diff --git a/src/index.js b/src/index.js index 33a1fd0cb..2f20efc91 100755 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ import { connect } from './db/connection'; import { userMiddleware } from './auth'; import schema from './data'; import './cronJobs'; +import { setAuthPermissions } from './data/permissions'; // load environment variables dotenv.config(); @@ -85,3 +86,6 @@ if (process.env.NODE_ENV === 'development') { }), ); } + +// set user login required wrapper for all queries and mutations +setAuthPermissions(); From 9c6c2f5b90ee66974bec15aa066613a753aa50f0 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 18:55:12 +0800 Subject: [PATCH 163/318] #21 Add knowledgeBaseQueries.test.js --- src/__tests__/knowledgeBaseQueries.test.js | 29 +++++++++++++++++++ src/data/index.js | 2 +- src/data/permissions.js | 29 ------------------- src/data/resolvers/mutations/knowledgeBase.js | 2 -- src/data/resolvers/queries/knowledgeBase.js | 3 -- 5 files changed, 30 insertions(+), 35 deletions(-) create mode 100644 src/__tests__/knowledgeBaseQueries.test.js diff --git a/src/__tests__/knowledgeBaseQueries.test.js b/src/__tests__/knowledgeBaseQueries.test.js new file mode 100644 index 000000000..d10fd613f --- /dev/null +++ b/src/__tests__/knowledgeBaseQueries.test.js @@ -0,0 +1,29 @@ +/* eslint-env jest */ + +import knowledgeBaseQueries from '../data/resolvers/queries/knowledgeBase'; + +describe('knowledgeBaseQueries', () => { + test(`test if Error('Login required') error is working as intended`, async () => { + expect.assertions(9); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(knowledgeBaseQueries.knowledgeBaseArticles); + expectError(knowledgeBaseQueries.knowledgeBaseArticlesDetail); + expectError(knowledgeBaseQueries.knowledgeBaseArticlesTotalCount); + + expectError(knowledgeBaseQueries.knowledgeBaseCategories); + expectError(knowledgeBaseQueries.knowledgeBaseCategoriesDetail); + expectError(knowledgeBaseQueries.knowledgeBaseCategoriesTotalCount); + + expectError(knowledgeBaseQueries.knowledgeBaseTopics); + expectError(knowledgeBaseQueries.knowledgeBaseTopicsDetail); + expectError(knowledgeBaseQueries.knowledgeBaseTopicsTotalCount); + }); +}); diff --git a/src/data/index.js b/src/data/index.js index 5e8b2adac..f74730478 100644 --- a/src/data/index.js +++ b/src/data/index.js @@ -1,6 +1,6 @@ import { makeExecutableSchema } from 'graphql-tools'; import resolvers from './resolvers'; -import { types, queries, mutations, subscriptions, permissions } from './schema'; +import { types, queries, mutations, subscriptions } from './schema'; export default makeExecutableSchema({ typeDefs: [types, queries, mutations, subscriptions], diff --git a/src/data/permissions.js b/src/data/permissions.js index 686b5a3dd..1a2980cf0 100644 --- a/src/data/permissions.js +++ b/src/data/permissions.js @@ -1,5 +1,4 @@ export const requireLogin = (cls, methodName) => { - console.log('bbb'); const oldMethod = cls[methodName]; cls[methodName] = (root, object2, { user }) => { @@ -7,7 +6,6 @@ export const requireLogin = (cls, methodName) => { throw new Error('Login required'); } - console.log('ccc'); return oldMethod(root, object2, { user }); }; }; @@ -18,34 +16,7 @@ export const moduleRequireLogin = mdl => { } }; -// import mutations from './resolvers/mutations'; -// import queries from './resolvers/queries'; -// import knowledgeBaseQueries from './resolvers/queries/knowledgeBase'; -// import knowledgeBaseMutations from './resolvers/mutations/knowledgeBase'; - -// export const setAuthPermissions = () => { -// console.log('setAuthPermissions'); -// for (let mutation in mutations) { -// requireLogin(mutations, mutation); -// } -// -// for (let query in queries) { -// requireLogin(queries, query); -// } -// -// for (let query in knowledgeBaseQueries) { -// requireLogin(knowledgeBaseQueries, query); -// } -// -// for (let query in knowledgeBaseMutations) { -// requireLogin(knowledgeBaseMutations, query); -// } -// } - -// setAuthPermissions(); - export default { requireLogin, moduleRequireLogin, - // setAuthPermissions, }; diff --git a/src/data/resolvers/mutations/knowledgeBase.js b/src/data/resolvers/mutations/knowledgeBase.js index 82f4f384c..e27478534 100644 --- a/src/data/resolvers/mutations/knowledgeBase.js +++ b/src/data/resolvers/mutations/knowledgeBase.js @@ -6,8 +6,6 @@ import { import { moduleRequireLogin } from '../../permissions'; -console.log('moduleRequireLogin: ', moduleRequireLogin); - const knowledgeBaseMutations = { /** * Create topic document diff --git a/src/data/resolvers/queries/knowledgeBase.js b/src/data/resolvers/queries/knowledgeBase.js index 2a5a03ad8..1cd3d3f6b 100644 --- a/src/data/resolvers/queries/knowledgeBase.js +++ b/src/data/resolvers/queries/knowledgeBase.js @@ -5,7 +5,6 @@ import { } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; -console.log('moduleRequireLogin: ', moduleRequireLogin); const knowledgeBaseQueries = { /** @@ -15,8 +14,6 @@ const knowledgeBaseQueries = { * @return {Promise} sorted article list */ knowledgeBaseArticles(root, { limit }) { - console.log('aaa'); - const articles = KnowledgeBaseArticles.find({}); const sort = { createdDate: -1 }; From 78189abd796c6f90446d341e5ab2c2177d1b75ec Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 19:08:04 +0800 Subject: [PATCH 164/318] #21 Add userQueries.test.js --- src/__tests__/userQueries.test.js | 22 ++++++++++++++++++++++ src/data/resolvers/mutations/users.js | 11 +++++++---- src/data/resolvers/queries/users.js | 8 +++++++- 3 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/userQueries.test.js diff --git a/src/__tests__/userQueries.test.js b/src/__tests__/userQueries.test.js new file mode 100644 index 000000000..758c4fea9 --- /dev/null +++ b/src/__tests__/userQueries.test.js @@ -0,0 +1,22 @@ +/* eslint-env jest */ + +import userQueries from '../data/resolvers/queries/users'; + +describe('userQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(4); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(userQueries.users); + expectError(userQueries.userDetail); + expectError(userQueries.usersTotalCount); + expectError(userQueries.currentUser); + }); +}); diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index 8dbc71b62..8f6c2a300 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -1,7 +1,8 @@ import { Users } from '../../../db/models'; import { sendEmail } from '../../../data/utils'; +import { requireLogin } from '../../permissions'; -export default { +const userMutations = { login(root, args) { return Users.login(args); }, @@ -33,11 +34,9 @@ export default { return Users.resetPassword(args); }, - usersAdd(root, args, { user }) { + usersAdd(root, args) { const { username, password, passwordConfirmation, email, role, details } = args; - if (!user) throw new Error('Login required'); - if (password !== passwordConfirmation) { throw new Error('Incorrect password confirmation'); } @@ -45,3 +44,7 @@ export default { return Users.createUser({ username, password, email, role, details }); }, }; + +requireLogin(userMutations, 'usersAdd'); + +export default userMutations; diff --git a/src/data/resolvers/queries/users.js b/src/data/resolvers/queries/users.js index 25d900f74..b611ed5ae 100644 --- a/src/data/resolvers/queries/users.js +++ b/src/data/resolvers/queries/users.js @@ -1,6 +1,8 @@ import { Users } from '../../../db/models'; -export default { +import { moduleRequireLogin } from '../../permissions'; + +const userQueries = { /** * Users list * @param {Object} args @@ -50,3 +52,7 @@ export default { return user; }, }; + +moduleRequireLogin(userQueries); + +export default userQueries; From b773c65be8f62f7c00e8b9b8345146d74a918f6f Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 19:52:41 +0800 Subject: [PATCH 165/318] #21 Add tagQueries.test.js --- src/__tests__/tagQueries.test.js | 19 +++++++++++++++++++ src/data/resolvers/mutations/tags.js | 23 ++++++++++------------- src/data/resolvers/queries/tags.js | 7 ++++++- 3 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 src/__tests__/tagQueries.test.js diff --git a/src/__tests__/tagQueries.test.js b/src/__tests__/tagQueries.test.js new file mode 100644 index 000000000..92aa4a35c --- /dev/null +++ b/src/__tests__/tagQueries.test.js @@ -0,0 +1,19 @@ +/* eslint-env jest */ + +import tagQueries from '../data/resolvers/queries/tags'; + +describe('tagQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(1); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(tagQueries.tags); + }); +}); diff --git a/src/data/resolvers/mutations/tags.js b/src/data/resolvers/mutations/tags.js index 27c688be5..96939b7a5 100644 --- a/src/data/resolvers/mutations/tags.js +++ b/src/data/resolvers/mutations/tags.js @@ -1,6 +1,7 @@ import { Tags } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const tagMutations = { /** * Create new tag * @param {String} doc.name @@ -8,9 +9,7 @@ export default { * @param {String} doc.colorCode * @return {Promise} newly created tag object */ - tagsAdd(root, doc, { user }) { - if (!user) throw new Error('Login required'); - + tagsAdd(root, doc) { return Tags.createTag(doc); }, @@ -21,9 +20,7 @@ export default { * @param {String} doc.colorCode * @return {Promise} updated tag object */ - tagsEdit(root, { _id, ...doc }, { user }) { - if (!user) throw new Error('Login required'); - + tagsEdit(root, { _id, ...doc }) { return Tags.updateTag(_id, doc); }, @@ -32,9 +29,7 @@ export default { * @param {[String]} ids * @return {Promise} */ - tagsRemove(root, { ids }, { user }) { - if (!user) throw new Error('Login required'); - + tagsRemove(root, { ids }) { return Tags.removeTag(ids); }, @@ -44,9 +39,11 @@ export default { * @param {[String]} targetIds * @param {[String]} tagIds */ - tagsTag(root, { type, targetIds, tagIds }, { user }) { - if (!user) throw new Error('Login required'); - + tagsTag(root, { type, targetIds, tagIds }) { Tags.tagsTag(type, targetIds, tagIds); }, }; + +moduleRequireLogin(tagMutations); + +export default tagMutations; diff --git a/src/data/resolvers/queries/tags.js b/src/data/resolvers/queries/tags.js index 452ce5d17..6658dd825 100644 --- a/src/data/resolvers/queries/tags.js +++ b/src/data/resolvers/queries/tags.js @@ -1,6 +1,7 @@ import { Tags } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const tagQueries = { /** * Tags list * @param {Object} args @@ -11,3 +12,7 @@ export default { return Tags.find({ type }); }, }; + +moduleRequireLogin(tagQueries); + +export default tagQueries; From 66c17299a6bb45675d665f6136ea3febbead3a07 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 3 Nov 2017 19:55:11 +0800 Subject: [PATCH 166/318] Add userProfile --- src/__tests__/userMutations.test.js | 37 ++++++++++++++++++++++++++- src/data/resolvers/mutations/users.js | 20 +++++++++++++++ src/data/resolvers/queries/users.js | 2 +- src/data/schema/user.js | 8 ++++++ 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js index a7013bf59..6b899de78 100644 --- a/src/__tests__/userMutations.test.js +++ b/src/__tests__/userMutations.test.js @@ -59,7 +59,7 @@ describe('User mutations', () => { } }; - expect.assertions(3); + expect.assertions(4); // users add checkLogin(userMutations.usersAdd, {}); @@ -67,6 +67,9 @@ describe('User mutations', () => { // users edit checkLogin(userMutations.usersEdit, {}); + // users edit profile + checkLogin(userMutations.usersEditProfile, {}); + // users remove checkLogin(userMutations.usersRemove, {}); }); @@ -164,6 +167,38 @@ describe('User mutations', () => { expect(Channels.updateUserChannels).toBeCalledWith(channelIds, userId); }); + test('Users edit profile: invalid password', async () => { + expect.assertions(1); + + const user = await userFactory({ password: 'p' }); + + try { + await userMutations.usersEditProfile({}, { password: 'password' }, { user }); + } catch (e) { + expect(e.message).toBe('Invalid password'); + } + }); + + test('Users edit profile: successfull', async () => { + const user = await userFactory({}); + + Users.editProfile = jest.fn(); + + const doc = { + username: 'username', + email: 'info@erxes.io', + details: { + fullName: 'fullName', + twitterUsername: 'twitterUsername', + position: 'position', + }, + }; + + await userMutations.usersEditProfile({}, { ...doc, password: 'Dombo@123' }, { user }); + + expect(Users.editProfile).toBeCalledWith(user._id, doc); + }); + test('Users remove: can not delete owner', async () => { expect.assertions(1); diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index 7cab511ea..9a8a14612 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -1,3 +1,4 @@ +import bcrypt from 'bcrypt'; import { Users, Channels } from '../../../db/models'; import utils from '../../../data/utils'; @@ -121,6 +122,25 @@ export default { return updatedUser; }, + /* + * Edit user profile + * @param {Object} args - User profile doc + * @return {Promise} - Updated user + */ + async usersEditProfile(root, { username, email, password, details }, { user }) { + if (!user) throw new Error('Login required'); + + const userOnDb = await Users.findOne({ _id: user._id }); + const valid = await bcrypt.compare(password, userOnDb.password); + + if (!password || !valid) { + // bad password + throw new Error('Invalid password'); + } + + return Users.editProfile(user._id, { username, email, details }); + }, + /* * Remove user * @param {String} _id - User _id diff --git a/src/data/resolvers/queries/users.js b/src/data/resolvers/queries/users.js index ddf6367e1..d11249fba 100644 --- a/src/data/resolvers/queries/users.js +++ b/src/data/resolvers/queries/users.js @@ -41,6 +41,6 @@ export default { * @return {Promise} total count */ currentUser(root, args, { user }) { - return user; + return Users.findOne({ _id: user._id }); }, }; diff --git a/src/data/schema/user.js b/src/data/schema/user.js index 83780898f..870e20d02 100644 --- a/src/data/schema/user.js +++ b/src/data/schema/user.js @@ -43,5 +43,13 @@ export const mutations = ` resetPassword(token: String!, newPassword: String!): String usersAdd(${commonParams}): User usersEdit(_id: String!, ${commonParams}): User + + usersEditProfile( + username: String!, + email: String!, + details: UserDetails, + password: String! + ): User + usersRemove(_id: String): String `; From bbe4821894375a9cc4077d29cb59b4fd04686a7d Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 20:04:10 +0800 Subject: [PATCH 167/318] #21 Add segmentQueries.test.js --- src/__tests__/segmentQueries.test.js | 21 +++++++++++++++++++++ src/data/resolvers/mutations/segments.js | 20 ++++++++++---------- src/data/resolvers/queries/segments.js | 8 +++++++- 3 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/segmentQueries.test.js diff --git a/src/__tests__/segmentQueries.test.js b/src/__tests__/segmentQueries.test.js new file mode 100644 index 000000000..a70d3c9ec --- /dev/null +++ b/src/__tests__/segmentQueries.test.js @@ -0,0 +1,21 @@ +/* eslint-env jest */ + +import segmentQueries from '../data/resolvers/queries/segments'; + +describe('segmentQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(3); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(segmentQueries.segments); + expectError(segmentQueries.segmentsGetHeads); + expectError(segmentQueries.segmentDetail); + }); +}); diff --git a/src/data/resolvers/mutations/segments.js b/src/data/resolvers/mutations/segments.js index 92f82cd37..fbafd0553 100644 --- a/src/data/resolvers/mutations/segments.js +++ b/src/data/resolvers/mutations/segments.js @@ -1,13 +1,13 @@ import { Segments } from '../../../db/models'; -export default { +import { moduleRequireLogin } from '../../permissions'; + +const segmentQueries = { /** * Create new segment * @return {Promise} segment object */ - segmentsAdd(root, doc, { user }) { - if (!user) throw new Error('Login required'); - + segmentsAdd(root, doc) { return Segments.createSegment(doc); }, @@ -15,9 +15,7 @@ export default { * Update segment * @return {Promise} segment object */ - async segmentsEdit(root, { _id, ...doc }, { user }) { - if (!user) throw new Error('Login required'); - + async segmentsEdit(root, { _id, ...doc }) { return Segments.updateSegment(_id, doc); }, @@ -25,9 +23,11 @@ export default { * Delete segment * @return {Promise} */ - async segmentsRemove(root, { _id }, { user }) { - if (!user) throw new Error('Login required'); - + async segmentsRemove(root, { _id }) { return Segments.removeSegment(_id); }, }; + +moduleRequireLogin(segmentQueries); + +export default segmentQueries; diff --git a/src/data/resolvers/queries/segments.js b/src/data/resolvers/queries/segments.js index eb4ba9382..ed7178d15 100644 --- a/src/data/resolvers/queries/segments.js +++ b/src/data/resolvers/queries/segments.js @@ -1,6 +1,8 @@ import { Segments } from '../../../db/models'; -export default { +import { moduleRequireLogin } from '../../permissions'; + +const segmentMutations = { /** * Segments list * @return {Promise} segment objects @@ -27,3 +29,7 @@ export default { return Segments.findOne({ _id }); }, }; + +moduleRequireLogin(segmentMutations); + +export default segmentMutations; From 3cb52dcd7628405a85b5321f87465a022ad6bffe Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 20:15:55 +0800 Subject: [PATCH 168/318] #21 Change knowledgeBase queries detail names --- src/__tests__/knowledgeBaseQueries.test.js | 6 +++--- src/data/resolvers/queries/knowledgeBase.js | 6 +++--- src/data/schema/knowledgeBase.js | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/__tests__/knowledgeBaseQueries.test.js b/src/__tests__/knowledgeBaseQueries.test.js index d10fd613f..0f57df766 100644 --- a/src/__tests__/knowledgeBaseQueries.test.js +++ b/src/__tests__/knowledgeBaseQueries.test.js @@ -15,15 +15,15 @@ describe('knowledgeBaseQueries', () => { }; expectError(knowledgeBaseQueries.knowledgeBaseArticles); - expectError(knowledgeBaseQueries.knowledgeBaseArticlesDetail); + expectError(knowledgeBaseQueries.knowledgeBaseArticleDetail); expectError(knowledgeBaseQueries.knowledgeBaseArticlesTotalCount); expectError(knowledgeBaseQueries.knowledgeBaseCategories); - expectError(knowledgeBaseQueries.knowledgeBaseCategoriesDetail); + expectError(knowledgeBaseQueries.knowledgeBaseCategoryDetail); expectError(knowledgeBaseQueries.knowledgeBaseCategoriesTotalCount); expectError(knowledgeBaseQueries.knowledgeBaseTopics); - expectError(knowledgeBaseQueries.knowledgeBaseTopicsDetail); + expectError(knowledgeBaseQueries.knowledgeBaseTopicDetail); expectError(knowledgeBaseQueries.knowledgeBaseTopicsTotalCount); }); }); diff --git a/src/data/resolvers/queries/knowledgeBase.js b/src/data/resolvers/queries/knowledgeBase.js index 1cd3d3f6b..374772f6e 100644 --- a/src/data/resolvers/queries/knowledgeBase.js +++ b/src/data/resolvers/queries/knowledgeBase.js @@ -30,7 +30,7 @@ const knowledgeBaseQueries = { * @param {String} args._id * @return {Promise} article detail */ - knowledgeBaseArticlesDetail(root, { _id }) { + knowledgeBaseArticleDetail(root, { _id }) { return KnowledgeBaseArticles.findOne({ _id }); }, @@ -65,7 +65,7 @@ const knowledgeBaseQueries = { * @param {String} args._id * @return {Promise} category detail */ - knowledgeBaseCategoriesDetail(root, { _id }) { + knowledgeBaseCategoryDetail(root, { _id }) { return KnowledgeBaseCategories.findOne({ _id }).then(category => { return category; }); @@ -102,7 +102,7 @@ const knowledgeBaseQueries = { * @param {String} args._id * @return {Promise} topic detail */ - knowledgeBaseTopicsDetail(root, { _id }) { + knowledgeBaseTopicDetail(root, { _id }) { return KnowledgeBaseTopics.findOne({ _id }); }, diff --git a/src/data/schema/knowledgeBase.js b/src/data/schema/knowledgeBase.js index 8ed989599..70e365342 100644 --- a/src/data/schema/knowledgeBase.js +++ b/src/data/schema/knowledgeBase.js @@ -59,15 +59,15 @@ export const types = ` export const queries = ` knowledgeBaseTopics(limit: Int): [KnowledgeBaseTopic] - knowledgeBaseTopicsDetail(_id: String!): KnowledgeBaseTopic + knowledgeBaseTopicDetail(_id: String!): KnowledgeBaseTopic knowledgeBaseTopicsTotalCount: Int knowledgeBaseCategories(limit: Int): [KnowledgeBaseCategory] - knowledgeBaseCategoriesDetail(_id: String!): KnowledgeBaseCategory + knowledgeBaseCategoryDetail(_id: String!): KnowledgeBaseCategory knowledgeBaseCategoriesTotalCount: Int knowledgeBaseArticles(limit: Int): [KnowledgeBaseArticle] - knowledgeBaseArticlesDetail(_id: String!): KnowledgeBaseArticle + knowledgeBaseArticleDetail(_id: String!): KnowledgeBaseArticle knowledgeBaseArticlesTotalCount: Int `; From aea4a072d710ee7ca7f2de0dfa5d8b469574e2a1 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 20:24:31 +0800 Subject: [PATCH 169/318] #21 Add responseTemplateQueries.test.js --- src/__tests__/responseTemplateQueries.test.js | 20 +++++++++++++++++++ .../resolvers/mutations/responseTemplates.js | 19 +++++++++--------- .../resolvers/queries/responseTemplates.js | 7 ++++++- 3 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/responseTemplateQueries.test.js diff --git a/src/__tests__/responseTemplateQueries.test.js b/src/__tests__/responseTemplateQueries.test.js new file mode 100644 index 000000000..2989c075b --- /dev/null +++ b/src/__tests__/responseTemplateQueries.test.js @@ -0,0 +1,20 @@ +/* eslint-env jest */ + +import responseTemplateQueries from '../data/resolvers/queries/responseTemplates'; + +describe('responseTemplateQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(2); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(responseTemplateQueries.responseTemplates); + expectError(responseTemplateQueries.responseTemplatesTotalCount); + }); +}); diff --git a/src/data/resolvers/mutations/responseTemplates.js b/src/data/resolvers/mutations/responseTemplates.js index b96686648..91e1f0bd4 100644 --- a/src/data/resolvers/mutations/responseTemplates.js +++ b/src/data/resolvers/mutations/responseTemplates.js @@ -1,14 +1,13 @@ import { ResponseTemplates } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const responseTemplateMutations = { /** * Create new response template * @param {Object} fields - response template fields * @return {Promise} newly created response template object */ - responseTemplatesAdd(root, doc, { user }) { - if (!user) throw new Error('Login required'); - + responseTemplatesAdd(root, doc) { return ResponseTemplates.create(doc); }, @@ -18,9 +17,7 @@ export default { * @param {Object} fields - response template fields * @return {Promise} updated response template object */ - responseTemplatesEdit(root, { _id, ...fields }, { user }) { - if (!user) throw new Error('Login required'); - + responseTemplatesEdit(root, { _id, ...fields }) { return ResponseTemplates.updateResponseTemplate(_id, fields); }, @@ -29,9 +26,11 @@ export default { * @param {String} _id - response template id * @return {Promise} */ - responseTemplatesRemove(root, { _id }, { user }) { - if (!user) throw new Error('Login required'); - + responseTemplatesRemove(root, { _id }) { return ResponseTemplates.removeResponseTemplate(_id); }, }; + +moduleRequireLogin(responseTemplateMutations); + +export default responseTemplateMutations; diff --git a/src/data/resolvers/queries/responseTemplates.js b/src/data/resolvers/queries/responseTemplates.js index 3ac4ce02d..6b7397eaa 100644 --- a/src/data/resolvers/queries/responseTemplates.js +++ b/src/data/resolvers/queries/responseTemplates.js @@ -1,6 +1,7 @@ import { ResponseTemplates } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const responseTemplateQueries = { /** * Response templates list * @param {Object} args @@ -25,3 +26,7 @@ export default { return ResponseTemplates.find({}).count(); }, }; + +moduleRequireLogin(responseTemplateQueries); + +export default responseTemplateQueries; From 0f5cbf880261bc7f414b809921c85a8daebbbbc5 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 20:36:12 +0800 Subject: [PATCH 170/318] #21 Add notificationQueries.test.js --- src/__tests__/notificationQueries.test.js | 20 +++++++++++++++++-- src/data/resolvers/mutations/notifications.js | 20 +++++++++---------- src/data/resolvers/queries/notifications.js | 8 +++++++- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/__tests__/notificationQueries.test.js b/src/__tests__/notificationQueries.test.js index c5b2e7c45..91c650624 100644 --- a/src/__tests__/notificationQueries.test.js +++ b/src/__tests__/notificationQueries.test.js @@ -7,9 +7,25 @@ import notificationsQueries from '../data/resolvers/queries/notifications'; beforeAll(() => connect()); afterAll(() => disconnect()); -describe('notification query test', () => { +describe('notificationsQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(1); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(notificationsQueries.notificationsModules); + }); + test('test of getting notification list with success', () => { - const modules = notificationsQueries.notificationsModules(); + const modules = notificationsQueries.notificationsModules(null, null, { + user: { _id: 'fakeUserId' }, + }); expect(modules).toBe(MODULES.ALL); }); }); diff --git a/src/data/resolvers/mutations/notifications.js b/src/data/resolvers/mutations/notifications.js index b3781cbe2..664877fd4 100644 --- a/src/data/resolvers/mutations/notifications.js +++ b/src/data/resolvers/mutations/notifications.js @@ -1,10 +1,12 @@ import { NotificationConfigurations, Notifications } from '../../../db/models'; -export default { +import { moduleRequireLogin } from '../../permissions'; + +const notificationMutations = { /** * Save notification configuration * @param {Object} object - * @param {Object} object - NotificationConfiguration object + * @param {Object} object2 - NotificationConfiguration object * @param {string} object2.notifType - Notification configuration notification type (module) * @param {Boolean} object2.isAllowed - Shows whether notifications will be received or not * @param {Object} object3 - Middleware data @@ -13,10 +15,6 @@ export default { * @throws {Error} throws Error('Login required') if user is not logged in */ notificationsSaveConfig(root, doc, { user }) { - if (!user) { - throw new Error('Login required'); - } - return NotificationConfigurations.createOrUpdateConfiguration(doc, user); }, @@ -30,11 +28,11 @@ export default { * @return {Promise} * @throws {Error} throws Error('Login required') if user is not logged in */ - notificationsMarkAsRead(root, { ids }, { user }) { - if (!user) { - throw new Error('Login required'); - } - + notificationsMarkAsRead(root, { ids }) { return Notifications.markAsRead(ids); }, }; + +moduleRequireLogin(notificationMutations); + +export default notificationMutations; diff --git a/src/data/resolvers/queries/notifications.js b/src/data/resolvers/queries/notifications.js index 15da83c20..250498939 100644 --- a/src/data/resolvers/queries/notifications.js +++ b/src/data/resolvers/queries/notifications.js @@ -1,6 +1,8 @@ import { MODULES } from '../../constants'; -export default { +import { moduleRequireLogin } from '../../permissions'; + +const notificationQueries = { /** * Module list used in notifications * @param {Object} args @@ -10,3 +12,7 @@ export default { return MODULES.ALL; }, }; + +moduleRequireLogin(notificationQueries); + +export default notificationQueries; From d4a5ad478f3dfe19a0d45f999c849f0bbadc951c Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 3 Nov 2017 20:36:58 +0800 Subject: [PATCH 171/318] Add change password --- src/__tests__/userDb.test.js | 24 ++++++++++++++++++++++++ src/__tests__/userMutations.test.js | 20 +++++++++++++++++++- src/data/resolvers/mutations/users.js | 12 ++++++++++++ src/data/schema/user.js | 3 ++- src/db/models/Users.js | 27 +++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index aa2ac9127..61fb8ba7a 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -165,6 +165,30 @@ describe('User db utils', () => { expect(bcrypt.compare('password', user.password)).toBeTruthy(); }); + test('Change password: incorrect current password', async () => { + expect.assertions(1); + + const user = await userFactory({}); + + try { + await Users.changePassword({ _id: user._id, currentPassword: 'p' }); + } catch (e) { + expect(e.message).toBe('Incorrect current password'); + } + }); + + test('Change password: successful', async () => { + const user = await userFactory({}); + + const updatedUser = await Users.changePassword({ + _id: user._id, + currentPassword: 'Dombo@123', + newPassword: 'Lombo@123', + }); + + expect(bcrypt.compare(updatedUser.password, 'Lombo@123')).toBeTruthy(); + }); + test('Forgot password', async () => { expect.assertions(3); diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js index 6b899de78..2d5b12a8a 100644 --- a/src/__tests__/userMutations.test.js +++ b/src/__tests__/userMutations.test.js @@ -50,6 +50,21 @@ describe('User mutations', () => { expect(Users.resetPassword).toBeCalledWith(doc); }); + test('Change password', async () => { + Users.changePassword = jest.fn(); + + const doc = { + currentPassword: 'currentPassword', + newPassword: 'newPassword', + }; + + const user = { _id: 'DFAFASD' }; + + await userMutations.usersChangePassword({}, doc, { user }); + + expect(Users.changePassword).toBeCalledWith({ _id: user._id, ...doc }); + }); + test('Login required checks', async () => { const checkLogin = async (fn, args) => { try { @@ -59,7 +74,10 @@ describe('User mutations', () => { } }; - expect.assertions(4); + expect.assertions(5); + + // users change password + checkLogin(userMutations.usersChangePassword, {}); // users add checkLogin(userMutations.usersAdd, {}); diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index 9a8a14612..8a2021327 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -52,6 +52,18 @@ export default { return Users.resetPassword(args); }, + /* + * Change user password + * @param {String} currentPassword - Current password + * @param {String} newPassword - New password to set + * @return {Promise} - Updated user object + */ + usersChangePassword(root, args, { user }) { + if (!user) throw new Error('Login required'); + + return Users.changePassword({ _id: user._id, ...args }); + }, + /* * Create new user * @param {Object} args - User doc diff --git a/src/data/schema/user.js b/src/data/schema/user.js index 870e20d02..d304d7657 100644 --- a/src/data/schema/user.js +++ b/src/data/schema/user.js @@ -51,5 +51,6 @@ export const mutations = ` password: String! ): User - usersRemove(_id: String): String + usersChangePassword(currentPassword: String!, newPassword: String!): User + usersRemove(_id: String!): String `; diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 9a292ace8..c911939ff 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -202,6 +202,33 @@ class User { return this.findOne({ _id: user._id }); } + /* + * Change user password + * @param {String} currentPassword - Current password + * @param {String} newPassword - New password + * @return {Promise} - Updated user information + */ + static async changePassword({ _id, currentPassword, newPassword }) { + const user = await this.findOne({ _id }); + + // check current password ============ + const valid = await bcrypt.compare(currentPassword, user.password); + + if (!valid) { + throw new Error('Incorrect current password'); + } + + // set new password + await this.findByIdAndUpdate( + { _id: user._id }, + { + password: bcrypt.hashSync(newPassword, 10), + }, + ); + + return this.findOne({ _id: user._id }); + } + /* * Sends reset password link to found user's email * @param {String} email - Registered user's email From ef7072b0b0a159d9e337a37a3b00490751274b09 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 20:45:11 +0800 Subject: [PATCH 172/318] #21 Add internalNoteQueries.test.js --- src/__tests__/internalNoteDb.test.js | 2 +- src/__tests__/internalNoteQueries.test.js | 19 +++++++++++++++++++ src/data/resolvers/mutations/internalNotes.js | 17 ++++++++--------- src/data/resolvers/queries/internalNotes.js | 7 ++++++- 4 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/internalNoteQueries.test.js diff --git a/src/__tests__/internalNoteDb.test.js b/src/__tests__/internalNoteDb.test.js index 176cf87d8..8d33e69bc 100644 --- a/src/__tests__/internalNoteDb.test.js +++ b/src/__tests__/internalNoteDb.test.js @@ -28,7 +28,7 @@ const checkValues = (internalNoteObj, doc) => { expect(internalNoteObj.content).toBe(doc.content); }; -describe('InternalNotes mutations', () => { +describe('InternalNotes model test', () => { let _user; let _internalNote; diff --git a/src/__tests__/internalNoteQueries.test.js b/src/__tests__/internalNoteQueries.test.js new file mode 100644 index 000000000..0d9f0ae1e --- /dev/null +++ b/src/__tests__/internalNoteQueries.test.js @@ -0,0 +1,19 @@ +/* eslint-env jest */ + +import internalNoteQueries from '../data/resolvers/queries/internalNotes'; + +describe('internalNoteQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(1); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(internalNoteQueries.internalNotes); + }); +}); diff --git a/src/data/resolvers/mutations/internalNotes.js b/src/data/resolvers/mutations/internalNotes.js index 6e9bd813e..3c7a358f6 100644 --- a/src/data/resolvers/mutations/internalNotes.js +++ b/src/data/resolvers/mutations/internalNotes.js @@ -1,13 +1,12 @@ import { InternalNotes } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const internalNoteMutations = { /** * Adds internalNote object * @return {Promise} */ internalNotesAdd(root, args, { user }) { - if (!user) throw new Error('Login required'); - return InternalNotes.createInternalNote(args, user); }, @@ -15,9 +14,7 @@ export default { * Updates internalNote object * @return {Promise} return Promise(null) */ - internalNotesEdit(root, { _id, ...doc }, { user }) { - if (!user) throw new Error('Login required'); - + internalNotesEdit(root, { _id, ...doc }) { return InternalNotes.updateInternalNote(_id, doc); }, @@ -25,9 +22,11 @@ export default { * Remove a channel * @return {Promise} */ - internalNotesRemove(root, { _id }, { user }) { - if (!user) throw new Error('Login required'); - + internalNotesRemove(root, { _id }) { return InternalNotes.removeInternalNote(_id); }, }; + +moduleRequireLogin(internalNoteMutations); + +export default internalNoteMutations; diff --git a/src/data/resolvers/queries/internalNotes.js b/src/data/resolvers/queries/internalNotes.js index bec4acd25..6fe2def02 100644 --- a/src/data/resolvers/queries/internalNotes.js +++ b/src/data/resolvers/queries/internalNotes.js @@ -1,6 +1,7 @@ import { InternalNotes } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const internalNoteQueries = { /** * InternalNotes list * @param {Object} args @@ -10,3 +11,7 @@ export default { return InternalNotes.find({ contentType, contentTypeId }).sort({ createdDate: 1 }); }, }; + +moduleRequireLogin(internalNoteQueries); + +export default internalNoteQueries; From 1dfe657e297d30099be852133300fe81b7ec2829 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 20:52:43 +0800 Subject: [PATCH 173/318] #21 add integrationQueries.test.js --- src/__tests__/integrationQueries.test.js | 21 ++++++++ src/data/resolvers/mutations/integrations.js | 56 +++++--------------- src/data/resolvers/queries/integrations.js | 19 ++++--- 3 files changed, 47 insertions(+), 49 deletions(-) create mode 100644 src/__tests__/integrationQueries.test.js diff --git a/src/__tests__/integrationQueries.test.js b/src/__tests__/integrationQueries.test.js new file mode 100644 index 000000000..e0b623fad --- /dev/null +++ b/src/__tests__/integrationQueries.test.js @@ -0,0 +1,21 @@ +/* eslint-env jest */ + +import integrationQueries from '../data/resolvers/queries/integrations'; + +describe('integrationQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(3); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(integrationQueries.integrations); + expectError(integrationQueries.integrationDetail); + expectError(integrationQueries.integrationsTotalCount); + }); +}); diff --git a/src/data/resolvers/mutations/integrations.js b/src/data/resolvers/mutations/integrations.js index 66d6253be..8f9bdf750 100644 --- a/src/data/resolvers/mutations/integrations.js +++ b/src/data/resolvers/mutations/integrations.js @@ -1,6 +1,7 @@ import { Integrations } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const integrationMutations = { /** * Create a new messenger integration * @param {Object} root @@ -10,13 +11,8 @@ export default { * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action * @return {Promise} return Promise resolving Integration document - * @throws {Error} throws Error('Login required') if user is not logged in */ - integrationsCreateMessengerIntegration(root, doc, { user }) { - if (!user) { - throw new Error('Login required'); - } - + integrationsCreateMessengerIntegration(root, doc) { return Integrations.createMessengerIntegration(doc); }, @@ -30,13 +26,8 @@ export default { * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action * @return {Promise} return Promise resolving Integration document - * @throws {Error} throws Error('Login required') if user is not logged in */ - integrationsEditMessengerIntegration(root, { _id, ...fields }, { user }) { - if (!user) { - throw new Error('Login required'); - } - + integrationsEditMessengerIntegration(root, { _id, ...fields }) { return Integrations.updateMessengerIntegration(_id, fields); }, @@ -52,13 +43,8 @@ export default { * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action * @return {Promise} return Promise resolving Integration document - * @throws {Error} throws Error('Login required') if user is not logged in */ - integrationsSaveMessengerAppearanceData(root, { _id, uiOptions }, { user }) { - if (!user) { - throw new Error('Login required'); - } - + integrationsSaveMessengerAppearanceData(root, { _id, uiOptions }) { return Integrations.saveMessengerAppearanceData(_id, uiOptions); }, @@ -72,13 +58,8 @@ export default { * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action * @return {Promise} return Promise resolving Integration document - * @throws {Error} throws Error('Login required') if user is not logged in */ - integrationsSaveMessengerConfigs(root, { _id, messengerData }, { user }) { - if (!user) { - throw new Error('Login required'); - } - + integrationsSaveMessengerConfigs(root, { _id, messengerData }) { return Integrations.saveMessengerConfigs(_id, messengerData); }, @@ -93,13 +74,8 @@ export default { * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action * @return {Promise} return Promise resolving Integration document - * @throws {Error} throws Error('Login required') if user is not logged in */ - integrationsCreateFormIntegration(root, doc, { user }) { - if (!user) { - throw new Error('Login required'); - } - + integrationsCreateFormIntegration(root, doc) { return Integrations.createFormIntegration(doc); }, @@ -115,13 +91,8 @@ export default { * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action * @return {Promise} return Promise resolving Integration document - * @throws {Error} throws Error('Login required') if user is not logged in */ - integrationsEditFormIntegration(root, { _id, ...doc }, { user }) { - if (!user) { - throw new Error('Login required'); - } - + integrationsEditFormIntegration(root, { _id, ...doc }) { return Integrations.updateFormIntegration(_id, doc); }, @@ -133,13 +104,12 @@ export default { * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action * @return {Promise} - * @throws {Error} throws Error('Login required') if user is not logged in */ - integrationsRemove(root, { _id }, { user }) { - if (!user) { - throw new Error('Login required'); - } - + integrationsRemove(root, { _id }) { return Integrations.removeIntegration(_id); }, }; + +moduleRequireLogin(integrationMutations); + +export default integrationMutations; diff --git a/src/data/resolvers/queries/integrations.js b/src/data/resolvers/queries/integrations.js index b3241257d..02feca8fb 100644 --- a/src/data/resolvers/queries/integrations.js +++ b/src/data/resolvers/queries/integrations.js @@ -1,11 +1,13 @@ import { Integrations } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const integrationQueries = { /** * Integrations list - * @param {Object} args - * @param {Integer} args.limit - * @param {String} args.kind + * @param {Object} object + * @param {Object} object2 - Apollo input data + * @param {Integer} object2.limit + * @param {String} object2.kind * @return {Promise} filterd and sorted integrations list */ integrations(root, { limit, kind }) { @@ -27,8 +29,9 @@ export default { /** * Get one integration - * @param {Object} args - * @param {String} args._id + * @param {Object} object + * @param {Object} object2 - Apollo input data + * @param {String} object2._id - Integration id * @return {Promise} found integration */ integrationDetail(root, { _id }) { @@ -49,3 +52,7 @@ export default { return Integrations.find(query).count(); }, }; + +moduleRequireLogin(integrationQueries); + +export default integrationQueries; From 6d1d0affc18a53e5d9ccb0a87a086a126d0d2311 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 21:18:17 +0800 Subject: [PATCH 174/318] #21 Add insightQueries.test.js --- src/__tests__/insightQueries.test.js | 22 ++++++++++++++++++++++ src/data/resolvers/queries/insights.js | 7 ++++++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/insightQueries.test.js diff --git a/src/__tests__/insightQueries.test.js b/src/__tests__/insightQueries.test.js new file mode 100644 index 000000000..21bbb80c0 --- /dev/null +++ b/src/__tests__/insightQueries.test.js @@ -0,0 +1,22 @@ +/* eslint-env jest */ + +import insightQueries from '../data/resolvers/queries/insights'; + +describe('insightQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(4); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(insightQueries.insights); + expectError(insightQueries.insightsPunchCard); + expectError(insightQueries.insightsMain); + expectError(insightQueries.insightsFirstResponse); + }); +}); diff --git a/src/data/resolvers/queries/insights.js b/src/data/resolvers/queries/insights.js index 0e524e6f8..d5ea9d909 100644 --- a/src/data/resolvers/queries/insights.js +++ b/src/data/resolvers/queries/insights.js @@ -14,8 +14,9 @@ import { getTime, formatTime, } from './insightUtils'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const insightQueries = { /** * Builds insights charting data contains * count of conversations in various integrations kinds. @@ -297,3 +298,7 @@ export default { return insightData; }, }; + +moduleRequireLogin(insightQueries); + +export default insightQueries; From d3f7d1e1ff18f1f5431501179418c6460e782e14 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 21:28:34 +0800 Subject: [PATCH 175/318] #21 Add formQueries.test.js --- src/__tests__/formQueries.test.js | 21 +++++++++++++++++ src/data/resolvers/mutations/forms.js | 33 ++++++++------------------- src/data/resolvers/queries/forms.js | 7 +++++- 3 files changed, 36 insertions(+), 25 deletions(-) create mode 100644 src/__tests__/formQueries.test.js diff --git a/src/__tests__/formQueries.test.js b/src/__tests__/formQueries.test.js new file mode 100644 index 000000000..321ad6208 --- /dev/null +++ b/src/__tests__/formQueries.test.js @@ -0,0 +1,21 @@ +/* eslint-env jest */ + +import formQueries from '../data/resolvers/queries/forms'; + +describe('formQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(3); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(formQueries.forms); + expectError(formQueries.formDetail); + expectError(formQueries.formsTotalCount); + }); +}); diff --git a/src/data/resolvers/mutations/forms.js b/src/data/resolvers/mutations/forms.js index 4f3c41034..22a0722c7 100644 --- a/src/data/resolvers/mutations/forms.js +++ b/src/data/resolvers/mutations/forms.js @@ -1,6 +1,7 @@ import { Forms } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const formMutations = { /** * Create a new form * @param {Object} root @@ -9,13 +10,8 @@ export default { * @param {string} doc.description - Form description * @param {Object} doc.user - The user who created this form * @return {Promise} return Promise resolving Form document - * @throws {Error} throws Error('Login required') if user is not logged in */ formsAdd(root, doc, { user }) { - if (!user) { - throw new Error('Login required'); - } - return Forms.createForm(doc, user); }, @@ -29,13 +25,8 @@ export default { * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user who is making this action * @return {Promise} return Promise resolving Form document - * @throws {Error} throws Error('Login required') if user is not logged in */ - formsEdit(root, { _id, ...doc }, { user }) { - if (!user) { - throw new Error('Login required'); - } - + formsEdit(root, { _id, ...doc }) { return Forms.updateForm(_id, doc); }, @@ -47,13 +38,8 @@ export default { * @param {Object} object3 - The middleware data * @param {Object} object3.user - The user making this action * @return {Promise} - * @throws {Error} throws Error('Login required') if user is not logged in */ - formsRemove(root, { _id }, { user }) { - if (!user) { - throw new Error('Login required'); - } - + formsRemove(root, { _id }) { return Forms.removeForm(_id); }, @@ -65,13 +51,12 @@ export default { * @param {Object} object3 - Middleware data * @param {Object} object3.user - The user making this action * @return {Promise} return Promise resolving the new duplication Form document - * @throws {Error} throws Error('Login required') if user is not logged in */ - formsDuplicate(root, { _id }, { user }) { - if (!user) { - throw new Error('Login required'); - } - + formsDuplicate(root, { _id }) { return Forms.duplicate(_id); }, }; + +moduleRequireLogin(formMutations); + +export default formMutations; diff --git a/src/data/resolvers/queries/forms.js b/src/data/resolvers/queries/forms.js index 8c73cf960..09ceb410c 100644 --- a/src/data/resolvers/queries/forms.js +++ b/src/data/resolvers/queries/forms.js @@ -1,6 +1,7 @@ import { Forms } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const formQueries = { /** * Forms list * @param {Object} args @@ -36,3 +37,7 @@ export default { return Forms.find({}).count(); }, }; + +moduleRequireLogin(formQueries); + +export default formQueries; From 6d7480a6605dba17e1b788df824c31974a7bb170 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 21:32:00 +0800 Subject: [PATCH 176/318] #21 Add fieldQueries.test.js --- src/data/resolvers/mutations/fields.js | 23 ++++++++++------------- src/data/resolvers/queries/fields.js | 7 ++++++- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/data/resolvers/mutations/fields.js b/src/data/resolvers/mutations/fields.js index 3c5726f13..8cf7e0d4b 100644 --- a/src/data/resolvers/mutations/fields.js +++ b/src/data/resolvers/mutations/fields.js @@ -1,13 +1,12 @@ import { Fields } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const fieldMutations = { /** * Adds field object * @return {Promise} */ - fieldsAdd(root, args, { user }) { - if (!user) throw new Error('Login required'); - + fieldsAdd(root, args) { return Fields.createField(args); }, @@ -15,9 +14,7 @@ export default { * Updates field object * @return {Promise} return Promise(null) */ - fieldsEdit(root, { _id, ...doc }, { user }) { - if (!user) throw new Error('Login required'); - + fieldsEdit(root, { _id, ...doc }) { return Fields.updateField(_id, doc); }, @@ -25,9 +22,7 @@ export default { * Remove a channel * @return {Promise} */ - fieldsRemove(root, { _id }, { user }) { - if (!user) throw new Error('Login required'); - + fieldsRemove(root, { _id }) { return Fields.removeField(_id); }, @@ -36,9 +31,11 @@ export default { * @param [OrderItem] [{ _id: [field id], order: [order value] }] * @return {Promise} updated fields */ - fieldsUpdateOrder(root, { orders }, { user }) { - if (!user) throw new Error('Login required'); - + fieldsUpdateOrder(root, { orders }) { return Fields.updateOrder(orders); }, }; + +moduleRequireLogin(fieldMutations); + +export default fieldMutations; diff --git a/src/data/resolvers/queries/fields.js b/src/data/resolvers/queries/fields.js index 0d2eab4f7..9929228cc 100644 --- a/src/data/resolvers/queries/fields.js +++ b/src/data/resolvers/queries/fields.js @@ -1,7 +1,8 @@ import { Companies, Customers, Fields } from '../../../db/models'; import { FIELD_CONTENT_TYPES } from '../../../data/constants'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const fieldQueries = { /** * Fields list * @param {Object} args @@ -115,3 +116,7 @@ export default { return []; }, }; + +moduleRequireLogin(fieldQueries); + +export default fieldQueries; From 667e83698f06a91abf20c227a3bf513974724e57 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 22:24:05 +0800 Subject: [PATCH 177/318] #21 Add fieldsTest.js, Fixed hanging on the fieldDb.test.js --- src/__tests__/fieldDb.test.js | 17 +++++++++-------- src/__tests__/fieldQueries.test.js | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 src/__tests__/fieldQueries.test.js diff --git a/src/__tests__/fieldDb.test.js b/src/__tests__/fieldDb.test.js index 906358fb6..89470a353 100644 --- a/src/__tests__/fieldDb.test.js +++ b/src/__tests__/fieldDb.test.js @@ -2,7 +2,7 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { Fields } from '../db/models'; +import { Forms, Fields } from '../db/models'; import { formFactory, fieldFactory } from '../db/factories'; beforeAll(() => connect()); @@ -20,9 +20,10 @@ describe('Fields', () => { _field = await fieldFactory({ contentType: 'form', order: 1 }); }); - afterEach(() => { - // Clearing test fields - return Fields.remove({}); + afterEach(async () => { + // Clearing test data + await Forms.remove(); + await Fields.remove({}); }); test('createField() without contentTypeId', async () => { @@ -157,19 +158,19 @@ describe('Fields', () => { // required ===== _field.isRequired = true; await _field.save(); - expectError('required', ''); + await expectError('required', ''); // email ===== await changeValidation('email'); - expectError('Invalid email', 'wrongValue'); + await expectError('Invalid email', 'wrongValue'); // number ===== await changeValidation('number'); - expectError('Invalid number', 'wrongValue'); + await expectError('Invalid number', 'wrongValue'); // date ===== await changeValidation('date'); - expectError('Invalid date', 'wrongValue'); + await expectError('Invalid date', 'wrongValue'); }); test('Validate submission: valid values', async () => { diff --git a/src/__tests__/fieldQueries.test.js b/src/__tests__/fieldQueries.test.js new file mode 100644 index 000000000..84e5d3642 --- /dev/null +++ b/src/__tests__/fieldQueries.test.js @@ -0,0 +1,21 @@ +/* eslint-env jest */ + +import fieldQueries from '../data/resolvers/queries/fields'; + +describe('fieldQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(3); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(fieldQueries.fields); + expectError(fieldQueries.fieldsCombinedByContentType); + expectError(fieldQueries.fieldsDefaultColumnsConfig); + }); +}); From 2bc9885034475c4f6c680c9c42e752c9eb2b1434 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 22:31:39 +0800 Subject: [PATCH 178/318] #21 Add engageMessageQueries.test.js --- src/__tests__/engageMessageQueries.test.js | 22 +++++++++++++++ src/data/resolvers/mutations/engages.js | 31 +++++++++------------- src/data/resolvers/queries/engages.js | 7 ++++- 3 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 src/__tests__/engageMessageQueries.test.js diff --git a/src/__tests__/engageMessageQueries.test.js b/src/__tests__/engageMessageQueries.test.js new file mode 100644 index 000000000..95259ae19 --- /dev/null +++ b/src/__tests__/engageMessageQueries.test.js @@ -0,0 +1,22 @@ +/* eslint-env jest */ + +import engageQueries from '../data/resolvers/queries/engages'; + +describe('engageQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(4); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(engageQueries.engageMessageCounts); + expectError(engageQueries.engageMessages); + expectError(engageQueries.engageMessageDetail); + expectError(engageQueries.engageMessagesTotalCount); + }); +}); diff --git a/src/data/resolvers/mutations/engages.js b/src/data/resolvers/mutations/engages.js index b96fa1aa8..826a41a09 100644 --- a/src/data/resolvers/mutations/engages.js +++ b/src/data/resolvers/mutations/engages.js @@ -1,8 +1,9 @@ import { EngageMessages } from '../../../db/models'; import { MESSAGE_KINDS } from '../../constants'; import { send } from './engageUtils'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const engageMutations = { /** * Create new message * @param {String} doc.title @@ -18,9 +19,7 @@ export default { * @param {[String]} doc.tagIds * @return {Promise} message object */ - async engageMessageAdd(root, doc, { user }) { - if (!user) throw new Error('Login required'); - + async engageMessageAdd(root, doc) { const engageMessage = EngageMessages.createEngageMessage(doc); // if manual and live then send immediately @@ -46,9 +45,7 @@ export default { * @param {[String]} doc.tagIds * @return {Promise} message object */ - engageMessageEdit(root, { _id, ...doc }, { user }) { - if (!user) throw new Error('Login required'); - + engageMessageEdit(root, { _id, ...doc }) { return EngageMessages.updateEngageMessage(_id, doc); }, @@ -57,9 +54,7 @@ export default { * @param {String} _id - Engage message id * @return {Promise} */ - engageMessageRemove(root, _id, { user }) { - if (!user) throw new Error('Login required'); - + engageMessageRemove(root, _id) { return EngageMessages.removeEngageMessage(_id); }, @@ -68,9 +63,7 @@ export default { * @param {String} _id - Engage message id * @return {Promise} updated message object */ - engageMessageSetLive(root, _id, { user }) { - if (!user) throw new Error('Login required'); - + engageMessageSetLive(root, _id) { return EngageMessages.engageMessageSetLive(_id); }, @@ -79,9 +72,7 @@ export default { * @param {String} _id - Engage message id * @return {Promise} updated message object */ - engageMessageSetPause(root, _id, { user }) { - if (!user) throw new Error('Login required'); - + engageMessageSetPause(root, _id) { return EngageMessages.engageMessageSetPause(_id); }, @@ -90,9 +81,7 @@ export default { * @param {String} _id - Engage message id * @return {Promise} updated message object */ - async engageMessageSetLiveManual(root, _id, { user }) { - if (!user) throw new Error('Login required'); - + async engageMessageSetLiveManual(root, _id) { const engageMessage = EngageMessages.engageMessageSetLive(_id); await send(engageMessage); @@ -100,3 +89,7 @@ export default { return engageMessage; }, }; + +moduleRequireLogin(engageMutations); + +export default engageMutations; diff --git a/src/data/resolvers/queries/engages.js b/src/data/resolvers/queries/engages.js index 2778ba85d..8834e78b9 100644 --- a/src/data/resolvers/queries/engages.js +++ b/src/data/resolvers/queries/engages.js @@ -1,4 +1,5 @@ import { EngageMessages, Tags } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; // basic count helper const count = selector => EngageMessages.find(selector).count(); @@ -74,7 +75,7 @@ const countsByTag = async ({ kind, status, user }) => { return response; }; -export default { +const engageQueries = { /** * Group engage messages counts by kind, status, tag * @@ -150,3 +151,7 @@ export default { return EngageMessages.find({}).count(); }, }; + +moduleRequireLogin(engageQueries); + +export default engageQueries; From d0dd630f857da5eca34397e2eb5c1743ab8a6f69 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 22:36:14 +0800 Subject: [PATCH 179/318] #21 Add emailTemplateQueries.test.js --- src/__tests__/emailTemplateQueries.test.js | 20 +++++++++++++++++++ .../resolvers/mutations/emailTemplates.js | 19 +++++++++--------- src/data/resolvers/queries/emailTemplates.js | 7 ++++++- 3 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/emailTemplateQueries.test.js diff --git a/src/__tests__/emailTemplateQueries.test.js b/src/__tests__/emailTemplateQueries.test.js new file mode 100644 index 000000000..8d9609faf --- /dev/null +++ b/src/__tests__/emailTemplateQueries.test.js @@ -0,0 +1,20 @@ +/* eslint-env jest */ + +import emailTemplateQueries from '../data/resolvers/queries/emailTemplates'; + +describe('emailTemplateQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(2); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(emailTemplateQueries.emailTemplates); + expectError(emailTemplateQueries.emailTemplatesTotalCount); + }); +}); diff --git a/src/data/resolvers/mutations/emailTemplates.js b/src/data/resolvers/mutations/emailTemplates.js index 84e09c32a..402f4ba49 100644 --- a/src/data/resolvers/mutations/emailTemplates.js +++ b/src/data/resolvers/mutations/emailTemplates.js @@ -1,14 +1,13 @@ import { EmailTemplates } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const emailTemplateMutations = { /** * Create new email template * @param {Object} doc - email templates fields * @return {Promise} newly created email template object */ - emailTemplatesAdd(root, doc, { user }) { - if (!user) throw new Error('Login required'); - + emailTemplatesAdd(root, doc) { return EmailTemplates.create(doc); }, @@ -18,9 +17,7 @@ export default { * @param {Object} fields - email templates fields * @return {Promise} updated email template object */ - emailTemplatesEdit(root, { _id, ...fields }, { user }) { - if (!user) throw new Error('Login required'); - + emailTemplatesEdit(root, { _id, ...fields }) { return EmailTemplates.updateEmailTemplate(_id, fields); }, @@ -29,9 +26,11 @@ export default { * @param {String} doc - email templates fields * @return {Promise} */ - emailTemplatesRemove(root, { _id }, { user }) { - if (!user) throw new Error('Login required'); - + emailTemplatesRemove(root, { _id }) { return EmailTemplates.removeEmailTemplate(_id); }, }; + +moduleRequireLogin(emailTemplateMutations); + +export default emailTemplateMutations; diff --git a/src/data/resolvers/queries/emailTemplates.js b/src/data/resolvers/queries/emailTemplates.js index b606481d0..576bc10a7 100644 --- a/src/data/resolvers/queries/emailTemplates.js +++ b/src/data/resolvers/queries/emailTemplates.js @@ -1,6 +1,7 @@ import { EmailTemplates } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const emailTemplateQueries = { /** * Email templates list * @param {Object} args @@ -25,3 +26,7 @@ export default { return EmailTemplates.find({}).count(); }, }; + +moduleRequireLogin(emailTemplateQueries); + +export default emailTemplateQueries; From 8af920b0f92bb9872eedbbd028f96dd108fe64c7 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 22:51:07 +0800 Subject: [PATCH 180/318] #21 Add customerQueries.test.js --- src/__tests__/customerQueries.test.js | 23 +++++++++++++++++++++++ src/data/resolvers/mutations/customers.js | 20 ++++++++++---------- src/data/resolvers/queries/customers.js | 7 ++++++- 3 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/customerQueries.test.js diff --git a/src/__tests__/customerQueries.test.js b/src/__tests__/customerQueries.test.js new file mode 100644 index 000000000..4d45d2741 --- /dev/null +++ b/src/__tests__/customerQueries.test.js @@ -0,0 +1,23 @@ +/* eslint-env jest */ + +import customerQueries from '../data/resolvers/queries/customers'; + +describe('customerQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(5); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(customerQueries.customers); + expectError(customerQueries.customerCounts); + expectError(customerQueries.customerListForSegmentPreview); + expectError(customerQueries.customerDetail); + expectError(customerQueries.customersTotalCount); + }); +}); diff --git a/src/data/resolvers/mutations/customers.js b/src/data/resolvers/mutations/customers.js index 7ba49f346..395846779 100644 --- a/src/data/resolvers/mutations/customers.js +++ b/src/data/resolvers/mutations/customers.js @@ -1,13 +1,13 @@ import { Customers } from '../../../db/models'; -export default { +import { moduleRequireLogin } from '../../permissions'; + +const customerMutations = { /** * Create new customer * @return {Promise} customer object */ - customersAdd(root, doc, { user }) { - if (!user) throw new Error('Login required'); - + customersAdd(root, doc) { return Customers.createCustomer(doc); }, @@ -15,9 +15,7 @@ export default { * Update customer * @return {Promise} customer object */ - async customersEdit(root, { _id, ...doc }, { user }) { - if (!user) throw new Error('Login required'); - + async customersEdit(root, { _id, ...doc }) { return Customers.updateCustomer(_id, doc); }, @@ -29,9 +27,11 @@ export default { * @param {String} args.website - Company website * @return {Promise} newly created customer */ - async customersAddCompany(root, args, { user }) { - if (!user) throw new Error('Login required'); - + async customersAddCompany(root, args) { return Customers.addCompany(args); }, }; + +moduleRequireLogin(customerMutations); + +export default customerMutations; diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index 93a52801e..b35d05b48 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -2,6 +2,7 @@ import _ from 'underscore'; import { Brands, Tags, Integrations, Customers, Segments } from '../../../db/models'; import { TAG_TYPES, INTEGRATION_KIND_CHOICES, SEGMENT_CONTENT_TYPES } from '../../constants'; import QueryBuilder from './segmentQueryBuilder.js'; +import { moduleRequireLogin } from '../../permissions'; const listQuery = async params => { const selector = {}; @@ -42,7 +43,7 @@ const listQuery = async params => { return selector; }; -export default { +const customerQueries = { /** * Customers list * @param {Object} args @@ -153,3 +154,7 @@ export default { return Customers.find({}).count(); }, }; + +moduleRequireLogin(customerQueries); + +export default customerQueries; From aadb9a3ac01df5569ead8c458df15bd56f8eba42 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 22:56:24 +0800 Subject: [PATCH 181/318] #21 Add conversationQueries.test.js --- src/__tests__/conversationQueries.test.js | 22 ++++++++++++++++ src/data/resolvers/mutations/conversations.js | 25 ++++++------------- src/data/resolvers/queries/conversations.js | 7 +++++- 3 files changed, 35 insertions(+), 19 deletions(-) create mode 100644 src/__tests__/conversationQueries.test.js diff --git a/src/__tests__/conversationQueries.test.js b/src/__tests__/conversationQueries.test.js new file mode 100644 index 000000000..c8fefe136 --- /dev/null +++ b/src/__tests__/conversationQueries.test.js @@ -0,0 +1,22 @@ +/* eslint-env jest */ + +import conversationQueries from '../data/resolvers/queries/conversations'; + +describe('conversationQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(4); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(conversationQueries.conversations); + expectError(conversationQueries.conversationCounts); + expectError(conversationQueries.conversationDetail); + expectError(conversationQueries.conversationsTotalCount); + }); +}); diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 57245b3a2..bfd5c7de7 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -3,6 +3,7 @@ import { pubsub } from '../subscriptions'; import { CONVERSATION_STATUSES, KIND_CHOICES } from '../../constants'; import utils from '../../utils'; import { _ } from 'underscore'; +import { moduleRequireLogin } from '../../permissions'; /** * conversation notrification receiver ids @@ -70,15 +71,13 @@ const conversationMessageCreated = async (message, conversationId) => { }); }; -export default { +const conversationMutations = { /** * Create new message in conversation * @param {Object} doc contains conversation message inputs * @return {Promise} newly created message object */ async conversationMessageAdd(root, doc, { user }) { - if (!user) throw new Error('Login required'); - const message = await ConversationMessages.addMessage(doc, user._id); const conversation = await Conversations.findOne({ _id: doc.conversationId }); @@ -147,8 +146,6 @@ export default { * @return {Promise} object list of assigned conversations */ async conversationsAssign(root, { conversationIds, assignedUserId }, { user }) { - if (!user) throw new Error('Login required'); - const updatedConversations = await Conversations.assignUserConversation( conversationIds, assignedUserId, @@ -179,9 +176,7 @@ export default { * @param {[String]} _ids of conversation * @return {Promise} unassigned conversations */ - async conversationsUnassign(root, { _ids }, { user }) { - if (!user) throw new Error('Login required'); - + async conversationsUnassign(root, { _ids }) { const conversations = await Conversations.unassignUserConversation(_ids); // notify graphl subscription @@ -197,8 +192,6 @@ export default { * @return {Promise} object list of updated conversations */ async conversationsChangeStatus(root, { _ids, status }, { user }) { - if (!user) throw new Error('Login required'); - const { conversations } = await Conversations.checkExistanceConversations(_ids); const changedConversations = await Conversations.changeStatusConversation(_ids, status); @@ -253,8 +246,6 @@ export default { * @return {Promise} user object of starred conversations */ async conversationsStar(root, { _ids }, { user }) { - if (!user) throw new Error('Login required'); - return Conversations.starConversation(_ids, user._id); }, @@ -264,8 +255,6 @@ export default { * @return {Promise} user object from unstarred conversations */ async conversationsUnstar(root, { _ids }, { user }) { - if (!user) throw new Error('Login required'); - return Conversations.unstarConversation(_ids, user._id); }, @@ -275,8 +264,6 @@ export default { * @return {Promise} updated conversations */ async conversationsToggleParticipate(root, { _ids }, { user }) { - if (!user) throw new Error('Login required'); - const conversations = await Conversations.toggleParticipatedUsers(_ids, user._id); // notify graphl subscription @@ -291,8 +278,10 @@ export default { * @return {Promise} Conversation object with mark as read */ async conversationMarkAsRead(root, { _id }, { user }) { - if (!user) throw new Error('Login required'); - return Conversations.markAsReadConversation(_id, user._id); }, }; + +moduleRequireLogin(conversationMutations); + +export default conversationMutations; diff --git a/src/data/resolvers/queries/conversations.js b/src/data/resolvers/queries/conversations.js index d5acd463d..97c61ce08 100644 --- a/src/data/resolvers/queries/conversations.js +++ b/src/data/resolvers/queries/conversations.js @@ -1,8 +1,9 @@ import { Channels, Brands, Conversations, Tags } from '../../../db/models'; import { INTEGRATION_KIND_CHOICES } from '../../constants'; import QueryBuilder from './conversationQueryBuilder'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const conversationQueries = { /** * Conversataions list * @param {Object} args @@ -166,3 +167,7 @@ export default { return Conversations.find(qb.mainQuery()).count(); }, }; + +moduleRequireLogin(conversationQueries); + +export default conversationQueries; From 9cf4ac08b755a9844fbc6156548c5680edab869e Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 23:00:39 +0800 Subject: [PATCH 182/318] #21 Add companyQueries.test.js --- src/__tests__/companyQueries.test.js | 22 ++++++++++++++++++++++ src/data/resolvers/mutations/companies.js | 19 +++++++++---------- src/data/resolvers/queries/companies.js | 7 ++++++- 3 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/companyQueries.test.js diff --git a/src/__tests__/companyQueries.test.js b/src/__tests__/companyQueries.test.js new file mode 100644 index 000000000..50c4ab7ea --- /dev/null +++ b/src/__tests__/companyQueries.test.js @@ -0,0 +1,22 @@ +/* eslint-env jest */ + +import companyQueries from '../data/resolvers/queries/companies'; + +describe('companyQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(4); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(companyQueries.companies); + expectError(companyQueries.companyCounts); + expectError(companyQueries.companyDetail); + expectError(companyQueries.companiesTotalCount); + }); +}); diff --git a/src/data/resolvers/mutations/companies.js b/src/data/resolvers/mutations/companies.js index cbbe048c1..906aca792 100644 --- a/src/data/resolvers/mutations/companies.js +++ b/src/data/resolvers/mutations/companies.js @@ -1,13 +1,12 @@ import { Companies } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const companyMutations = { /** * Create new company * @return {Promise} company object */ - companiesAdd(root, doc, { user }) { - if (!user) throw new Error('Login required'); - + companiesAdd(root, doc) { return Companies.createCompany(doc); }, @@ -15,9 +14,7 @@ export default { * Update company * @return {Promise} company object */ - async companiesEdit(root, { _id, ...doc }, { user }) { - if (!user) throw new Error('Login required'); - + async companiesEdit(root, { _id, ...doc }) { return Companies.updateCompany(_id, doc); }, @@ -29,9 +26,11 @@ export default { * @param {String} args.email - Customer email * @return {Promise} newly created customer */ - async companiesAddCustomer(root, args, { user }) { - if (!user) throw new Error('Login required'); - + async companiesAddCustomer(root, args) { return Companies.addCustomer(args); }, }; + +moduleRequireLogin(companyMutations); + +export default companyMutations; diff --git a/src/data/resolvers/queries/companies.js b/src/data/resolvers/queries/companies.js index 2bf5c9947..a171a9c32 100644 --- a/src/data/resolvers/queries/companies.js +++ b/src/data/resolvers/queries/companies.js @@ -1,6 +1,7 @@ import { Companies, Segments } from '../../../db/models'; import QueryBuilder from './segmentQueryBuilder.js'; import { SEGMENT_CONTENT_TYPES } from '../../constants'; +import { moduleRequireLogin } from '../../permissions'; const listQuery = async params => { const selector = {}; @@ -15,7 +16,7 @@ const listQuery = async params => { return selector; }; -export default { +const companyQueries = { /** * Companies list * @param {Object} args @@ -80,3 +81,7 @@ export default { return Companies.find({}).count(); }, }; + +moduleRequireLogin(companyQueries); + +export default companyQueries; From 2c05cd3b0c0180317a9a9bb08c20ad769b45673c Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 23:05:12 +0800 Subject: [PATCH 183/318] #21 Add channelQueries.test.js --- src/__tests__/channelQueries.test.js | 20 ++++++++++++++++++++ src/data/resolvers/mutations/channels.js | 9 +++++++-- src/data/resolvers/queries/channels.js | 7 ++++++- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/channelQueries.test.js diff --git a/src/__tests__/channelQueries.test.js b/src/__tests__/channelQueries.test.js new file mode 100644 index 000000000..3aacd9ab9 --- /dev/null +++ b/src/__tests__/channelQueries.test.js @@ -0,0 +1,20 @@ +/* eslint-env jest */ + +import channelQueries from '../data/resolvers/queries/channels'; + +describe('channelQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(2); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(channelQueries.channels); + expectError(channelQueries.channelsTotalCount); + }); +}); diff --git a/src/data/resolvers/mutations/channels.js b/src/data/resolvers/mutations/channels.js index 733de3026..559f6617e 100644 --- a/src/data/resolvers/mutations/channels.js +++ b/src/data/resolvers/mutations/channels.js @@ -1,6 +1,7 @@ import { MODULES } from '../../constants'; import { Channels } from '../../../db/models'; import utils from '../../utils'; +import { moduleRequireLogin } from '../../permissions'; /** * Send notification to all members of this channel except the sender @@ -25,7 +26,7 @@ export const sendChannelNotifications = async channel => { }); }; -export default { +const channelMutations = { /** * Create a new channel and send notifications to its members bar the creator * @param {Object} root @@ -76,9 +77,13 @@ export default { * @param {Object|String} object3.user - User making this action * @return {Promise} */ - async channelsRemove(root, { _id }, { user }) { + async channelsRemove(root, { _id }) { await Channels.removeChannel(_id); return _id; }, }; + +moduleRequireLogin(channelMutations); + +export default channelMutations; diff --git a/src/data/resolvers/queries/channels.js b/src/data/resolvers/queries/channels.js index fdaf5e908..2b45f857c 100644 --- a/src/data/resolvers/queries/channels.js +++ b/src/data/resolvers/queries/channels.js @@ -1,6 +1,7 @@ import { Channels } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const channelQueries = { /** * Channels list * @param {Object} args @@ -33,3 +34,7 @@ export default { return Channels.find({}).count(); }, }; + +moduleRequireLogin(channelQueries); + +export default channelQueries; From 50cdfbc3257f3b2da76b891926c318caee33952f Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 3 Nov 2017 23:09:59 +0800 Subject: [PATCH 184/318] #23 Add brandQueries.test.js --- src/__tests__/brandQueries.test.js | 21 +++++++++++++++++++++ src/data/resolvers/mutations/brands.js | 21 +++++++++------------ src/data/resolvers/queries/brands.js | 7 ++++++- 3 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 src/__tests__/brandQueries.test.js diff --git a/src/__tests__/brandQueries.test.js b/src/__tests__/brandQueries.test.js new file mode 100644 index 000000000..2b0700fcb --- /dev/null +++ b/src/__tests__/brandQueries.test.js @@ -0,0 +1,21 @@ +/* eslint-env jest */ + +import brandQueries from '../data/resolvers/queries/brands'; + +describe('brandQueries', () => { + test(`test if Error('Login required') exception is working as intended`, async () => { + expect.assertions(3); + + const expectError = async func => { + try { + await func(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; + + expectError(brandQueries.brands); + expectError(brandQueries.brandDetail); + expectError(brandQueries.brandsTotalCount); + }); +}); diff --git a/src/data/resolvers/mutations/brands.js b/src/data/resolvers/mutations/brands.js index 4a14ec763..b149e2eb8 100644 --- a/src/data/resolvers/mutations/brands.js +++ b/src/data/resolvers/mutations/brands.js @@ -1,14 +1,13 @@ import { Brands } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const brandMutations = { /** * Create new brand * @param {Object} doc - brand fields * @return {Promise} brand object */ brandsAdd(root, doc, { user }) { - if (!user) throw new Error('Login required'); - return Brands.createBrand({ userId: user._id, ...doc }); }, @@ -18,9 +17,7 @@ export default { * @param {Object} fields - brand fields * @return {Promise} brand object */ - brandsEdit(root, { _id, ...fields }, { user }) { - if (!user) throw new Error('Login required'); - + brandsEdit(root, { _id, ...fields }) { return Brands.updateBrand(_id, fields); }, @@ -29,9 +26,7 @@ export default { * @param {String} _id - brand id * @return {Promise} */ - brandsRemove(root, { _id }, { user }) { - if (!user) throw new Error('Login required'); - + brandsRemove(root, { _id }) { return Brands.removeBrand(_id); }, @@ -41,9 +36,11 @@ export default { * @param {Object} emailConfig - brand email config fields * @return {Promise} updated brand object */ - async brandsConfigEmail(root, { _id, emailConfig }, { user }) { - if (!user) throw new Error('Login required'); - + async brandsConfigEmail(root, { _id, emailConfig }) { return Brands.updateEmailConfig(_id, emailConfig); }, }; + +moduleRequireLogin(brandMutations); + +export default brandMutations; diff --git a/src/data/resolvers/queries/brands.js b/src/data/resolvers/queries/brands.js index 392645f0c..249dbeffc 100644 --- a/src/data/resolvers/queries/brands.js +++ b/src/data/resolvers/queries/brands.js @@ -1,6 +1,7 @@ import { Brands } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; -export default { +const brandQueries = { /** * Brands list * @param {Object} args @@ -36,3 +37,7 @@ export default { return Brands.find({}).count(); }, }; + +moduleRequireLogin(brandQueries); + +export default brandQueries; From e52c8af8a7724e40304331cd5fd47162cb2fe8e9 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 4 Nov 2017 00:27:20 +0800 Subject: [PATCH 185/318] #21 Add admin permission check for intergrationsRemove mutation --- src/__tests__/integrationMutations.test.js | 27 +++++++++++++++++--- src/data/permissions.js | 19 +++++++++++++- src/data/resolvers/mutations/integrations.js | 10 ++++++-- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js index fbe3795b8..2df6eee3f 100644 --- a/src/__tests__/integrationMutations.test.js +++ b/src/__tests__/integrationMutations.test.js @@ -6,6 +6,7 @@ import { connect, disconnect } from '../db/connection'; import { FORM_LOAD_TYPES, MESSENGER_DATA_AVAILABILITY } from '../data/constants'; import { userFactory } from '../db/factories'; import { Integrations, Users } from '../db/models'; +import { ROLES } from '../data/constants'; import integrationMutations from '../data/resolvers/mutations/integrations'; beforeAll(() => connect()); @@ -15,17 +16,20 @@ describe('mutations', () => { const _fakeBrandId = 'fakeBrandId'; const _fakeFormId = 'fakeFormId'; const _fakeIntegrationId = '_fakeIntegrationId'; + let _user; + let _adminUser; - beforeEach(async () => { + beforeAll(async () => { _user = await userFactory({}); + _adminUser = await userFactory({ role: ROLES.ADMIN }); }); - afterEach(async () => { + afterAll(async () => { await Users.remove({}); }); - test('test if `logging required` error is working as intended', () => { + test(`test if Error('Login required') exception is working as intended`, () => { expect.assertions(7); // Login required ================== @@ -58,6 +62,21 @@ describe('mutations', () => { ); }); + test(`test if Error('Permission required') exception is working as intended`, async () => { + expect.assertions(1); + + const expectError = async func => { + try { + await func(null, {}, { user: _user }); + } catch (e) { + expect(e.message).toBe('Permission required'); + } + }; + + // Login required ================== + expectError(integrationMutations.integrationsRemove); + }); + test('test Integrations.createMessengerIntegration', async () => { const doc = { name: 'Integration test', @@ -206,7 +225,7 @@ describe('mutations', () => { await integrationMutations.integrationsRemove( null, { _id: _fakeIntegrationId }, - { user: _user }, + { user: _adminUser }, ); expect(Integrations.removeIntegration).toBeCalledWith(_fakeIntegrationId); diff --git a/src/data/permissions.js b/src/data/permissions.js index 1a2980cf0..4fe32b4c5 100644 --- a/src/data/permissions.js +++ b/src/data/permissions.js @@ -1,4 +1,16 @@ -export const requireLogin = (cls, methodName) => { +import { ROLES } from './constants'; + +export const PERMISSIONS = { + ADMIN: 'admin', +}; + +export const checkPermission = (permission, user) => { + if (permission === PERMISSIONS.ADMIN && user.role === ROLES.CONTRIBUTOR) { + throw new Error('Permission required'); + } +}; + +export const requireLogin = (cls, methodName, permissions) => { const oldMethod = cls[methodName]; cls[methodName] = (root, object2, { user }) => { @@ -6,6 +18,10 @@ export const requireLogin = (cls, methodName) => { throw new Error('Login required'); } + for (let permission of permissions || []) { + checkPermission(permission, user); + } + return oldMethod(root, object2, { user }); }; }; @@ -19,4 +35,5 @@ export const moduleRequireLogin = mdl => { export default { requireLogin, moduleRequireLogin, + PERMISSIONS, }; diff --git a/src/data/resolvers/mutations/integrations.js b/src/data/resolvers/mutations/integrations.js index 8f9bdf750..07460df0f 100644 --- a/src/data/resolvers/mutations/integrations.js +++ b/src/data/resolvers/mutations/integrations.js @@ -1,5 +1,5 @@ import { Integrations } from '../../../db/models'; -import { moduleRequireLogin } from '../../permissions'; +import { requireLogin, PERMISSIONS } from '../../permissions'; const integrationMutations = { /** @@ -110,6 +110,12 @@ const integrationMutations = { }, }; -moduleRequireLogin(integrationMutations); +requireLogin(integrationMutations, 'integrationsCreateMessengerIntegration'); +requireLogin(integrationMutations, 'integrationsEditMessengerIntegration'); +requireLogin(integrationMutations, 'integrationsSaveMessengerAppearanceData'); +requireLogin(integrationMutations, 'integrationsSaveMessengerConfigs'); +requireLogin(integrationMutations, 'integrationsCreateFormIntegration'); +requireLogin(integrationMutations, 'integrationsEditFormIntegration'); +requireLogin(integrationMutations, 'integrationsRemove', [PERMISSIONS.ADMIN]); export default integrationMutations; From 73ca93d8c236ba4030b52b0f7089307a8e57c248 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 4 Nov 2017 00:44:13 +0800 Subject: [PATCH 186/318] #21 Add permission checks on form mutations --- src/__tests__/formMutations.test.js | 45 +++++++++++++--------- src/__tests__/integrationMutations.test.js | 20 ++-------- src/data/resolvers/mutations/forms.js | 7 +++- 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/__tests__/formMutations.test.js b/src/__tests__/formMutations.test.js index f3fb42563..858d62c3c 100644 --- a/src/__tests__/formMutations.test.js +++ b/src/__tests__/formMutations.test.js @@ -1,27 +1,17 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ -import { connect, disconnect } from '../db/connection'; import formMutations from '../data/resolvers/mutations/forms'; -import { userFactory } from '../db/factories'; -import { Forms, Users } from '../db/models'; - -beforeAll(() => connect()); -afterAll(() => disconnect()); +import { Forms } from '../db/models'; +import { ROLES } from '../data/constants'; describe('form and formField mutations', () => { const _formId = 'formId'; - let _user; - beforeEach(async () => { - _user = await userFactory({}); - }); + const _user = { _id: 'fakeId', role: ROLES.CONTRIBUTOR }; + const _adminUser = { _id: 'fakeId', role: ROLES.ADMIN }; - afterEach(async () => { - await Users.remove({}); - }); - - test('test if `logging required` error is working as intended', () => { + test(`test if Error('Login required') exception is working as intended`, () => { expect.assertions(4); // Login required ================== @@ -31,6 +21,23 @@ describe('form and formField mutations', () => { expect(() => formMutations.formsDuplicate(null, {}, {})).toThrowError('Login required'); }); + test(`test if Error('Permission required') exception is working as intended`, () => { + expect.assertions(3); + + const expectError = async func => { + try { + await func(null, {}, { user: _user }); + } catch (e) { + expect(e.message).toBe('Permission required'); + } + }; + + // Login required ================== + expectError(formMutations.formsAdd); + expectError(formMutations.formsEdit); + expectError(formMutations.formsRemove); + }); + test(`test mutations.formsAdd`, async () => { Forms.createForm = jest.fn(); @@ -39,9 +46,9 @@ describe('form and formField mutations', () => { description: 'Test form description', }; - await formMutations.formsAdd(null, doc, { user: _user }); + await formMutations.formsAdd(null, doc, { user: _adminUser }); - expect(Forms.createForm).toBeCalledWith(doc, _user); + expect(Forms.createForm).toBeCalledWith(doc, _adminUser); expect(Forms.createForm.mock.calls.length).toBe(1); }); @@ -54,7 +61,7 @@ describe('form and formField mutations', () => { Forms.updateForm = jest.fn(); - await formMutations.formsEdit(null, doc, { user: _user }); + await formMutations.formsEdit(null, doc, { user: _adminUser }); const formId = _formId; delete doc._id; @@ -66,7 +73,7 @@ describe('form and formField mutations', () => { test('test mutations.formsRemove', async () => { Forms.removeForm = jest.fn(); - await formMutations.formsRemove(null, { _id: _formId }, { user: _user }); + await formMutations.formsRemove(null, { _id: _formId }, { user: _adminUser }); expect(Forms.removeForm).toBeCalledWith(_formId); }); diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js index 2df6eee3f..c3a905600 100644 --- a/src/__tests__/integrationMutations.test.js +++ b/src/__tests__/integrationMutations.test.js @@ -2,32 +2,18 @@ /* eslint-disable no-underscore-dangle */ import faker from 'faker'; -import { connect, disconnect } from '../db/connection'; import { FORM_LOAD_TYPES, MESSENGER_DATA_AVAILABILITY } from '../data/constants'; -import { userFactory } from '../db/factories'; -import { Integrations, Users } from '../db/models'; +import { Integrations } from '../db/models'; import { ROLES } from '../data/constants'; import integrationMutations from '../data/resolvers/mutations/integrations'; -beforeAll(() => connect()); -afterAll(() => disconnect()); - describe('mutations', () => { const _fakeBrandId = 'fakeBrandId'; const _fakeFormId = 'fakeFormId'; const _fakeIntegrationId = '_fakeIntegrationId'; - let _user; - let _adminUser; - - beforeAll(async () => { - _user = await userFactory({}); - _adminUser = await userFactory({ role: ROLES.ADMIN }); - }); - - afterAll(async () => { - await Users.remove({}); - }); + const _user = { _id: 'fakeId', role: ROLES.CONTRIBUTOR }; + const _adminUser = { _id: 'fakeId', role: ROLES.ADMIN }; test(`test if Error('Login required') exception is working as intended`, () => { expect.assertions(7); diff --git a/src/data/resolvers/mutations/forms.js b/src/data/resolvers/mutations/forms.js index 22a0722c7..f9ca13d11 100644 --- a/src/data/resolvers/mutations/forms.js +++ b/src/data/resolvers/mutations/forms.js @@ -1,5 +1,5 @@ import { Forms } from '../../../db/models'; -import { moduleRequireLogin } from '../../permissions'; +import { requireLogin, PERMISSIONS } from '../../permissions'; const formMutations = { /** @@ -57,6 +57,9 @@ const formMutations = { }, }; -moduleRequireLogin(formMutations); +requireLogin(formMutations, 'formsAdd', [PERMISSIONS.ADMIN]); +requireLogin(formMutations, 'formsEdit', [PERMISSIONS.ADMIN]); +requireLogin(formMutations, 'formsRemove', [PERMISSIONS.ADMIN]); +requireLogin(formMutations, 'formsDuplicate'); export default formMutations; From 70ff1597203bf994f42ae6299c1e85bbdd8549df Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 4 Nov 2017 00:50:09 +0800 Subject: [PATCH 187/318] #21 Add permission checks on channel mutations --- src/__tests__/channelMutations.test.js | 43 +++++++++++++----------- src/data/permissions.js | 4 +-- src/data/resolvers/mutations/channels.js | 4 +-- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/__tests__/channelMutations.test.js b/src/__tests__/channelMutations.test.js index fd5b5bd4e..21bf72bf8 100644 --- a/src/__tests__/channelMutations.test.js +++ b/src/__tests__/channelMutations.test.js @@ -1,27 +1,16 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ -import { connect, disconnect } from '../db/connection'; -import { userFactory } from '../db/factories'; -import { Channels, Users } from '../db/models'; +import { Channels } from '../db/models'; +import { ROLES } from '../data/constants'; import { MODULES } from '../data/constants'; import channelMutations from '../data/resolvers/mutations/channels'; import utils from '../data/utils'; -beforeAll(() => connect()); -afterAll(() => disconnect()); - describe('mutations', () => { const _channelId = 'fakeChannelId'; - let _user; - - beforeEach(async () => { - _user = await userFactory({}); - }); - - afterEach(async () => { - await Users.remove({}); - }); + const _user = { _id: 'fakeId', role: ROLES.CONTRIBUTOR }; + const _adminUser = { _id: 'fakeId', role: ROLES.ADMIN }; test(`test if Error('Login required') error is working as intended`, async () => { expect.assertions(3); @@ -39,6 +28,22 @@ describe('mutations', () => { expectError(channelMutations.channelsRemove); }); + test(`test if Error('Permission required') error is working as intended`, async () => { + expect.assertions(3); + + const expectError = async func => { + try { + await func(null, {}, { user: _user }); + } catch (e) { + expect(e.message).toBe('Permission required'); + } + }; + + expectError(channelMutations.channelsAdd); + expectError(channelMutations.channelsEdit); + expectError(channelMutations.channelsRemove); + }); + test('test mutations.channelsAdd', async () => { let doc = { name: 'Channel test', @@ -71,9 +76,9 @@ describe('mutations', () => { jest.spyOn(utils, 'sendNotification').mockImplementation(() => ({})); - await channelMutations.channelsAdd(null, doc, { user: _user }); + await channelMutations.channelsAdd(null, doc, { user: _adminUser }); - expect(Channels.createChannel).toBeCalledWith(doc, _user); + expect(Channels.createChannel).toBeCalledWith(doc, _adminUser); expect(Channels.createChannel.mock.calls.length).toBe(1); expect(utils.sendNotification).toBeCalledWith(sendNotificationDoc); @@ -116,7 +121,7 @@ describe('mutations', () => { ...doc, _id: _channelId, }, - { user: _user }, + { user: _adminUser }, ); expect(Channels.updateChannel).toBeCalledWith(_channelId, doc); @@ -129,7 +134,7 @@ describe('mutations', () => { test('test mutations.channelsRemove', async () => { Channels.removeChannel = jest.fn(); - await channelMutations.channelsRemove(null, { _id: _channelId }, { user: _user }); + await channelMutations.channelsRemove(null, { _id: _channelId }, { user: _adminUser }); expect(Channels.removeChannel).toBeCalledWith(_channelId); expect(Channels.removeChannel.mock.calls.length).toEqual(1); diff --git a/src/data/permissions.js b/src/data/permissions.js index 4fe32b4c5..2ad026744 100644 --- a/src/data/permissions.js +++ b/src/data/permissions.js @@ -26,9 +26,9 @@ export const requireLogin = (cls, methodName, permissions) => { }; }; -export const moduleRequireLogin = mdl => { +export const moduleRequireLogin = (mdl, permissions) => { for (let method in mdl) { - requireLogin(mdl, method); + requireLogin(mdl, method, permissions); } }; diff --git a/src/data/resolvers/mutations/channels.js b/src/data/resolvers/mutations/channels.js index 559f6617e..65a57c2e3 100644 --- a/src/data/resolvers/mutations/channels.js +++ b/src/data/resolvers/mutations/channels.js @@ -1,7 +1,7 @@ import { MODULES } from '../../constants'; import { Channels } from '../../../db/models'; import utils from '../../utils'; -import { moduleRequireLogin } from '../../permissions'; +import { moduleRequireLogin, PERMISSIONS } from '../../permissions'; /** * Send notification to all members of this channel except the sender @@ -84,6 +84,6 @@ const channelMutations = { }, }; -moduleRequireLogin(channelMutations); +moduleRequireLogin(channelMutations, [PERMISSIONS.ADMIN]); export default channelMutations; From a036d35e48feab65670230bd2a516d2bad3c88e7 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 4 Nov 2017 00:57:31 +0800 Subject: [PATCH 188/318] #21 Add permission checks on brand mutations --- src/__tests__/brandMutations.test.js | 70 ++++++++++++++---------- src/data/resolvers/mutations/brands.js | 4 +- src/data/resolvers/mutations/channels.js | 4 +- 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/__tests__/brandMutations.test.js b/src/__tests__/brandMutations.test.js index dd3438676..3d8b77945 100644 --- a/src/__tests__/brandMutations.test.js +++ b/src/__tests__/brandMutations.test.js @@ -1,37 +1,49 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ -import { connect, disconnect } from '../db/connection'; -import { Brands, Users } from '../db/models'; -import { brandFactory, userFactory } from '../db/factories'; +import { Brands } from '../db/models'; +import { ROLES } from '../data/constants'; import brandMutations from '../data/resolvers/mutations/brands'; -beforeAll(() => connect()); +describe('Brands mutations', () => { + const _brand = { + _id: 'fakeBrandId', + code: 'fakeBrandCode', + name: 'fakeBrandName', + }; + const _user = { _id: 'fakeId', role: ROLES.CONTRIBUTOR }; + const _adminUser = { _id: 'fakeId', role: ROLES.ADMIN }; -afterAll(() => disconnect()); + test('Check login required mutations', async () => { + const checkLogin = async (fn, args) => { + try { + await fn({}, args, {}); + } catch (e) { + expect(e.message).toEqual('Login required'); + } + }; -describe('Brands mutations', () => { - let _brand; - let _user; + expect.assertions(4); - beforeEach(async () => { - // Creating test data - _brand = await brandFactory(); - _user = await userFactory(); - }); + // brands add + checkLogin(brandMutations.brandsAdd, { code: _brand.code, name: _brand.name }); - afterEach(async () => { - // Clearing test data - await Brands.remove({}); - await Users.remove({}); + // brands edit + checkLogin(brandMutations.brandsEdit, { _id: _brand._id }); + + // brands remove + checkLogin(brandMutations.brandsRemove, { _id: _brand._id }); + + // brands update email config + checkLogin(brandMutations.brandsConfigEmail, { _id: _brand._id }); }); - test('Check login required mutations', async () => { + test(`test if Error('Permission required') error is working as intended`, async () => { const checkLogin = async (fn, args) => { try { - await fn({}, args, {}); + await fn({}, args, { user: _user }); } catch (e) { - expect(e.message).toEqual('Login required'); + expect(e.message).toEqual('Permission required'); } }; @@ -41,13 +53,13 @@ describe('Brands mutations', () => { checkLogin(brandMutations.brandsAdd, { code: _brand.code, name: _brand.name }); // brands edit - checkLogin(brandMutations.brandsEdit, { _id: _brand.id }); + checkLogin(brandMutations.brandsEdit, { _id: _brand._id }); // brands remove - checkLogin(brandMutations.brandsRemove, { _id: _brand.id }); + checkLogin(brandMutations.brandsRemove, { _id: _brand._id }); // brands update email config - checkLogin(brandMutations.brandsConfigEmail, { _id: _brand.id }); + checkLogin(brandMutations.brandsConfigEmail, { _id: _brand._id }); }); test('Create brand', async () => { @@ -59,10 +71,10 @@ describe('Brands mutations', () => { Brands.createBrand = jest.fn(); - await brandMutations.brandsAdd({}, _doc, { user: _user }); + await brandMutations.brandsAdd({}, _doc, { user: _adminUser }); expect(Brands.createBrand.mock.calls.length).toBe(1); - expect(Brands.createBrand).toBeCalledWith({ userId: _user._id, ..._doc }); + expect(Brands.createBrand).toBeCalledWith({ userId: _adminUser._id, ..._doc }); }); test('Update brand', async () => { @@ -75,7 +87,7 @@ describe('Brands mutations', () => { }; // update brand object - await brandMutations.brandsEdit({}, { _id: _brand._id, ..._doc }, { user: _user }); + await brandMutations.brandsEdit({}, { _id: _brand._id, ..._doc }, { user: _adminUser }); expect(Brands.updateBrand.mock.calls.length).toBe(1); expect(Brands.updateBrand).toBeCalledWith(_brand._id, _doc); @@ -84,7 +96,7 @@ describe('Brands mutations', () => { test('Delete brand', async () => { Brands.removeBrand = jest.fn(); - await brandMutations.brandsRemove({}, { _id: _brand.id }, { user: _user }); + await brandMutations.brandsRemove({}, { _id: _brand._id }, { user: _adminUser }); expect(Brands.removeBrand.mock.calls.length).toBe(1); expect(Brands.removeBrand).toBeCalledWith(_brand._id); }); @@ -94,8 +106,8 @@ describe('Brands mutations', () => { await brandMutations.brandsConfigEmail( {}, - { _id: _brand.id, emailConfig: _brand.emailConfig }, - { user: _user._id }, + { _id: _brand._id, emailConfig: _brand.emailConfig }, + { user: _adminUser._id }, ); expect(Brands.updateEmailConfig.mock.calls.length).toBe(1); diff --git a/src/data/resolvers/mutations/brands.js b/src/data/resolvers/mutations/brands.js index b149e2eb8..bdc4e6720 100644 --- a/src/data/resolvers/mutations/brands.js +++ b/src/data/resolvers/mutations/brands.js @@ -1,5 +1,5 @@ import { Brands } from '../../../db/models'; -import { moduleRequireLogin } from '../../permissions'; +import { moduleRequireLogin, PERMISSIONS } from '../../permissions'; const brandMutations = { /** @@ -41,6 +41,6 @@ const brandMutations = { }, }; -moduleRequireLogin(brandMutations); +moduleRequireLogin(brandMutations, [PERMISSIONS.ADMIN]); export default brandMutations; diff --git a/src/data/resolvers/mutations/channels.js b/src/data/resolvers/mutations/channels.js index 65a57c2e3..340305949 100644 --- a/src/data/resolvers/mutations/channels.js +++ b/src/data/resolvers/mutations/channels.js @@ -35,8 +35,8 @@ const channelMutations = { * @param {string} doc.description - Channel description * @param {String[]} doc.memberIds - Members assigned to the channel being created * @param {String[]} doc.integrationIds - Integrations related to the channel - * @param {Object} object3 - Graphql input data - * @param {Object|string} user - User making this action + * @param {Object} object3 - Middleware data + * @param {Object} object.user - User making this action * @return {Promise} return Promise resolving created Channel document */ async channelsAdd(root, doc, { user }) { From cb06f55b597933e4cf7a16ade70cc9dfd2a597e6 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 4 Nov 2017 01:08:31 +0800 Subject: [PATCH 189/318] #21 Add permission checks on user mutations --- src/__tests__/userMutations.test.js | 27 ++++++++++++++++++++++----- src/data/resolvers/mutations/users.js | 4 ++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js index 2d5b12a8a..9ca070842 100644 --- a/src/__tests__/userMutations.test.js +++ b/src/__tests__/userMutations.test.js @@ -2,6 +2,7 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; +import { ROLES } from '../data/constants'; import { Users, Channels } from '../db/models'; import { userFactory, channelFactory } from '../db/factories'; import userMutations from '../data/resolvers/mutations/users'; @@ -12,7 +13,8 @@ beforeAll(() => connect()); afterAll(() => disconnect()); describe('User mutations', () => { - const user = { _id: 'DFAFDFDFD' }; + const user = { _id: 'DFAFDFDFD', role: ROLES.CONTRIBUTOR }; + const _adminUser = { _id: 'fakeId', role: ROLES.ADMIN }; afterEach(async () => { // Clearing test data @@ -92,6 +94,21 @@ describe('User mutations', () => { checkLogin(userMutations.usersRemove, {}); }); + test(`test if Error('Permission required') error is working as intended`, async () => { + const checkLogin = async fn => { + try { + await fn({}, {}, { user }); + } catch (e) { + expect(e.message).toEqual('Permission required'); + } + }; + + expect.assertions(1); + + // users remove + checkLogin(userMutations.usersRemove); + }); + test('Users add & edit: wrong password confirmation', async () => { expect.assertions(2); @@ -223,7 +240,7 @@ describe('User mutations', () => { const owner = await userFactory({ isOwner: true }); try { - await userMutations.usersRemove({}, { _id: owner._id }, { user }); + await userMutations.usersRemove({}, { _id: owner._id }, { user: _adminUser }); } catch (e) { expect(e.message).toBe('Can not remove owner'); } @@ -236,7 +253,7 @@ describe('User mutations', () => { await channelFactory({ userId: userToRemove._id }); try { - await userMutations.usersRemove({}, { _id: userToRemove._id }, { user }); + await userMutations.usersRemove({}, { _id: userToRemove._id }, { user: _adminUser }); } catch (e) { expect(e.message).toBe('You cannot delete this user. This user belongs other channel.'); } @@ -249,7 +266,7 @@ describe('User mutations', () => { await channelFactory({ memberIds: ['DFAFSFDSFDS', userToRemove._id] }); try { - await userMutations.usersRemove({}, { _id: userToRemove._id }, { user }); + await userMutations.usersRemove({}, { _id: userToRemove._id }, { user: _adminUser }); } catch (e) { expect(e.message).toBe('You cannot delete this user. This user belongs other channel.'); } @@ -259,7 +276,7 @@ describe('User mutations', () => { const removeUser = await userFactory({}); const removeUserId = removeUser._id; - await userMutations.usersRemove({}, { _id: removeUserId }, { user }); + await userMutations.usersRemove({}, { _id: removeUserId }, { user: _adminUser }); // ensure removed expect(await Users.findOne({ _id: removeUserId })).toBe(null); diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index faf2eac52..8527d7d10 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -1,7 +1,7 @@ import bcrypt from 'bcrypt'; import { Users, Channels } from '../../../db/models'; import utils from '../../../data/utils'; -import { requireLogin } from '../../permissions'; +import { requireLogin, PERMISSIONS } from '../../permissions'; const userMutations = { /* @@ -176,6 +176,6 @@ requireLogin(userMutations, 'usersAdd'); requireLogin(userMutations, 'usersEdit'); requireLogin(userMutations, 'usersChangePassword'); requireLogin(userMutations, 'usersEditProfile'); -requireLogin(userMutations, 'usersRemove'); +requireLogin(userMutations, 'usersRemove', [PERMISSIONS.ADMIN]); export default userMutations; From 203b47e55f56a01a93b6f2e45b57a9a4555e2a61 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 4 Nov 2017 15:10:39 +0800 Subject: [PATCH 190/318] #21 Refactor permissions --- src/__tests__/brandMutations.test.js | 2 +- src/data/permissions.js | 38 ++++++++++++-------- src/data/resolvers/mutations/brands.js | 4 +-- src/data/resolvers/mutations/channels.js | 4 +-- src/data/resolvers/mutations/forms.js | 8 ++--- src/data/resolvers/mutations/integrations.js | 4 +-- src/data/resolvers/mutations/users.js | 4 +-- 7 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/__tests__/brandMutations.test.js b/src/__tests__/brandMutations.test.js index 3d8b77945..1be3dc84b 100644 --- a/src/__tests__/brandMutations.test.js +++ b/src/__tests__/brandMutations.test.js @@ -107,7 +107,7 @@ describe('Brands mutations', () => { await brandMutations.brandsConfigEmail( {}, { _id: _brand._id, emailConfig: _brand.emailConfig }, - { user: _adminUser._id }, + { user: _adminUser }, ); expect(Brands.updateEmailConfig.mock.calls.length).toBe(1); diff --git a/src/data/permissions.js b/src/data/permissions.js index 2ad026744..3e30625c2 100644 --- a/src/data/permissions.js +++ b/src/data/permissions.js @@ -1,39 +1,49 @@ import { ROLES } from './constants'; -export const PERMISSIONS = { - ADMIN: 'admin', +export const checkLogin = user => { + if (!user) { + throw new Error('Login required'); + } }; -export const checkPermission = (permission, user) => { - if (permission === PERMISSIONS.ADMIN && user.role === ROLES.CONTRIBUTOR) { +export const checkAdmin = user => { + if (user.role != ROLES.ADMIN) { throw new Error('Permission required'); } }; -export const requireLogin = (cls, methodName, permissions) => { +export const permissionWrapper = (cls, methodName, checkers) => { const oldMethod = cls[methodName]; cls[methodName] = (root, object2, { user }) => { - if (!user) { - throw new Error('Login required'); - } - - for (let permission of permissions || []) { - checkPermission(permission, user); + for (let checker of checkers) { + checker(user); } return oldMethod(root, object2, { user }); }; }; -export const moduleRequireLogin = (mdl, permissions) => { +export const requireLogin = (cls, methodName) => permissionWrapper(cls, methodName, [checkLogin]); + +export const requireAdmin = (cls, methodName) => + permissionWrapper(cls, methodName, [checkLogin, checkAdmin]); + +export const moduleRequireLogin = mdl => { + for (let method in mdl) { + requireLogin(mdl, method); + } +}; + +export const moduleRequireAdmin = mdl => { for (let method in mdl) { - requireLogin(mdl, method, permissions); + requireAdmin(mdl, method); } }; export default { requireLogin, + requireAdmin, moduleRequireLogin, - PERMISSIONS, + moduleRequireAdmin, }; diff --git a/src/data/resolvers/mutations/brands.js b/src/data/resolvers/mutations/brands.js index bdc4e6720..1da586817 100644 --- a/src/data/resolvers/mutations/brands.js +++ b/src/data/resolvers/mutations/brands.js @@ -1,5 +1,5 @@ import { Brands } from '../../../db/models'; -import { moduleRequireLogin, PERMISSIONS } from '../../permissions'; +import { moduleRequireAdmin } from '../../permissions'; const brandMutations = { /** @@ -41,6 +41,6 @@ const brandMutations = { }, }; -moduleRequireLogin(brandMutations, [PERMISSIONS.ADMIN]); +moduleRequireAdmin(brandMutations); export default brandMutations; diff --git a/src/data/resolvers/mutations/channels.js b/src/data/resolvers/mutations/channels.js index 340305949..76deb4706 100644 --- a/src/data/resolvers/mutations/channels.js +++ b/src/data/resolvers/mutations/channels.js @@ -1,7 +1,7 @@ import { MODULES } from '../../constants'; import { Channels } from '../../../db/models'; import utils from '../../utils'; -import { moduleRequireLogin, PERMISSIONS } from '../../permissions'; +import { moduleRequireAdmin } from '../../permissions'; /** * Send notification to all members of this channel except the sender @@ -84,6 +84,6 @@ const channelMutations = { }, }; -moduleRequireLogin(channelMutations, [PERMISSIONS.ADMIN]); +moduleRequireAdmin(channelMutations); export default channelMutations; diff --git a/src/data/resolvers/mutations/forms.js b/src/data/resolvers/mutations/forms.js index f9ca13d11..2b0a08b45 100644 --- a/src/data/resolvers/mutations/forms.js +++ b/src/data/resolvers/mutations/forms.js @@ -1,5 +1,5 @@ import { Forms } from '../../../db/models'; -import { requireLogin, PERMISSIONS } from '../../permissions'; +import { requireAdmin, requireLogin } from '../../permissions'; const formMutations = { /** @@ -57,9 +57,9 @@ const formMutations = { }, }; -requireLogin(formMutations, 'formsAdd', [PERMISSIONS.ADMIN]); -requireLogin(formMutations, 'formsEdit', [PERMISSIONS.ADMIN]); -requireLogin(formMutations, 'formsRemove', [PERMISSIONS.ADMIN]); +requireAdmin(formMutations, 'formsAdd'); +requireAdmin(formMutations, 'formsEdit'); +requireAdmin(formMutations, 'formsRemove'); requireLogin(formMutations, 'formsDuplicate'); export default formMutations; diff --git a/src/data/resolvers/mutations/integrations.js b/src/data/resolvers/mutations/integrations.js index 07460df0f..379201058 100644 --- a/src/data/resolvers/mutations/integrations.js +++ b/src/data/resolvers/mutations/integrations.js @@ -1,5 +1,5 @@ import { Integrations } from '../../../db/models'; -import { requireLogin, PERMISSIONS } from '../../permissions'; +import { requireLogin, requireAdmin } from '../../permissions'; const integrationMutations = { /** @@ -116,6 +116,6 @@ requireLogin(integrationMutations, 'integrationsSaveMessengerAppearanceData'); requireLogin(integrationMutations, 'integrationsSaveMessengerConfigs'); requireLogin(integrationMutations, 'integrationsCreateFormIntegration'); requireLogin(integrationMutations, 'integrationsEditFormIntegration'); -requireLogin(integrationMutations, 'integrationsRemove', [PERMISSIONS.ADMIN]); +requireAdmin(integrationMutations, 'integrationsRemove'); export default integrationMutations; diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index 8527d7d10..feca7193b 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -1,7 +1,7 @@ import bcrypt from 'bcrypt'; import { Users, Channels } from '../../../db/models'; import utils from '../../../data/utils'; -import { requireLogin, PERMISSIONS } from '../../permissions'; +import { requireLogin, requireAdmin } from '../../permissions'; const userMutations = { /* @@ -176,6 +176,6 @@ requireLogin(userMutations, 'usersAdd'); requireLogin(userMutations, 'usersEdit'); requireLogin(userMutations, 'usersChangePassword'); requireLogin(userMutations, 'usersEditProfile'); -requireLogin(userMutations, 'usersRemove', [PERMISSIONS.ADMIN]); +requireAdmin(userMutations, 'usersRemove'); export default userMutations; From 9b00591b89fc6ae43f797f49146ac955161d6fb5 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 4 Nov 2017 15:36:43 +0800 Subject: [PATCH 191/318] #21 Refactor --- src/index.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/index.js b/src/index.js index 2f20efc91..33a1fd0cb 100755 --- a/src/index.js +++ b/src/index.js @@ -13,7 +13,6 @@ import { connect } from './db/connection'; import { userMiddleware } from './auth'; import schema from './data'; import './cronJobs'; -import { setAuthPermissions } from './data/permissions'; // load environment variables dotenv.config(); @@ -86,6 +85,3 @@ if (process.env.NODE_ENV === 'development') { }), ); } - -// set user login required wrapper for all queries and mutations -setAuthPermissions(); From d05cbe4b23ed61386eeb87308268a2f622b3bec2 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 4 Nov 2017 15:45:47 +0800 Subject: [PATCH 192/318] #21 Add documentation on permissions --- src/data/permissions.js | 47 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/data/permissions.js b/src/data/permissions.js index 3e30625c2..ae3d22040 100644 --- a/src/data/permissions.js +++ b/src/data/permissions.js @@ -1,40 +1,83 @@ import { ROLES } from './constants'; +/** + * Checks whether user is logged in or not + * @param {Object} user - User object + * @throws {Exception} throws Error('Login required') + * @return {null} + */ export const checkLogin = user => { if (!user) { throw new Error('Login required'); } }; +/** + * Checks if user is logged and if user is admin + * @param {Object} user - User object + * @throws {Exception} throws Error('Permission required') + * @return {null} + */ export const checkAdmin = user => { if (user.role != ROLES.ADMIN) { throw new Error('Permission required'); } }; +/** + * Wraps object property (function) with permission checkers + * @param {Object} cls - Object + * @param {string} methodName - name of the property (method) of the object + * @param {function[]} checkers - List of permission checkers + * @return {function} returns wrapped method + */ export const permissionWrapper = (cls, methodName, checkers) => { const oldMethod = cls[methodName]; - cls[methodName] = (root, object2, { user }) => { + cls[methodName] = (root, args, { user }) => { for (let checker of checkers) { checker(user); } - return oldMethod(root, object2, { user }); + return oldMethod(root, args, { user }); }; }; +/** + * Wraps a method with 'Login required' permission checker + * @param {Object} cls - Object + * @param {string} methodName - name of the property (method) of the object + * @return {function} returns wrapped method + */ export const requireLogin = (cls, methodName) => permissionWrapper(cls, methodName, [checkLogin]); +/** + * Wraps a method with 'Permission required' permission checker + * @param {Object} cls - Object + * @param {string} methodName - name of the property (method) of the object + * @return {function} returns wrapped method + */ export const requireAdmin = (cls, methodName) => permissionWrapper(cls, methodName, [checkLogin, checkAdmin]); +/** + * Wraps all properties (methods) of a given object with 'Login required' permission checker + * @param {Object} cls - Object + * @param {string} methodName - name of the property (method) of the object + * @return {function} returns wrapped method + */ export const moduleRequireLogin = mdl => { for (let method in mdl) { requireLogin(mdl, method); } }; +/** + * Wraps all properties (methods) of a given object with 'Permission required' permission checker + * @param {Object} cls - Object + * @param {string} methodName - name of the property (method) of the object + * @return {function} returns wrapped method + */ export const moduleRequireAdmin = mdl => { for (let method in mdl) { requireAdmin(mdl, method); From 7f51e85be0312c92d780c57a66d66c1b5fc69efc Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 5 Nov 2017 11:33:32 +0800 Subject: [PATCH 193/318] Add configEmailSignatures --- src/__tests__/userMutations.test.js | 16 +++++++++++++++- src/data/resolvers/mutations/users.js | 6 ++++++ src/data/schema/user.js | 8 ++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js index 9ca070842..5518117af 100644 --- a/src/__tests__/userMutations.test.js +++ b/src/__tests__/userMutations.test.js @@ -76,7 +76,7 @@ describe('User mutations', () => { } }; - expect.assertions(5); + expect.assertions(6); // users change password checkLogin(userMutations.usersChangePassword, {}); @@ -92,6 +92,9 @@ describe('User mutations', () => { // users remove checkLogin(userMutations.usersRemove, {}); + + // users remove + checkLogin(userMutations.usersConfigEmailSignatures, {}); }); test(`test if Error('Permission required') error is working as intended`, async () => { @@ -281,4 +284,15 @@ describe('User mutations', () => { // ensure removed expect(await Users.findOne({ _id: removeUserId })).toBe(null); }); + + test('User config email signatures', async () => { + const user = await userFactory({}); + const signatures = [{ brandId: 'DFADF', signature: 'signature' }]; + + Users.configEmailSignatures = jest.fn(); + + await userMutations.usersConfigEmailSignatures({}, { signatures }, { user }); + + expect(Users.configEmailSignatures).toBeCalledWith(user._id, signatures); + }); }); diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index feca7193b..b37aafa81 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -170,6 +170,12 @@ const userMutations = { return Users.removeUser(_id); }, + + usersConfigEmailSignatures(root, { signatures }, { user }) { + if (!user) throw new Error('Login required'); + + return Users.configEmailSignatures(user._id, signatures); + }, }; requireLogin(userMutations, 'usersAdd'); diff --git a/src/data/schema/user.js b/src/data/schema/user.js index d304d7657..f0df5355a 100644 --- a/src/data/schema/user.js +++ b/src/data/schema/user.js @@ -6,12 +6,18 @@ export const types = ` twitterUsername: String } + input EmailSignature { + brandId: String + signature: String + } + type User { _id: String! username: String email: String role: String details: JSON + emailSignatures: JSON } type AuthPayload { @@ -53,4 +59,6 @@ export const mutations = ` usersChangePassword(currentPassword: String!, newPassword: String!): User usersRemove(_id: String!): String + + usersConfigEmailSignatures(signatures: [EmailSignature]): User `; From d131952e3dfe71ab881c04e5c3baf36f6bc18264 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 5 Nov 2017 12:08:35 +0800 Subject: [PATCH 194/318] Add getNotificationByEmail --- src/__tests__/userMutations.test.js | 17 +++++++++++++++-- src/data/resolvers/mutations/users.js | 8 ++++++-- src/data/schema/user.js | 2 ++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js index 5518117af..79939bda9 100644 --- a/src/__tests__/userMutations.test.js +++ b/src/__tests__/userMutations.test.js @@ -76,7 +76,7 @@ describe('User mutations', () => { } }; - expect.assertions(6); + expect.assertions(7); // users change password checkLogin(userMutations.usersChangePassword, {}); @@ -93,8 +93,11 @@ describe('User mutations', () => { // users remove checkLogin(userMutations.usersRemove, {}); - // users remove + // users config email signatures checkLogin(userMutations.usersConfigEmailSignatures, {}); + + // users config get notification by email + checkLogin(userMutations.usersConfigGetNotificationByEmail, {}); }); test(`test if Error('Permission required') error is working as intended`, async () => { @@ -295,4 +298,14 @@ describe('User mutations', () => { expect(Users.configEmailSignatures).toBeCalledWith(user._id, signatures); }); + + test('User config get notification by email', async () => { + const user = await userFactory({}); + + Users.configGetNotificationByEmail = jest.fn(); + + await userMutations.usersConfigGetNotificationByEmail({}, { isAllowed: true }, { user }); + + expect(Users.configGetNotificationByEmail).toBeCalledWith(user._id, true); + }); }); diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index b37aafa81..b9b7ae8b2 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -172,16 +172,20 @@ const userMutations = { }, usersConfigEmailSignatures(root, { signatures }, { user }) { - if (!user) throw new Error('Login required'); - return Users.configEmailSignatures(user._id, signatures); }, + + usersConfigGetNotificationByEmail(root, { isAllowed }, { user }) { + return Users.configGetNotificationByEmail(user._id, isAllowed); + }, }; requireLogin(userMutations, 'usersAdd'); requireLogin(userMutations, 'usersEdit'); requireLogin(userMutations, 'usersChangePassword'); requireLogin(userMutations, 'usersEditProfile'); +requireLogin(userMutations, 'usersConfigGetNotificationByEmail'); +requireLogin(userMutations, 'usersConfigEmailSignatures'); requireAdmin(userMutations, 'usersRemove'); export default userMutations; diff --git a/src/data/schema/user.js b/src/data/schema/user.js index f0df5355a..b94b40e7a 100644 --- a/src/data/schema/user.js +++ b/src/data/schema/user.js @@ -18,6 +18,7 @@ export const types = ` role: String details: JSON emailSignatures: JSON + getNotificationByEmail: Boolean } type AuthPayload { @@ -61,4 +62,5 @@ export const mutations = ` usersRemove(_id: String!): String usersConfigEmailSignatures(signatures: [EmailSignature]): User + usersConfigGetNotificationByEmail(isAllowed: Boolean): User `; From 0aa80845653f3f94298f7aec355ac59574919739 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 5 Nov 2017 12:57:29 +0800 Subject: [PATCH 195/318] Refactor notifications --- src/__tests__/channelMutations.test.js | 6 ++-- src/__tests__/notificationDb.test.js | 14 ++++---- src/__tests__/notificationQueries.test.js | 4 +-- src/__tests__/notificationTools.test.js | 12 +++---- src/data/constants.js | 34 ++++++++++++++++++- src/data/resolvers/mutations/channels.js | 4 +-- src/data/resolvers/mutations/conversations.js | 7 ++-- src/data/resolvers/queries/notifications.js | 15 ++++++-- src/data/schema/notification.js | 5 +-- src/db/models/Notifications.js | 6 ++-- 10 files changed, 75 insertions(+), 32 deletions(-) diff --git a/src/__tests__/channelMutations.test.js b/src/__tests__/channelMutations.test.js index 21bf72bf8..c8044a47f 100644 --- a/src/__tests__/channelMutations.test.js +++ b/src/__tests__/channelMutations.test.js @@ -3,7 +3,7 @@ import { Channels } from '../db/models'; import { ROLES } from '../data/constants'; -import { MODULES } from '../data/constants'; +import { NOTIFICATION_TYPES } from '../data/constants'; import channelMutations from '../data/resolvers/mutations/channels'; import utils from '../data/utils'; @@ -63,7 +63,7 @@ describe('mutations', () => { const sendNotificationDoc = { createdUser: channel.userId, - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, title: content, content, link: `/inbox/${channel._id}`, @@ -104,7 +104,7 @@ describe('mutations', () => { const sendNotificationDoc = { createdUser: channel.userId, - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, title: content, content, link: `/inbox/${channel._id}`, diff --git a/src/__tests__/notificationDb.test.js b/src/__tests__/notificationDb.test.js index ed45129d3..660ccf622 100644 --- a/src/__tests__/notificationDb.test.js +++ b/src/__tests__/notificationDb.test.js @@ -4,7 +4,7 @@ import { connect, disconnect } from '../db/connection'; import { Notifications, NotificationConfigurations, Users } from '../db/models'; import { userFactory, notificationConfigurationFactory } from '../db/factories'; -import { MODULES } from '../data/constants'; +import { NOTIFICATION_TYPES } from '../data/constants'; beforeAll(() => connect()); afterAll(() => disconnect()); @@ -39,13 +39,13 @@ describe('Notification model tests', () => { await notificationConfigurationFactory({ user: _user2._id, - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, isAllowed: false, }); // Create notification let doc = { - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, title: 'new Notification title', content: 'new Notification content', link: 'new Notification link', @@ -63,7 +63,7 @@ describe('Notification model tests', () => { // Create notification ================ let doc = { - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, title: 'new Notification title', content: 'new Notification content', link: 'new Notification link', @@ -119,7 +119,7 @@ describe('NotificationConfiguration model tests', async () => { expect.assertions(1); const doc = { - notifType: MODULES.CONVERSATION_ADD_MESSAGE, + notifType: NOTIFICATION_TYPES.CONVERSATION_ADD_MESSAGE, isAllowed: true, }; @@ -135,7 +135,7 @@ describe('NotificationConfiguration model tests', async () => { const user = await userFactory({}); const doc = { - notifType: MODULES.CONVERSATION_ADD_MESSAGE, + notifType: NOTIFICATION_TYPES.CONVERSATION_ADD_MESSAGE, isAllowed: true, }; @@ -149,7 +149,7 @@ describe('NotificationConfiguration model tests', async () => { expect(notificationConfigurations.user).toEqual(user._id); // creating another notification configuration ============ - doc.notifType = MODULES.CONVERSATION_ASSIGNEE_CHANGE; + doc.notifType = NOTIFICATION_TYPES.CONVERSATION_ASSIGNEE_CHANGE; notificationConfigurations = await NotificationConfigurations.createOrUpdateConfiguration( doc, diff --git a/src/__tests__/notificationQueries.test.js b/src/__tests__/notificationQueries.test.js index 91c650624..9b9c5ed31 100644 --- a/src/__tests__/notificationQueries.test.js +++ b/src/__tests__/notificationQueries.test.js @@ -1,7 +1,7 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { MODULES } from '../data/constants'; +import { NOTIFICATION_MODULES } from '../data/constants'; import notificationsQueries from '../data/resolvers/queries/notifications'; beforeAll(() => connect()); @@ -26,6 +26,6 @@ describe('notificationsQueries', () => { const modules = notificationsQueries.notificationsModules(null, null, { user: { _id: 'fakeUserId' }, }); - expect(modules).toBe(MODULES.ALL); + expect(modules).toBe(NOTIFICATION_MODULES); }); }); diff --git a/src/__tests__/notificationTools.test.js b/src/__tests__/notificationTools.test.js index 6d016485d..56db96845 100644 --- a/src/__tests__/notificationTools.test.js +++ b/src/__tests__/notificationTools.test.js @@ -3,7 +3,7 @@ import { connect, disconnect } from '../db/connection'; import { userFactory, notificationConfigurationFactory, channelFactory } from '../db/factories'; -import { MODULES } from '../data/constants'; +import { NOTIFICATION_TYPES } from '../data/constants'; import utils from '../data/utils'; import { sendChannelNotifications } from '../data/resolvers/mutations/channels'; import { Notifications, NotificationConfigurations, Users } from '../db/models'; @@ -29,25 +29,25 @@ describe('testings helper methods', () => { test('testing tools.sendNotification method', async () => { // Try to send notifications when there is config not allowing it ========= await notificationConfigurationFactory({ - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, isAllowed: false, user: _user._id, }); await notificationConfigurationFactory({ - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, isAllowed: false, user: _user2._id, }); await notificationConfigurationFactory({ - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, isAllowed: false, user: _user3._id, }); const doc = { - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, createdUser: _user._id, title: 'new Notification title', content: 'new Notification content', @@ -92,7 +92,7 @@ describe('testings helper methods', () => { expect(utils.sendNotification).toBeCalledWith({ createdUser: channel.userId, - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, title: content, content, link: `/inbox/${channel._id}`, diff --git a/src/data/constants.js b/src/data/constants.js index 025026e14..56e9748de 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -79,7 +79,7 @@ export const KIND_CHOICES = { }; // module constants -export const MODULES = { +export const NOTIFICATION_TYPES = { CHANNEL_MEMBERS_CHANGE: 'channelMembersChange', CONVERSATION_ADD_MESSAGE: 'conversationAddMessage', CONVERSATION_ASSIGNEE_CHANGE: 'conversationAssigneeChange', @@ -92,6 +92,38 @@ export const MODULES = { ], }; +export const NOTIFICATION_MODULES = [ + { + name: 'conversations', + description: 'Conversations', + types: [ + { + name: 'conversationStateChange', + text: 'State change', + }, + { + name: 'conversationAssigneeChange', + text: 'Assignee change', + }, + { + name: 'conversationAddMessage', + text: 'Add message', + }, + ], + }, + + { + name: 'channels', + description: 'Channels', + types: [ + { + name: 'channelMembersChange', + text: 'Members change', + }, + ], + }, +]; + export const FORM_FIELDS = { TYPES: { INPUT: 'input', diff --git a/src/data/resolvers/mutations/channels.js b/src/data/resolvers/mutations/channels.js index 76deb4706..ab3765970 100644 --- a/src/data/resolvers/mutations/channels.js +++ b/src/data/resolvers/mutations/channels.js @@ -1,4 +1,4 @@ -import { MODULES } from '../../constants'; +import { NOTIFICATION_TYPES } from '../../constants'; import { Channels } from '../../../db/models'; import utils from '../../utils'; import { moduleRequireAdmin } from '../../permissions'; @@ -16,7 +16,7 @@ export const sendChannelNotifications = async channel => { return utils.sendNotification({ createdUser: channel.userId, - notifType: MODULES.CHANNEL_MEMBERS_CHANGE, + notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, title: content, content, link: `/inbox/${channel._id}`, diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index bfd5c7de7..9e7406c83 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -1,4 +1,5 @@ import { Conversations, ConversationMessages, Integrations, Customers } from '../../../db/models'; +import { NOTIFICATION_TYPES } from '../../constants'; import { pubsub } from '../subscriptions'; import { CONVERSATION_STATUSES, KIND_CHOICES } from '../../constants'; import utils from '../../utils'; @@ -86,7 +87,7 @@ const conversationMutations = { // send notification utils.sendNotification({ createdUser: user._id, - notifType: 'conversationAddMessage', + notifType: NOTIFICATION_TYPES.CONVERSATION_ADD_MESSAGE, title, content: doc.content, link: `/inbox/details/${conversation._id}`, @@ -160,7 +161,7 @@ const conversationMutations = { // send notification utils.sendNotification({ createdUser: user._id, - notifType: 'conversationAssigneeChange', + notifType: NOTIFICATION_TYPES.CONVERSATION_ASSIGNEE_CHANGE, title: content, content, link: `/inbox/details/${conversation._id}`, @@ -229,7 +230,7 @@ const conversationMutations = { utils.sendNotification({ createdUser: user._id, - notifType: 'conversationStateChange', + notifType: NOTIFICATION_TYPES.CONVERSATION_STATE_CHANGE, title: content, content, link: `/inbox/details/${conversation._id}`, diff --git a/src/data/resolvers/queries/notifications.js b/src/data/resolvers/queries/notifications.js index 250498939..414076d82 100644 --- a/src/data/resolvers/queries/notifications.js +++ b/src/data/resolvers/queries/notifications.js @@ -1,6 +1,6 @@ -import { MODULES } from '../../constants'; - +import { NOTIFICATION_MODULES } from '../../constants'; import { moduleRequireLogin } from '../../permissions'; +import { NotificationConfigurations } from '../../../db/models'; const notificationQueries = { /** @@ -9,7 +9,16 @@ const notificationQueries = { * @return {String[]} returns module list */ notificationsModules() { - return MODULES.ALL; + return NOTIFICATION_MODULES; + }, + + /** + * Get per user configuration + * @param {Object} args + * @return {[Object]} - user's notification configurations + */ + notificationsGetConfigurations(root, args, { user }) { + return NotificationConfigurations.find({ user: user._id }); }, }; diff --git a/src/data/schema/notification.js b/src/data/schema/notification.js index af2663f7a..edcdd6ce0 100644 --- a/src/data/schema/notification.js +++ b/src/data/schema/notification.js @@ -20,10 +20,11 @@ export const types = ` `; export const queries = ` - notificationsModules : [String] + notificationsModules : [JSON] + notificationsGetConfigurations : [NotificationConfiguration] `; export const mutations = ` - notificationsSaveConfig (notifType: String, isAllowed: Boolean): NotificationConfiguration + notificationsSaveConfig (notifType: String!, isAllowed: Boolean): NotificationConfiguration notificationsMarkAsRead (_ids: [String]!) : Boolean `; diff --git a/src/db/models/Notifications.js b/src/db/models/Notifications.js index 6a7d2daf8..4b4a7da56 100644 --- a/src/db/models/Notifications.js +++ b/src/db/models/Notifications.js @@ -1,6 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -import { MODULES } from '../../data/constants'; +import { NOTIFICATION_TYPES } from '../../data/constants'; // Notification schema const NotificationSchema = new mongoose.Schema({ @@ -11,7 +11,7 @@ const NotificationSchema = new mongoose.Schema({ }, notifType: { type: String, - enum: MODULES.ALL, + enum: NOTIFICATION_TYPES.ALL, }, title: String, link: String, @@ -113,7 +113,7 @@ const ConfigSchema = new mongoose.Schema({ user: String, notifType: { type: String, - enum: MODULES.ALL, + enum: NOTIFICATION_TYPES.ALL, }, isAllowed: Boolean, }); From 54ef3b430a443d42df7c13faf2acb1eb2f84abe5 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 5 Nov 2017 23:20:37 +0800 Subject: [PATCH 196/318] Add model ActivityLogs --- src/__tests__/activityLogDb.test.js | 49 +++++++++++++ src/data/constants.js | 8 +-- src/db/factories.js | 4 +- src/db/models/ActivityLogs.js | 102 ++++++++++++++++++++++++++++ src/db/models/InternalNotes.js | 4 +- src/db/models/Segments.js | 4 +- src/db/models/index.js | 2 + 7 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 src/__tests__/activityLogDb.test.js create mode 100644 src/db/models/ActivityLogs.js diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js new file mode 100644 index 000000000..6322ca8c8 --- /dev/null +++ b/src/__tests__/activityLogDb.test.js @@ -0,0 +1,49 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { CUSTOMER_CONTENT_TYPES } from '../data/constants'; +import { ActivityLogs } from '../db/models'; +import { ACTION_PERFORMER_TYPES, ACTIVITY_TYPES } from '../db/models/ActivityLogs'; +import { userFactory, internalNoteFactory, customerFactory } from '../db/factories'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('ActivityLogs model methods', () => { + test(`createInternalNoteLog without setting 'actionPerformedBy'`, async () => { + const customer = await customerFactory(); + + const internalNote = await internalNoteFactory({ + contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + contentTypeId: customer, + }); + + const aLog = await ActivityLogs.createInternalNoteLog(internalNote); + + expect(aLog.performedBy.type).toBe(ACTION_PERFORMER_TYPES.SYSTEM); + expect(aLog.contentType).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); + expect(aLog.contentTypeId).toBe(internalNote._id); + expect(aLog.activityType).toBe(ACTIVITY_TYPES.INTERNAL_NOTE_CREATED); + }); + + test(`createInternalNoteLog with setting 'actionPerformedBy'`, async () => { + const user = await userFactory({}); + + const customer = await customerFactory(); + + const internalNote = await internalNoteFactory({ + contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + contentTypeId: customer, + }); + + const aLog = await ActivityLogs.createInternalNoteLog(internalNote, user); + + expect(aLog.performedBy.type).toBe(ACTION_PERFORMER_TYPES.USER); + expect(aLog.performedBy.id).toBe(user._id); + expect(aLog.contentType).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); + expect(aLog.contentTypeId).toBe(internalNote._id); + expect(aLog.activityType).toBe(ACTIVITY_TYPES.INTERNAL_NOTE_CREATED); + // expect(aLog.actionPerformedBy).toBe(user); + }); +}); diff --git a/src/data/constants.js b/src/data/constants.js index 56e9748de..3df029238 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -170,13 +170,7 @@ export const FIELD_CONTENT_TYPES = { ALL: ['form', 'customer', 'company'], }; -export const INTERNAL_NOTE_CONTENT_TYPES = { - CUSTOMER: 'customer', - COMPANY: 'company', - ALL: ['customer', 'company'], -}; - -export const SEGMENT_CONTENT_TYPES = { +export const CUSTOMER_CONTENT_TYPES = { CUSTOMER: 'customer', COMPANY: 'company', ALL: ['customer', 'company'], diff --git a/src/db/factories.js b/src/db/factories.js index 5ec3e2e7b..0b0c4c396 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -1,6 +1,6 @@ import faker from 'faker'; import Random from 'meteor-random'; -import { MODULES } from '../data/constants'; +import { MODULES, CUSTOMER_CONTENT_TYPES } from '../data/constants'; import { Users, @@ -141,7 +141,7 @@ export const segmentFactory = (params = {}) => { export const internalNoteFactory = (params = {}) => { const internalNote = new InternalNotes({ - contentType: params.contentType || 'customer', + contentType: params.contentType || CUSTOMER_CONTENT_TYPES.CUSTOMER, contentTypeId: params.contentTypeId || 'DFASFDFSDAFDF', content: params.content || faker.random.word(), }); diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js new file mode 100644 index 000000000..f385e5e18 --- /dev/null +++ b/src/db/models/ActivityLogs.js @@ -0,0 +1,102 @@ +import mongoose from 'mongoose'; +import Random from 'meteor-random'; +import { CUSTOMER_CONTENT_TYPES } from '../../data/constants'; + +export const ACTIVITY_TYPES = { + USER_REGISTERED: 'user_registered', + COMPANY_REGISTERED: 'company_registered', + INTERNAL_NOTE_CREATED: 'internal_note_created', + CONVERSATION_CREATED: 'conversation_created', + SEGMENT_UPDATED: 'segment_updated', + + ALL: [ + 'user_registered', + 'company_registered', + 'internal_note_created', + 'conversation_created', + 'segment_updated', + ], +}; + +export const ACTION_PERFORMER_TYPES = { + SYSTEM: 'ACTION_PERFORMER_SYSTEM', + USER: 'ACTION_PERFORMER_USER', + + ALL: ['ACTION_PERFORMER_SYSTEM', 'ACTION_PERFORMER_USER'], +}; + +export const ACTION_PERFORMER_SYSTEM = 'ACTION_PERFORMER_SYSTEM'; + +const ActionPerformer = mongoose.Schema({ + type: { + type: String, + enum: ACTION_PERFORMER_TYPES.ALL, + default: ACTION_PERFORMER_TYPES.SYSTEM, + required: true, + }, + id: { + type: String, + }, +}); + +const ActivityLogSchema = mongoose.Schema({ + _id: { + type: String, + unique: true, + default: () => Random.id(), + }, + activityType: { + type: String, + required: true, + enum: ACTIVITY_TYPES.ALL, + }, + createdAt: { + type: Date, + required: true, + default: Date.now(), + }, + contentTypeId: { + type: String, + required: true, + }, + contentType: { + type: String, + enum: CUSTOMER_CONTENT_TYPES.ALL, + required: true, + }, + performedBy: ActionPerformer, +}); + +class ActivityLog { + static createDoc({ performedBy, ...doc }) { + if (performedBy && performedBy._id) { + performedBy = { + type: ACTION_PERFORMER_TYPES.USER, + id: performedBy._id, + }; + } else { + performedBy = {}; + } + + return this.create({ performedBy, ...doc }); + } + + /** + * Create activity log for internal note + * @param {InternalNote} internalNote - Internal note document + * @param {User|undefined} performedBy - User collection document + * @return {Promise} returns Promise resolving created ActivityLog document + */ + static createInternalNoteLog(internalNote, performedBy) { + return this.createDoc({ + activityType: ACTIVITY_TYPES.INTERNAL_NOTE_CREATED, + contentType: internalNote.contentType, + contentTypeId: internalNote._id, + performedBy, + }); + } +} + +ActivityLogSchema.loadClass(ActivityLog); + +export default mongoose.model('activity_logs', ActivityLogSchema); diff --git a/src/db/models/InternalNotes.js b/src/db/models/InternalNotes.js index 76d399607..bc10cf652 100644 --- a/src/db/models/InternalNotes.js +++ b/src/db/models/InternalNotes.js @@ -1,6 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -import { INTERNAL_NOTE_CONTENT_TYPES } from '../../data/constants'; +import { CUSTOMER_CONTENT_TYPES } from '../../data/constants'; /* * internal note schema @@ -13,7 +13,7 @@ const InternalNoteSchema = mongoose.Schema({ }, contentType: { type: String, - enum: INTERNAL_NOTE_CONTENT_TYPES.ALL, + enum: CUSTOMER_CONTENT_TYPES.ALL, }, contentTypeId: String, content: { diff --git a/src/db/models/Segments.js b/src/db/models/Segments.js index 5fb4354f8..c3aa27d6f 100644 --- a/src/db/models/Segments.js +++ b/src/db/models/Segments.js @@ -1,6 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -import { SEGMENT_CONTENT_TYPES } from '../../data/constants'; +import { CUSTOMER_CONTENT_TYPES } from '../../data/constants'; const ConditionSchema = mongoose.Schema( { @@ -29,7 +29,7 @@ const SegmentSchema = mongoose.Schema({ }, contentType: { type: String, - enum: SEGMENT_CONTENT_TYPES.ALL, + enum: CUSTOMER_CONTENT_TYPES.ALL, }, name: String, description: String, diff --git a/src/db/models/index.js b/src/db/models/index.js index 469390f3b..bcef06135 100644 --- a/src/db/models/index.js +++ b/src/db/models/index.js @@ -20,6 +20,7 @@ import { KnowledgeBaseTopics, } from './KnowledgeBase'; import { Notifications, NotificationConfigurations } from './Notifications'; +import ActivityLogs from './ActivityLogs'; export { Users, @@ -43,4 +44,5 @@ export { KnowledgeBaseTopics, Notifications, NotificationConfigurations, + ActivityLogs, }; From 25ef99093b3a374c65c6e75203748f65ee1c97ad Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 5 Nov 2017 23:28:25 +0800 Subject: [PATCH 197/318] #22 Refactor ActivityLogs.createInternalNoteLog --- src/__tests__/activityLogDb.test.js | 17 +++++++++-------- src/db/models/ActivityLogs.js | 3 +++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index 6322ca8c8..4ca8c795a 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -11,7 +11,8 @@ beforeAll(() => connect()); afterAll(() => disconnect()); describe('ActivityLogs model methods', () => { - test(`createInternalNoteLog without setting 'actionPerformedBy'`, async () => { + test(`test if exception is being thrown when calling + createInternalNoteLog without setting 'actionPerformedBy'`, async () => { const customer = await customerFactory(); const internalNote = await internalNoteFactory({ @@ -19,12 +20,13 @@ describe('ActivityLogs model methods', () => { contentTypeId: customer, }); - const aLog = await ActivityLogs.createInternalNoteLog(internalNote); - - expect(aLog.performedBy.type).toBe(ACTION_PERFORMER_TYPES.SYSTEM); - expect(aLog.contentType).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); - expect(aLog.contentTypeId).toBe(internalNote._id); - expect(aLog.activityType).toBe(ACTIVITY_TYPES.INTERNAL_NOTE_CREATED); + try { + await ActivityLogs.createInternalNoteLog(internalNote); + } catch (e) { + expect(e.message).toBe( + `'performedBy' must be supplied when adding activity log for internal note`, + ); + } }); test(`createInternalNoteLog with setting 'actionPerformedBy'`, async () => { @@ -44,6 +46,5 @@ describe('ActivityLogs model methods', () => { expect(aLog.contentType).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); expect(aLog.contentTypeId).toBe(internalNote._id); expect(aLog.activityType).toBe(ACTIVITY_TYPES.INTERNAL_NOTE_CREATED); - // expect(aLog.actionPerformedBy).toBe(user); }); }); diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index f385e5e18..0b91a0bc1 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -88,6 +88,9 @@ class ActivityLog { * @return {Promise} returns Promise resolving created ActivityLog document */ static createInternalNoteLog(internalNote, performedBy) { + if (performedBy == null || (performedBy && !performedBy._id)) { + throw new Error(`'performedBy' must be supplied when adding activity log for internal note`); + } return this.createDoc({ activityType: ACTIVITY_TYPES.INTERNAL_NOTE_CREATED, contentType: internalNote.contentType, From 23c0d399ba8053b1078cadff04d73b672f017e9b Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 5 Nov 2017 23:39:32 +0800 Subject: [PATCH 198/318] #22 Add test for ActivityLogs.createDoc --- src/__tests__/activityLogDb.test.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index 4ca8c795a..69da2f0bf 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -11,13 +11,30 @@ beforeAll(() => connect()); afterAll(() => disconnect()); describe('ActivityLogs model methods', () => { - test(`test if exception is being thrown when calling - createInternalNoteLog without setting 'actionPerformedBy'`, async () => { + test(`check whether not setting 'performedBy' + is setting expected values in the collection or not`, async () => { + const doc = { + activityType: ACTIVITY_TYPES.INTERNAL_NOTE_CREATED, + contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + contentTypeId: 'fakeCustomerId', + performedBy: null, + }; + + const aLog = await ActivityLogs.createDoc(doc); + + expect(aLog.activityType).toBe(ACTIVITY_TYPES.INTERNAL_NOTE_CREATED); + expect(aLog.contentType).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); + expect(aLog.contentTypeId).toBe(doc.contentTypeId); + expect(aLog.performedBy.type).toBe(ACTION_PERFORMER_TYPES.SYSTEM); + }); + + test(`check if exception is being thrown when calling + createInternalNoteLog without setting 'actionPerformedBy'`, async () => { const customer = await customerFactory(); const internalNote = await internalNoteFactory({ contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, - contentTypeId: customer, + contentTypeId: customer._id, }); try { From eaa1507803597d8653fca69cf66c62504b713bb7 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Mon, 6 Nov 2017 00:11:06 +0800 Subject: [PATCH 199/318] #22 Fixed a test --- src/__tests__/activityLogDb.test.js | 5 +++++ src/data/resolvers/queries/customers.js | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index 69da2f0bf..74583fcfa 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -64,4 +64,9 @@ describe('ActivityLogs model methods', () => { expect(aLog.contentTypeId).toBe(internalNote._id); expect(aLog.activityType).toBe(ACTIVITY_TYPES.INTERNAL_NOTE_CREATED); }); + + test(`check if exception is being thrown when calling + createSegmentLog without setting 'actionPerformedBy'`, async () => {}); + + test(`createSegmentLog with setting 'actionPerformedBy'`, async () => {}); }); diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index b35d05b48..5e0b499a8 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -1,6 +1,6 @@ import _ from 'underscore'; import { Brands, Tags, Integrations, Customers, Segments } from '../../../db/models'; -import { TAG_TYPES, INTEGRATION_KIND_CHOICES, SEGMENT_CONTENT_TYPES } from '../../constants'; +import { TAG_TYPES, INTEGRATION_KIND_CHOICES, CUSTOMER_CONTENT_TYPES } from '../../constants'; import QueryBuilder from './segmentQueryBuilder.js'; import { moduleRequireLogin } from '../../permissions'; @@ -82,7 +82,7 @@ const customerQueries = { // Count customers by segments const segments = await Segments.find({ - contentType: SEGMENT_CONTENT_TYPES.CUSTOMER, + contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, }); for (let s of segments) { From 26e42d736f1b92b878fd21228c0d6843dd4134e5 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Mon, 6 Nov 2017 01:19:16 +0800 Subject: [PATCH 200/318] Fixed a bug where a segment with a condition 'equal to name' where name starts with an uppercase letter never would find the expected results --- src/data/resolvers/mutations/segments.js | 6 +++--- src/data/resolvers/queries/segmentQueryBuilder.js | 5 +++-- src/data/resolvers/queries/segments.js | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/data/resolvers/mutations/segments.js b/src/data/resolvers/mutations/segments.js index fbafd0553..442692a7e 100644 --- a/src/data/resolvers/mutations/segments.js +++ b/src/data/resolvers/mutations/segments.js @@ -2,7 +2,7 @@ import { Segments } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; -const segmentQueries = { +const segmentMutations = { /** * Create new segment * @return {Promise} segment object @@ -28,6 +28,6 @@ const segmentQueries = { }, }; -moduleRequireLogin(segmentQueries); +moduleRequireLogin(segmentMutations); -export default segmentQueries; +export default segmentMutations; diff --git a/src/data/resolvers/queries/segmentQueryBuilder.js b/src/data/resolvers/queries/segmentQueryBuilder.js index 50ad79833..8533b9ee1 100644 --- a/src/data/resolvers/queries/segmentQueryBuilder.js +++ b/src/data/resolvers/queries/segmentQueryBuilder.js @@ -54,15 +54,16 @@ function convertConditionToQuery(condition) { switch (operator) { case 'e': + return { $regex: `^${escapeRegExp(transformedValue)}$`, $options: 'i' }; case 'et': default: return transformedValue; case 'dne': return { $ne: transformedValue }; case 'c': - return { $regex: new RegExp(`.*${escapeRegExp(transformedValue)}.*`, 'i') }; + return { $regex: `.*${escapeRegExp(transformedValue)}.*`, $options: 'i' }; case 'dnc': - return { $regex: new RegExp(`^((?!${escapeRegExp(transformedValue)}).)*$`, 'i') }; + return { $regex: `^((?!${escapeRegExp(transformedValue)}).)*$`, $options: 'i' }; case 'igt': return { $gt: transformedValue }; case 'ilt': diff --git a/src/data/resolvers/queries/segments.js b/src/data/resolvers/queries/segments.js index ed7178d15..a28e5c0fb 100644 --- a/src/data/resolvers/queries/segments.js +++ b/src/data/resolvers/queries/segments.js @@ -2,7 +2,7 @@ import { Segments } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; -const segmentMutations = { +const segmentQueries = { /** * Segments list * @return {Promise} segment objects @@ -30,6 +30,6 @@ const segmentMutations = { }, }; -moduleRequireLogin(segmentMutations); +moduleRequireLogin(segmentQueries); -export default segmentMutations; +export default segmentQueries; From bcbcd4c0a6c4440f3e72f22fd29d220c432aff45 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Mon, 6 Nov 2017 15:52:18 +0800 Subject: [PATCH 201/318] #22 Refactor ActivityLog schema --- src/__tests__/activityLogDb.test.js | 37 +++++-- src/db/models/ActivityLogs.js | 143 ++++++++++++++++++++-------- 2 files changed, 129 insertions(+), 51 deletions(-) diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index 74583fcfa..bda3a778f 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -4,7 +4,11 @@ import { connect, disconnect } from '../db/connection'; import { CUSTOMER_CONTENT_TYPES } from '../data/constants'; import { ActivityLogs } from '../db/models'; -import { ACTION_PERFORMER_TYPES, ACTIVITY_TYPES } from '../db/models/ActivityLogs'; +import { + ACTION_PERFORMER_TYPES, + ACTIVITY_TYPES, + ACTIVITY_ACTIONS, +} from '../db/models/ActivityLogs'; import { userFactory, internalNoteFactory, customerFactory } from '../db/factories'; beforeAll(() => connect()); @@ -13,18 +17,27 @@ afterAll(() => disconnect()); describe('ActivityLogs model methods', () => { test(`check whether not setting 'performedBy' is setting expected values in the collection or not`, async () => { + const activityDoc = { + type: ACTIVITY_TYPES.INTERNAL_NOTE, + action: ACTIVITY_ACTIONS.CREATE, + id: 'testInternalNoteId', + }; + + const customerDoc = { + type: CUSTOMER_CONTENT_TYPES.CUSTOMER, + id: 'testCustomerId', + }; + const doc = { - activityType: ACTIVITY_TYPES.INTERNAL_NOTE_CREATED, - contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, - contentTypeId: 'fakeCustomerId', + activity: activityDoc, + customer: customerDoc, performedBy: null, }; const aLog = await ActivityLogs.createDoc(doc); - expect(aLog.activityType).toBe(ACTIVITY_TYPES.INTERNAL_NOTE_CREATED); - expect(aLog.contentType).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); - expect(aLog.contentTypeId).toBe(doc.contentTypeId); + expect(aLog.activity.toObject()).toEqual(activityDoc); + expect(aLog.customer.toObject()).toEqual(customerDoc); expect(aLog.performedBy.type).toBe(ACTION_PERFORMER_TYPES.SYSTEM); }); @@ -60,9 +73,13 @@ describe('ActivityLogs model methods', () => { expect(aLog.performedBy.type).toBe(ACTION_PERFORMER_TYPES.USER); expect(aLog.performedBy.id).toBe(user._id); - expect(aLog.contentType).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); - expect(aLog.contentTypeId).toBe(internalNote._id); - expect(aLog.activityType).toBe(ACTIVITY_TYPES.INTERNAL_NOTE_CREATED); + expect(aLog.customer.type).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); + expect(aLog.customer.id).toBe(internalNote.contentTypeId); + expect(aLog.activity.toObject()).toEqual({ + type: ACTIVITY_TYPES.INTERNAL_NOTE, + action: ACTIVITY_ACTIONS.CREATE, + id: internalNote._id, + }); }); test(`check if exception is being thrown when calling diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 0b91a0bc1..0cbc9d648 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -3,19 +3,21 @@ import Random from 'meteor-random'; import { CUSTOMER_CONTENT_TYPES } from '../../data/constants'; export const ACTIVITY_TYPES = { - USER_REGISTERED: 'user_registered', - COMPANY_REGISTERED: 'company_registered', - INTERNAL_NOTE_CREATED: 'internal_note_created', - CONVERSATION_CREATED: 'conversation_created', - SEGMENT_UPDATED: 'segment_updated', - - ALL: [ - 'user_registered', - 'company_registered', - 'internal_note_created', - 'conversation_created', - 'segment_updated', - ], + CUSTOMER: 'customer', + COMPANY: 'company', + INTERNAL_NOTE: 'internal_note', + CONVERSATION: 'conversation', + SEGMENT: 'segment', + + ALL: ['customer', 'company', 'internal_note', 'conversation', 'segment'], +}; + +export const ACTIVITY_ACTIONS = { + CREATE: 'create', + UPDATE: 'update', + DELETE: 'delete', + + ALL: ['create', 'update', 'delete'], }; export const ACTION_PERFORMER_TYPES = { @@ -27,17 +29,78 @@ export const ACTION_PERFORMER_TYPES = { export const ACTION_PERFORMER_SYSTEM = 'ACTION_PERFORMER_SYSTEM'; -const ActionPerformer = mongoose.Schema({ - type: { - type: String, - enum: ACTION_PERFORMER_TYPES.ALL, - default: ACTION_PERFORMER_TYPES.SYSTEM, - required: true, +// Performer of the action: +// *system* cron job, user +// ex: Sales manager that has registered a new customer +// Sales manager is the action performer +const ActionPerformer = mongoose.Schema( + { + type: { + type: String, + enum: ACTION_PERFORMER_TYPES.ALL, + default: ACTION_PERFORMER_TYPES.SYSTEM, + required: true, + }, + id: { + type: String, + }, }, - id: { - type: String, + { _id: false }, +); + +// The action that is being performed +// ex1: A user writes an internal note +// in this case: type is InternalNote +// action is create (write) +// id is the InternalNote id +// ex2: Sales manager registers a new customer +// in this case: type is customer +// action is create (register) +// id is Customer id +// customer and activity contentTypes are the same in this case +// ex3: Cronjob runs and a customer is found to be suitable for a particular segment +// action is create: a new segment user +// type is segment +// id is Segment id +// ex4: An internalNote concerning a customer was updated +// action is update +// type is InternalNote +// id is InternalNote id +const Activity = mongoose.Schema( + { + type: { + type: String, + required: true, + enum: ACTIVITY_TYPES.ALL, + }, + action: { + type: String, + required: true, + enum: ACTIVITY_ACTIONS.ALL, + }, + id: { + type: String, + }, }, -}); + { _id: false }, +); + +// the customer that is related to a given ActivityLog +// can be both Company or Customer documents +const Customer = mongoose.Schema( + { + id: { + type: String, + required: true, + }, + type: { + type: String, + enum: CUSTOMER_CONTENT_TYPES.ALL, + required: true, + }, + }, + { _id: false }, +); const ActivityLogSchema = mongoose.Schema({ _id: { @@ -45,26 +108,16 @@ const ActivityLogSchema = mongoose.Schema({ unique: true, default: () => Random.id(), }, - activityType: { - type: String, - required: true, - enum: ACTIVITY_TYPES.ALL, - }, + + activity: Activity, + performedBy: ActionPerformer, + customer: Customer, + createdAt: { type: Date, required: true, default: Date.now(), }, - contentTypeId: { - type: String, - required: true, - }, - contentType: { - type: String, - enum: CUSTOMER_CONTENT_TYPES.ALL, - required: true, - }, - performedBy: ActionPerformer, }); class ActivityLog { @@ -84,18 +137,26 @@ class ActivityLog { /** * Create activity log for internal note * @param {InternalNote} internalNote - Internal note document - * @param {User|undefined} performedBy - User collection document + * @param {User} performedBy - User collection document + * @param {Customer|Company} customer - Customer or Company document * @return {Promise} returns Promise resolving created ActivityLog document */ static createInternalNoteLog(internalNote, performedBy) { if (performedBy == null || (performedBy && !performedBy._id)) { throw new Error(`'performedBy' must be supplied when adding activity log for internal note`); } + return this.createDoc({ - activityType: ACTIVITY_TYPES.INTERNAL_NOTE_CREATED, - contentType: internalNote.contentType, - contentTypeId: internalNote._id, + activity: { + type: ACTIVITY_TYPES.INTERNAL_NOTE, + action: ACTIVITY_ACTIONS.CREATE, + id: internalNote._id, + }, performedBy, + customer: { + id: internalNote.contentTypeId, + type: internalNote.contentType, + }, }); } } From fd608ecf8525d387e0c913772d8b0b7e704b440e Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Mon, 6 Nov 2017 16:38:00 +0800 Subject: [PATCH 202/318] #22 Add method ActivityLogs.createConversationLog and its tests --- src/__tests__/activityLogDb.test.js | 55 ++++++++++++++++++++++++++--- src/db/models/ActivityLogs.js | 45 ++++++++++++++++++----- 2 files changed, 86 insertions(+), 14 deletions(-) diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index bda3a778f..5fc8b842a 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -9,13 +9,18 @@ import { ACTIVITY_TYPES, ACTIVITY_ACTIONS, } from '../db/models/ActivityLogs'; -import { userFactory, internalNoteFactory, customerFactory } from '../db/factories'; +import { + userFactory, + internalNoteFactory, + customerFactory, + conversationFactory, +} from '../db/factories'; beforeAll(() => connect()); afterAll(() => disconnect()); describe('ActivityLogs model methods', () => { - test(`check whether not setting 'performedBy' + test(`check whether not setting 'user' is setting expected values in the collection or not`, async () => { const activityDoc = { type: ACTIVITY_TYPES.INTERNAL_NOTE, @@ -53,9 +58,7 @@ describe('ActivityLogs model methods', () => { try { await ActivityLogs.createInternalNoteLog(internalNote); } catch (e) { - expect(e.message).toBe( - `'performedBy' must be supplied when adding activity log for internal note`, - ); + expect(e.message).toBe(`'user' must be supplied when adding activity log for internal note`); } }); @@ -82,8 +85,50 @@ describe('ActivityLogs model methods', () => { }); }); + // TODO: write this test test(`check if exception is being thrown when calling createSegmentLog without setting 'actionPerformedBy'`, async () => {}); + // TODO: write this test test(`createSegmentLog with setting 'actionPerformedBy'`, async () => {}); + + test(`check if exceptions are being thrown as intended when calling createConversationLog`, async () => { + const conversation = await conversationFactory({}); + const customer = await customerFactory({}); + + try { + await ActivityLogs.createConversationLog(conversation, null, customer); + } catch (e) { + expect(e.message).toBe(`'user' must be supplied when adding activity log for internal note`); + } + + try { + await ActivityLogs.createConversationLog(conversation, conversation, null); + } catch (e) { + expect(e.message).toBe( + `'customer' must be supplied when adding activity log for conversations`, + ); + } + }); + + test(`check if createConversationLog is working as intended`, async () => { + const conversation = await conversationFactory({}); + const customer = await customerFactory({}); + const customerDoc = { + type: CUSTOMER_CONTENT_TYPES.CUSTOMER, + id: customer._id, + }; + const user = await userFactory({}); + + const aLog = await ActivityLogs.createConversationLog(conversation, user, customerDoc); + + expect(aLog.performedBy.type).toBe(ACTION_PERFORMER_TYPES.USER); + expect(aLog.performedBy.id).toBe(user._id); + expect(aLog.customer.toObject()).toEqual(customerDoc); + expect(aLog.activity.toObject()).toEqual({ + type: ACTIVITY_TYPES.CONVERSATION, + action: ACTIVITY_ACTIONS.CREATE, + id: conversation._id, + }); + }); }); diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 0cbc9d648..b729fa92e 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -21,14 +21,12 @@ export const ACTIVITY_ACTIONS = { }; export const ACTION_PERFORMER_TYPES = { - SYSTEM: 'ACTION_PERFORMER_SYSTEM', - USER: 'ACTION_PERFORMER_USER', + SYSTEM: 'SYSTEM', + USER: 'USER', - ALL: ['ACTION_PERFORMER_SYSTEM', 'ACTION_PERFORMER_USER'], + ALL: ['SYSTEM', 'USER'], }; -export const ACTION_PERFORMER_SYSTEM = 'ACTION_PERFORMER_SYSTEM'; - // Performer of the action: // *system* cron job, user // ex: Sales manager that has registered a new customer @@ -141,9 +139,9 @@ class ActivityLog { * @param {Customer|Company} customer - Customer or Company document * @return {Promise} returns Promise resolving created ActivityLog document */ - static createInternalNoteLog(internalNote, performedBy) { - if (performedBy == null || (performedBy && !performedBy._id)) { - throw new Error(`'performedBy' must be supplied when adding activity log for internal note`); + static createInternalNoteLog(internalNote, user) { + if (user == null || (user && !user._id)) { + throw new Error(`'user' must be supplied when adding activity log for internal note`); } return this.createDoc({ @@ -152,13 +150,42 @@ class ActivityLog { action: ACTIVITY_ACTIONS.CREATE, id: internalNote._id, }, - performedBy, + performedBy: user, customer: { id: internalNote.contentTypeId, type: internalNote.contentType, }, }); } + + /** + * @param {Object} conversation - Conversation object + * @param {string} conversation._id - Conversation document id + * @param {Object} user - User object + * @param {Object} user._id - User document id + * @param {Object} customer - Customer object + * @param {string} customer.type - One of CUSTOMER_CONTENT_TYPES choices + * @param {string} customer.id - Customer document id + */ + static createConversationLog(conversation, user, customer) { + if (user == null || (user && !user._id)) { + throw new Error(`'user' must be supplied when adding activity log for internal note`); + } + + if (customer == null || (customer && !user._id)) { + throw new Error(`'customer' must be supplied when adding activity log for conversations`); + } + + return this.createDoc({ + activity: { + type: ACTIVITY_TYPES.CONVERSATION, + action: ACTIVITY_ACTIONS.CREATE, + id: conversation._id, + }, + performedBy: user, + customer, + }); + } } ActivityLogSchema.loadClass(ActivityLog); From d905cd5fe9b66424327ddb2ec9e4ac2b15254b33 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 6 Nov 2017 18:47:47 +0800 Subject: [PATCH 203/318] Add pagination to most of the queries --- src/data/resolvers/queries/brands.js | 16 ++----- src/data/resolvers/queries/channels.js | 17 +++---- src/data/resolvers/queries/emailTemplates.js | 14 ++---- src/data/resolvers/queries/engages.js | 13 +++--- src/data/resolvers/queries/forms.js | 16 ++----- src/data/resolvers/queries/integrations.js | 21 +++------ src/data/resolvers/queries/knowledgeBase.js | 46 ++++++------------- .../resolvers/queries/responseTemplates.js | 14 ++---- src/data/resolvers/queries/users.js | 17 ++----- src/data/resolvers/queries/utils.js | 8 ++++ src/data/schema/brand.js | 2 +- src/data/schema/channel.js | 2 +- src/data/schema/emailTemplate.js | 2 +- src/data/schema/engage.js | 2 +- src/data/schema/form.js | 2 +- src/data/schema/integration.js | 2 +- src/data/schema/knowledgeBase.js | 6 +-- src/data/schema/responseTemplate.js | 2 +- src/data/schema/user.js | 2 +- 19 files changed, 74 insertions(+), 130 deletions(-) create mode 100644 src/data/resolvers/queries/utils.js diff --git a/src/data/resolvers/queries/brands.js b/src/data/resolvers/queries/brands.js index 249dbeffc..c181b485f 100644 --- a/src/data/resolvers/queries/brands.js +++ b/src/data/resolvers/queries/brands.js @@ -1,22 +1,16 @@ import { Brands } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; +import { paginate } from './utils'; const brandQueries = { /** * Brands list - * @param {Object} args - * @param {Integer} args.limit + * @param {Object} params - Query params * @return {Promise} sorted brands list */ - brands(root, { limit }) { - const brands = Brands.find({}); - const sort = { createdAt: -1 }; - - if (limit) { - return brands.sort(sort).limit(limit); - } - - return brands.sort(sort); + brands(root, { params = {} }) { + const brands = paginate(Brands.find({}), params); + return brands.sort({ createdAt: -1 }); }, /** diff --git a/src/data/resolvers/queries/channels.js b/src/data/resolvers/queries/channels.js index 2b45f857c..2777cde55 100644 --- a/src/data/resolvers/queries/channels.js +++ b/src/data/resolvers/queries/channels.js @@ -1,27 +1,22 @@ import { Channels } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; +import { paginate } from './utils'; const channelQueries = { /** * Channels list - * @param {Object} args - * @param {Integer} args.limit - * @param {[String]} args.memberIds + * @param {Object} args - Search params * @return {Promise} filtered channels list by given parameters */ - channels(root, { limit, memberIds }) { + channels(root, { params }) { const query = {}; const sort = { createdAt: -1 }; - if (memberIds) { - query.memberIds = { $in: memberIds }; + if (params.memberIds) { + query.memberIds = { $in: params.memberIds }; } - const channels = Channels.find(query); - - if (limit) { - return channels.limit(limit).sort(sort); - } + const channels = paginate(Channels.find(query), params); return channels.sort(sort); }, diff --git a/src/data/resolvers/queries/emailTemplates.js b/src/data/resolvers/queries/emailTemplates.js index 576bc10a7..b240ef146 100644 --- a/src/data/resolvers/queries/emailTemplates.js +++ b/src/data/resolvers/queries/emailTemplates.js @@ -1,21 +1,15 @@ import { EmailTemplates } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; +import { paginate } from './utils'; const emailTemplateQueries = { /** * Email templates list - * @param {Object} args - * @param {Integer} args.limit + * @param {Object} args - Search params * @return {Promise} email template objects */ - emailTemplates(root, { limit }) { - const emailTemplates = EmailTemplates.find({}); - - if (limit) { - return emailTemplates.limit(limit); - } - - return emailTemplates; + emailTemplates(root, { params }) { + return paginate(EmailTemplates.find({}), params); }, /** diff --git a/src/data/resolvers/queries/engages.js b/src/data/resolvers/queries/engages.js index 8834e78b9..01ccd7f3e 100644 --- a/src/data/resolvers/queries/engages.js +++ b/src/data/resolvers/queries/engages.js @@ -1,5 +1,6 @@ import { EngageMessages, Tags } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; +import { paginate } from './utils'; // basic count helper const count = selector => EngageMessages.find(selector).count(); @@ -101,14 +102,12 @@ const engageQueries = { /** * Engage messages list - * @param {Object} args - * @param {String} args.kind - * @param {String} args.status - * @param {String} args.tag - * @param {[String]} args.ids + * @param {Object} params - Search params * @return {Promise} filtered messages list by given parameters */ - engageMessages(root, { kind, status, tag, ids }, { user }) { + engageMessages(root, { params }, { user }) { + const { kind, status, tag, ids } = params; + if (ids) { return EngageMessages.find({ _id: { $in: ids } }); } @@ -130,7 +129,7 @@ const engageQueries = { query = { ...query, ...tagQueryBuilder(tag) }; } - return EngageMessages.find(query); + return paginate(EngageMessages.find(query), params); }, /** diff --git a/src/data/resolvers/queries/forms.js b/src/data/resolvers/queries/forms.js index 09ceb410c..9c1207e4b 100644 --- a/src/data/resolvers/queries/forms.js +++ b/src/data/resolvers/queries/forms.js @@ -1,22 +1,16 @@ import { Forms } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; +import { paginate } from './utils'; const formQueries = { /** * Forms list - * @param {Object} args - * @param {Integer} args.limit + * @param {Object} params - Search params * @return {Promise} sorted forms list */ - forms(root, { limit }) { - const forms = Forms.find({}); - const sort = { name: 1 }; - - if (limit) { - return forms.sort(sort).limit(limit); - } - - return forms.sort(sort); + forms(root, { params }) { + const forms = paginate(Forms.find({}), params); + return forms.sort({ name: 1 }); }, /** diff --git a/src/data/resolvers/queries/integrations.js b/src/data/resolvers/queries/integrations.js index 02feca8fb..edd96245e 100644 --- a/src/data/resolvers/queries/integrations.js +++ b/src/data/resolvers/queries/integrations.js @@ -1,30 +1,23 @@ import { Integrations } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; +import { paginate } from './utils'; const integrationQueries = { /** * Integrations list - * @param {Object} object - * @param {Object} object2 - Apollo input data - * @param {Integer} object2.limit - * @param {String} object2.kind + * @param {Object} params - Search params * @return {Promise} filterd and sorted integrations list */ - integrations(root, { limit, kind }) { + integrations(root, { params }) { const query = {}; - const sort = { createdAt: -1 }; - if (kind) { - query.kind = kind; + if (params.kind) { + query.kind = params.kind; } - const integrations = Integrations.find(query); - - if (limit) { - return integrations.sort(sort).limit(limit); - } + const integrations = paginate(Integrations.find(query), params); - return integrations.sort(sort); + return integrations.sort({ createdAt: -1 }); }, /** diff --git a/src/data/resolvers/queries/knowledgeBase.js b/src/data/resolvers/queries/knowledgeBase.js index 374772f6e..f43f33fdf 100644 --- a/src/data/resolvers/queries/knowledgeBase.js +++ b/src/data/resolvers/queries/knowledgeBase.js @@ -5,23 +5,17 @@ import { } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; +import { paginate } from './utils'; const knowledgeBaseQueries = { /** * Article list - * @param {Object} args - * @param {Integer} args.limit + * @param {Object} params - Search params * @return {Promise} sorted article list */ - knowledgeBaseArticles(root, { limit }) { - const articles = KnowledgeBaseArticles.find({}); - const sort = { createdDate: -1 }; - - if (limit) { - return articles.sort(sort).limit(limit); - } - - return articles.sort(sort); + knowledgeBaseArticles(root, { params }) { + const articles = paginate(KnowledgeBaseArticles.find({}), params); + return articles.sort({ createdDate: -1 }); }, /** @@ -44,19 +38,12 @@ const knowledgeBaseQueries = { /** * Category list - * @param {Object} args - * @param {Integer} args.limit + * @param {Object} params - Search params * @return {Promise} sorted category list */ - knowledgeBaseCategories(root, { limit }) { - const categories = KnowledgeBaseCategories.find({}); - const sort = { createdDate: -1 }; - - if (limit) { - return categories.sort(sort).limit(limit); - } - - return categories.sort(sort); + knowledgeBaseCategories(root, { params }) { + const categories = paginate(KnowledgeBaseCategories.find({}), params); + return categories.sort({ createdDate: -1 }); }, /** @@ -81,19 +68,12 @@ const knowledgeBaseQueries = { /** * Topic list - * @param {Object} args - * @param {Integer} args.limit + * @param {Object} params - Search params * @return {Promise} sorted topic list */ - knowledgeBaseTopics(root, { limit }) { - const topics = KnowledgeBaseTopics.find({}); - const sort = { createdDate: -1 }; - - if (limit) { - return topics.sort(sort).limit(limit); - } - - return topics.sort(sort); + knowledgeBaseTopics(root, { params }) { + const topics = paginate(KnowledgeBaseTopics.find({}), params); + return topics.sort({ createdDate: -1 }); }, /** diff --git a/src/data/resolvers/queries/responseTemplates.js b/src/data/resolvers/queries/responseTemplates.js index 6b7397eaa..2ba9f9688 100644 --- a/src/data/resolvers/queries/responseTemplates.js +++ b/src/data/resolvers/queries/responseTemplates.js @@ -1,21 +1,15 @@ import { ResponseTemplates } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; +import { paginate } from './utils'; const responseTemplateQueries = { /** * Response templates list - * @param {Object} args - * @param {Integer} args.limit + * @param {Object} args - Search params * @return {Promise} response template objects */ - responseTemplates(root, { limit }) { - const responseTemplate = ResponseTemplates.find({}); - - if (limit) { - return responseTemplate.limit(limit); - } - - return responseTemplate; + responseTemplates(root, { params }) { + return paginate(ResponseTemplates.find({}), params); }, /** diff --git a/src/data/resolvers/queries/users.js b/src/data/resolvers/queries/users.js index a4c10c1ce..e2d8e7a67 100644 --- a/src/data/resolvers/queries/users.js +++ b/src/data/resolvers/queries/users.js @@ -1,25 +1,18 @@ import { Users } from '../../../db/models'; - import { moduleRequireLogin } from '../../permissions'; +import { paginate } from './utils'; const userQueries = { /** * Users list - * @param {Object} args - * @param {Integer} args.limit + * @param {Object} args - Search params * @param {Object} object3 - Graphql middleware data * @param {Object} object3.user - User making this request * @return {Promise} sorted and filtered users objects */ - users(root, { limit }) { - const users = Users.find({}); - const sort = { username: 1 }; - - if (limit) { - return users.limit(limit).sort(sort); - } - - return users.sort(sort); + users(root, { params }) { + const users = paginate(Users.find({}), params); + return users.sort({ username: 1 }); }, /** diff --git a/src/data/resolvers/queries/utils.js b/src/data/resolvers/queries/utils.js new file mode 100644 index 000000000..566b3bf69 --- /dev/null +++ b/src/data/resolvers/queries/utils.js @@ -0,0 +1,8 @@ +/* eslint-disable no-underscore-dangle */ + +export const paginate = (collection, { page, perPage }) => { + const _page = Number(page || '1'); + const _limit = Number(perPage || '20'); + + return collection.limit(_limit).skip((_page - 1) * _limit); +}; diff --git a/src/data/schema/brand.js b/src/data/schema/brand.js index 16953c8f1..bafd7f83b 100644 --- a/src/data/schema/brand.js +++ b/src/data/schema/brand.js @@ -11,7 +11,7 @@ export const types = ` `; export const queries = ` - brands(limit: Int): [Brand] + brands(params: JSON): [Brand] brandDetail(_id: String!): Brand brandsTotalCount: Int `; diff --git a/src/data/schema/channel.js b/src/data/schema/channel.js index dde75269d..21ae6b3bc 100644 --- a/src/data/schema/channel.js +++ b/src/data/schema/channel.js @@ -13,7 +13,7 @@ export const types = ` `; export const queries = ` - channels(limit: Int, memberIds: [String]): [Channel] + channels(params: JSON): [Channel] channelsTotalCount: Int `; diff --git a/src/data/schema/emailTemplate.js b/src/data/schema/emailTemplate.js index 206a116ee..745df3f5c 100644 --- a/src/data/schema/emailTemplate.js +++ b/src/data/schema/emailTemplate.js @@ -7,7 +7,7 @@ export const types = ` `; export const queries = ` - emailTemplates(limit: Int): [EmailTemplate] + emailTemplates(params: JSON): [EmailTemplate] emailTemplatesTotalCount: Int `; diff --git a/src/data/schema/engage.js b/src/data/schema/engage.js index dc0b46825..780072210 100644 --- a/src/data/schema/engage.js +++ b/src/data/schema/engage.js @@ -46,7 +46,7 @@ export const types = ` `; export const queries = ` - engageMessages(kind: String, status: String, tag: String, ids: [String]): [EngageMessage] + engageMessages(params: JSON): [EngageMessage] engageMessageDetail(_id: String): EngageMessage engageMessageCounts(name: String!, kind: String, status: String): JSON engageMessagesTotalCount: Int diff --git a/src/data/schema/form.js b/src/data/schema/form.js index 593efea37..c857f7fe8 100644 --- a/src/data/schema/form.js +++ b/src/data/schema/form.js @@ -17,7 +17,7 @@ export const mutations = ` `; export const queries = ` - forms(limit: Int): [Form] + forms(params: JSON): [Form] formDetail(_id: String!): Form formsTotalCount: Int `; diff --git a/src/data/schema/integration.js b/src/data/schema/integration.js index 9a8812f6a..6a0a2f7e3 100644 --- a/src/data/schema/integration.js +++ b/src/data/schema/integration.js @@ -57,7 +57,7 @@ export const types = ` `; export const queries = ` - integrations(limit: Int, kind: String): [Integration] + integrations(params: JSON): [Integration] integrationDetail(_id: String!): Integration integrationsTotalCount(kind: String): Int `; diff --git a/src/data/schema/knowledgeBase.js b/src/data/schema/knowledgeBase.js index 70e365342..314c8f8a9 100644 --- a/src/data/schema/knowledgeBase.js +++ b/src/data/schema/knowledgeBase.js @@ -58,15 +58,15 @@ export const types = ` `; export const queries = ` - knowledgeBaseTopics(limit: Int): [KnowledgeBaseTopic] + knowledgeBaseTopics(params: JSON): [KnowledgeBaseTopic] knowledgeBaseTopicDetail(_id: String!): KnowledgeBaseTopic knowledgeBaseTopicsTotalCount: Int - knowledgeBaseCategories(limit: Int): [KnowledgeBaseCategory] + knowledgeBaseCategories(params: JSON): [KnowledgeBaseCategory] knowledgeBaseCategoryDetail(_id: String!): KnowledgeBaseCategory knowledgeBaseCategoriesTotalCount: Int - knowledgeBaseArticles(limit: Int): [KnowledgeBaseArticle] + knowledgeBaseArticles(params: JSON): [KnowledgeBaseArticle] knowledgeBaseArticleDetail(_id: String!): KnowledgeBaseArticle knowledgeBaseArticlesTotalCount: Int `; diff --git a/src/data/schema/responseTemplate.js b/src/data/schema/responseTemplate.js index eb16ce7d0..0929ce121 100644 --- a/src/data/schema/responseTemplate.js +++ b/src/data/schema/responseTemplate.js @@ -11,7 +11,7 @@ export const types = ` `; export const queries = ` - responseTemplates(limit: Int): [ResponseTemplate] + responseTemplates(params: JSON): [ResponseTemplate] responseTemplatesTotalCount: Int `; diff --git a/src/data/schema/user.js b/src/data/schema/user.js index b94b40e7a..786a8e85b 100644 --- a/src/data/schema/user.js +++ b/src/data/schema/user.js @@ -28,7 +28,7 @@ export const types = ` `; export const queries = ` - users(limit: Int): [User] + users(params: JSON): [User] userDetail(_id: String): User usersTotalCount: Int currentUser: User From 9b2c44ff69ab53564cbbed33d5fb9752de170e5e Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 6 Nov 2017 18:52:46 +0800 Subject: [PATCH 204/318] Add default values to list params --- src/data/resolvers/queries/channels.js | 2 +- src/data/resolvers/queries/emailTemplates.js | 2 +- src/data/resolvers/queries/engages.js | 2 +- src/data/resolvers/queries/forms.js | 2 +- src/data/resolvers/queries/integrations.js | 2 +- src/data/resolvers/queries/knowledgeBase.js | 6 +++--- src/data/resolvers/queries/responseTemplates.js | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/data/resolvers/queries/channels.js b/src/data/resolvers/queries/channels.js index 2777cde55..031a8879f 100644 --- a/src/data/resolvers/queries/channels.js +++ b/src/data/resolvers/queries/channels.js @@ -8,7 +8,7 @@ const channelQueries = { * @param {Object} args - Search params * @return {Promise} filtered channels list by given parameters */ - channels(root, { params }) { + channels(root, { params = {} }) { const query = {}; const sort = { createdAt: -1 }; diff --git a/src/data/resolvers/queries/emailTemplates.js b/src/data/resolvers/queries/emailTemplates.js index b240ef146..7a63c30d8 100644 --- a/src/data/resolvers/queries/emailTemplates.js +++ b/src/data/resolvers/queries/emailTemplates.js @@ -8,7 +8,7 @@ const emailTemplateQueries = { * @param {Object} args - Search params * @return {Promise} email template objects */ - emailTemplates(root, { params }) { + emailTemplates(root, { params = {} }) { return paginate(EmailTemplates.find({}), params); }, diff --git a/src/data/resolvers/queries/engages.js b/src/data/resolvers/queries/engages.js index 01ccd7f3e..216a64a3b 100644 --- a/src/data/resolvers/queries/engages.js +++ b/src/data/resolvers/queries/engages.js @@ -105,7 +105,7 @@ const engageQueries = { * @param {Object} params - Search params * @return {Promise} filtered messages list by given parameters */ - engageMessages(root, { params }, { user }) { + engageMessages(root, { params = {} }, { user }) { const { kind, status, tag, ids } = params; if (ids) { diff --git a/src/data/resolvers/queries/forms.js b/src/data/resolvers/queries/forms.js index 9c1207e4b..ed4669a73 100644 --- a/src/data/resolvers/queries/forms.js +++ b/src/data/resolvers/queries/forms.js @@ -8,7 +8,7 @@ const formQueries = { * @param {Object} params - Search params * @return {Promise} sorted forms list */ - forms(root, { params }) { + forms(root, { params = {} }) { const forms = paginate(Forms.find({}), params); return forms.sort({ name: 1 }); }, diff --git a/src/data/resolvers/queries/integrations.js b/src/data/resolvers/queries/integrations.js index edd96245e..c1dd32429 100644 --- a/src/data/resolvers/queries/integrations.js +++ b/src/data/resolvers/queries/integrations.js @@ -8,7 +8,7 @@ const integrationQueries = { * @param {Object} params - Search params * @return {Promise} filterd and sorted integrations list */ - integrations(root, { params }) { + integrations(root, { params = {} }) { const query = {}; if (params.kind) { diff --git a/src/data/resolvers/queries/knowledgeBase.js b/src/data/resolvers/queries/knowledgeBase.js index f43f33fdf..0962e091a 100644 --- a/src/data/resolvers/queries/knowledgeBase.js +++ b/src/data/resolvers/queries/knowledgeBase.js @@ -13,7 +13,7 @@ const knowledgeBaseQueries = { * @param {Object} params - Search params * @return {Promise} sorted article list */ - knowledgeBaseArticles(root, { params }) { + knowledgeBaseArticles(root, { params = {} }) { const articles = paginate(KnowledgeBaseArticles.find({}), params); return articles.sort({ createdDate: -1 }); }, @@ -41,7 +41,7 @@ const knowledgeBaseQueries = { * @param {Object} params - Search params * @return {Promise} sorted category list */ - knowledgeBaseCategories(root, { params }) { + knowledgeBaseCategories(root, { params = {} }) { const categories = paginate(KnowledgeBaseCategories.find({}), params); return categories.sort({ createdDate: -1 }); }, @@ -71,7 +71,7 @@ const knowledgeBaseQueries = { * @param {Object} params - Search params * @return {Promise} sorted topic list */ - knowledgeBaseTopics(root, { params }) { + knowledgeBaseTopics(root, { params = {} }) { const topics = paginate(KnowledgeBaseTopics.find({}), params); return topics.sort({ createdDate: -1 }); }, diff --git a/src/data/resolvers/queries/responseTemplates.js b/src/data/resolvers/queries/responseTemplates.js index 2ba9f9688..f516c144f 100644 --- a/src/data/resolvers/queries/responseTemplates.js +++ b/src/data/resolvers/queries/responseTemplates.js @@ -8,7 +8,7 @@ const responseTemplateQueries = { * @param {Object} args - Search params * @return {Promise} response template objects */ - responseTemplates(root, { params }) { + responseTemplates(root, { params = {} }) { return paginate(ResponseTemplates.find({}), params); }, From ded42106b18fd38b82dda2f8c454eeaa8ccc0249 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 6 Nov 2017 22:14:55 +0800 Subject: [PATCH 205/318] Add customer pagination --- src/data/resolvers/queries/customers.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index b35d05b48..6b52eb070 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -3,6 +3,7 @@ import { Brands, Tags, Integrations, Customers, Segments } from '../../../db/mod import { TAG_TYPES, INTEGRATION_KIND_CHOICES, SEGMENT_CONTENT_TYPES } from '../../constants'; import QueryBuilder from './segmentQueryBuilder.js'; import { moduleRequireLogin } from '../../permissions'; +import { paginate } from './utils'; const listQuery = async params => { const selector = {}; @@ -51,15 +52,16 @@ const customerQueries = { * @return {Promise} filtered customers list by given parameters */ async customers(root, { params }) { + const sort = { 'messengerData.lastSeenAt': -1 }; + if (params.ids) { - return Customers.find({ _id: { $in: params.ids } }).sort({ 'messengerData.lastSeenAt': -1 }); + const selector = { _id: { $in: params.ids } }; + return paginate(Customers.find(selector), params).sort(sort); } const selector = await listQuery(params); - return Customers.find(selector) - .sort({ 'messengerData.lastSeenAt': -1 }) - .limit(params.limit || 0); + return paginate(Customers.find(selector), params).sort(sort); }, /** From 075045ff9d0cde83ce44602d9fca24ee7fc1a33b Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 6 Nov 2017 22:32:00 +0800 Subject: [PATCH 206/318] Add company pagination --- src/data/resolvers/queries/companies.js | 13 +++---------- src/data/resolvers/queries/customers.js | 8 -------- src/data/resolvers/queries/users.js | 2 +- src/data/schema/company.js | 1 - src/data/schema/customer.js | 1 - 5 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/data/resolvers/queries/companies.js b/src/data/resolvers/queries/companies.js index a171a9c32..23cae9fd6 100644 --- a/src/data/resolvers/queries/companies.js +++ b/src/data/resolvers/queries/companies.js @@ -2,6 +2,7 @@ import { Companies, Segments } from '../../../db/models'; import QueryBuilder from './segmentQueryBuilder.js'; import { SEGMENT_CONTENT_TYPES } from '../../constants'; import { moduleRequireLogin } from '../../permissions'; +import { paginate } from './utils'; const listQuery = async params => { const selector = {}; @@ -25,12 +26,12 @@ const companyQueries = { */ async companies(root, { params }) { if (params.ids) { - return Companies.find({ _id: { $in: params.ids } }); + return paginate(Companies.find({ _id: { $in: params.ids } }), params); } const selector = await listQuery(params); - return Companies.find(selector).limit(params.limit || 0); + return paginate(Companies.find(selector), params); }, /** @@ -72,14 +73,6 @@ const companyQueries = { companyDetail(root, { _id }) { return Companies.findOne({ _id }); }, - - /** - * Get all companies count. We will use it in pager - * @return {Promise} total count - */ - companiesTotalCount() { - return Companies.find({}).count(); - }, }; moduleRequireLogin(companyQueries); diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index 6b52eb070..ecc06b13f 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -147,14 +147,6 @@ const customerQueries = { customerDetail(root, { _id }) { return Customers.findOne({ _id }); }, - - /** - * Get all customers count. We will use it in pager - * @return {Promise} total count - */ - customersTotalCount() { - return Customers.find({}).count(); - }, }; moduleRequireLogin(customerQueries); diff --git a/src/data/resolvers/queries/users.js b/src/data/resolvers/queries/users.js index e2d8e7a67..51bcce8e1 100644 --- a/src/data/resolvers/queries/users.js +++ b/src/data/resolvers/queries/users.js @@ -10,7 +10,7 @@ const userQueries = { * @param {Object} object3.user - User making this request * @return {Promise} sorted and filtered users objects */ - users(root, { params }) { + users(root, { params = {} }) { const users = paginate(Users.find({}), params); return users.sort({ username: 1 }); }, diff --git a/src/data/schema/company.js b/src/data/schema/company.js index c9704892b..794ec2621 100644 --- a/src/data/schema/company.js +++ b/src/data/schema/company.js @@ -27,7 +27,6 @@ export const queries = ` companies(params: CompanyListParams): [Company] companyCounts(params: CompanyListParams): JSON companyDetail(_id: String!): Company - companiesTotalCount: Int `; const commonFields = ` diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index d39355954..a43210dda 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -38,7 +38,6 @@ export const queries = ` customerCounts(params: CustomerListParams): JSON customerDetail(_id: String!): Customer customerListForSegmentPreview(segment: JSON, limit: Int): [Customer] - customersTotalCount: Int `; const fields = ` From 5eb36d67a401d9b3055532807201e7a8b25f86b5 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 6 Nov 2017 23:24:24 +0800 Subject: [PATCH 207/318] test fixes --- src/__tests__/companyQueries.test.js | 3 +-- src/__tests__/customerQueries.test.js | 3 +-- src/data/resolvers/queries/utils.js | 4 +++- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/__tests__/companyQueries.test.js b/src/__tests__/companyQueries.test.js index 50c4ab7ea..cf5a5549d 100644 --- a/src/__tests__/companyQueries.test.js +++ b/src/__tests__/companyQueries.test.js @@ -4,7 +4,7 @@ import companyQueries from '../data/resolvers/queries/companies'; describe('companyQueries', () => { test(`test if Error('Login required') exception is working as intended`, async () => { - expect.assertions(4); + expect.assertions(3); const expectError = async func => { try { @@ -17,6 +17,5 @@ describe('companyQueries', () => { expectError(companyQueries.companies); expectError(companyQueries.companyCounts); expectError(companyQueries.companyDetail); - expectError(companyQueries.companiesTotalCount); }); }); diff --git a/src/__tests__/customerQueries.test.js b/src/__tests__/customerQueries.test.js index 4d45d2741..46bcbe3c0 100644 --- a/src/__tests__/customerQueries.test.js +++ b/src/__tests__/customerQueries.test.js @@ -4,7 +4,7 @@ import customerQueries from '../data/resolvers/queries/customers'; describe('customerQueries', () => { test(`test if Error('Login required') exception is working as intended`, async () => { - expect.assertions(5); + expect.assertions(4); const expectError = async func => { try { @@ -18,6 +18,5 @@ describe('customerQueries', () => { expectError(customerQueries.customerCounts); expectError(customerQueries.customerListForSegmentPreview); expectError(customerQueries.customerDetail); - expectError(customerQueries.customersTotalCount); }); }); diff --git a/src/data/resolvers/queries/utils.js b/src/data/resolvers/queries/utils.js index 566b3bf69..1a3320192 100644 --- a/src/data/resolvers/queries/utils.js +++ b/src/data/resolvers/queries/utils.js @@ -1,6 +1,8 @@ /* eslint-disable no-underscore-dangle */ -export const paginate = (collection, { page, perPage }) => { +export const paginate = (collection, params) => { + const { page, perPage } = params || {}; + const _page = Number(page || '1'); const _limit = Number(perPage || '20'); From 577fd154a3c2352e63e58afa2e0b6e6527804b22 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Tue, 7 Nov 2017 02:33:00 +0800 Subject: [PATCH 208/318] Add activityLogCronJob with its tests, ActivityLogs.createSegmentLog, segmentQueryBuilder moved to src/ --- src/__tests__/activityLogCronJob.test.js | 51 +++++++++++++++++ src/cronJobs/activityLogs.js | 39 +++++++++++++ src/cronJobs/index.js | 2 + src/data/resolvers/mutations/engageUtils.js | 2 +- src/data/resolvers/queries/companies.js | 6 +- src/data/resolvers/queries/customers.js | 2 +- src/db/factories.js | 1 + src/db/models/ActivityLogs.js | 31 +++++++++++ .../queries => }/segmentQueryBuilder.js | 55 ++++++++++--------- 9 files changed, 158 insertions(+), 31 deletions(-) create mode 100644 src/__tests__/activityLogCronJob.test.js create mode 100644 src/cronJobs/activityLogs.js rename src/{data/resolvers/queries => }/segmentQueryBuilder.js (68%) diff --git a/src/__tests__/activityLogCronJob.test.js b/src/__tests__/activityLogCronJob.test.js new file mode 100644 index 000000000..302c789e3 --- /dev/null +++ b/src/__tests__/activityLogCronJob.test.js @@ -0,0 +1,51 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import cronJobs from '../cronJobs'; +import { CUSTOMER_CONTENT_TYPES } from '../data/constants'; +import { customerFactory, segmentFactory } from '../db/factories'; +import ActivityLogs, { + ACTIVITY_TYPES, + ACTIVITY_ACTIONS, + ACTION_PERFORMER_TYPES, +} from '../db/models/ActivityLogs'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('test activityLogsCronJob', () => { + test('1', async () => { + const nameEqualsConditions = [ + { + type: 'string', + dateUnit: 'days', + value: 'John Smith', + operator: 'e', + field: 'name', + }, + ]; + + const customer = await customerFactory({ name: 'John Smith' }); + const segment = await segmentFactory({ + contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + conditions: nameEqualsConditions, + }); + + await cronJobs.createActivityLogsFromSegments(); + + const aLog = await ActivityLogs.findOne(); + expect(aLog.activity.toObject()).toEqual({ + type: ACTIVITY_TYPES.SEGMENT, + action: ACTIVITY_ACTIONS.CREATE, + id: segment._id, + }); + expect(aLog.customer.toObject()).toEqual({ + type: CUSTOMER_CONTENT_TYPES.CUSTOMER, + id: customer._id, + }); + expect(aLog.performedBy.toObject()).toEqual({ + type: ACTION_PERFORMER_TYPES.SYSTEM, + }); + }); +}); diff --git a/src/cronJobs/activityLogs.js b/src/cronJobs/activityLogs.js new file mode 100644 index 000000000..e7b1fea09 --- /dev/null +++ b/src/cronJobs/activityLogs.js @@ -0,0 +1,39 @@ +import schedule from 'node-schedule'; +import { Segments, Customers, ActivityLogs } from '../db/models'; +import QueryBuilder from '../segmentQueryBuilder'; + +/** +* Send conversation messages to customer +*/ +export const createActivityLogsFromSegments = async () => { + const segments = await Segments.find({}); + + for (let segment of segments) { + const selector = await QueryBuilder.segments(segment); + const customers = await Customers.find(selector); + + for (let customer of customers) { + await ActivityLogs.createSegmentLog(segment, customer); + } + } +}; + +/** +* * * * * * * +* ┬ ┬ ┬ ┬ ┬ ┬ +* │ │ │ │ │ | +* │ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) +* │ │ │ │ └───── month (1 - 12) +* │ │ │ └────────── day of month (1 - 31) +* │ │ └─────────────── hour (0 - 23) +* │ └──────────────────── minute (0 - 59) +* └───────────────────────── second (0 - 59, OPTIONAL) +*/ +// every 10 minutes +schedule.scheduleJob('*/10 * * * *', function() { + createActivityLogsFromSegments(); +}); + +export default { + createActivityLogsFromSegments, +}; diff --git a/src/cronJobs/index.js b/src/cronJobs/index.js index 7c6bbf320..398b4caba 100644 --- a/src/cronJobs/index.js +++ b/src/cronJobs/index.js @@ -1,5 +1,7 @@ import conversations from './conversations'; +import activityLogs from './activityLogs'; export default { ...conversations, + ...activityLogs, }; diff --git a/src/data/resolvers/mutations/engageUtils.js b/src/data/resolvers/mutations/engageUtils.js index 0c3cb77f9..31444b8e6 100644 --- a/src/data/resolvers/mutations/engageUtils.js +++ b/src/data/resolvers/mutations/engageUtils.js @@ -15,7 +15,7 @@ import { INTEGRATION_KIND_CHOICES, } from '../../constants'; import Random from 'meteor-random'; -import QueryBuilder from '../queries/segmentQueryBuilder'; +import QueryBuilder from '../../../segmentQueryBuilder'; import { createTransporter } from '../../utils'; /** diff --git a/src/data/resolvers/queries/companies.js b/src/data/resolvers/queries/companies.js index a171a9c32..1ba20b493 100644 --- a/src/data/resolvers/queries/companies.js +++ b/src/data/resolvers/queries/companies.js @@ -1,6 +1,6 @@ import { Companies, Segments } from '../../../db/models'; -import QueryBuilder from './segmentQueryBuilder.js'; -import { SEGMENT_CONTENT_TYPES } from '../../constants'; +import QueryBuilder from '../../../segmentQueryBuilder'; +import { CUSTOMER_CONTENT_TYPES } from '../../constants'; import { moduleRequireLogin } from '../../permissions'; const listQuery = async params => { @@ -53,7 +53,7 @@ const companyQueries = { // Count companies by segments const segments = await Segments.find({ - contentType: SEGMENT_CONTENT_TYPES.COMPANY, + contentType: CUSTOMER_CONTENT_TYPES.COMPANY, }); for (let s of segments) { diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index 5e0b499a8..fbd122776 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import { Brands, Tags, Integrations, Customers, Segments } from '../../../db/models'; import { TAG_TYPES, INTEGRATION_KIND_CHOICES, CUSTOMER_CONTENT_TYPES } from '../../constants'; -import QueryBuilder from './segmentQueryBuilder.js'; +import QueryBuilder from '../../../segmentQueryBuilder.js'; import { moduleRequireLogin } from '../../permissions'; const listQuery = async params => { diff --git a/src/db/factories.js b/src/db/factories.js index 0b0c4c396..2988368e5 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -128,6 +128,7 @@ export const segmentFactory = (params = {}) => { ]; const segment = new Segments({ + contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER || params.contentType, name: faker.random.word(), description: params.description || faker.random.word(), subOf: params.subOf || 'DFSAFDFDSFDSF', diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index b729fa92e..c9e0df635 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -186,6 +186,37 @@ class ActivityLog { customer, }); } + + static async createSegmentLog(segment, customer) { + if (!customer) { + throw new Error('customer must be supplied'); + } + + const foundSegment = await this.findOne({ + 'activity.type': ACTIVITY_TYPES.SEGMENT, + 'activity.action': ACTIVITY_ACTIONS.CREATE, + 'activity.id': segment._id, + 'customer.type': segment.contentType, + 'customer.id': customer._id, + }); + + if (foundSegment) { + // since this type of activity log already exists, new one won't be created + return foundSegment; + } + + return this.createDoc({ + activity: { + type: ACTIVITY_TYPES.SEGMENT, + action: ACTIVITY_ACTIONS.CREATE, + id: segment._id, + }, + customer: { + type: segment.contentType, + id: customer._id, + }, + }); + } } ActivityLogSchema.loadClass(ActivityLog); diff --git a/src/data/resolvers/queries/segmentQueryBuilder.js b/src/segmentQueryBuilder.js similarity index 68% rename from src/data/resolvers/queries/segmentQueryBuilder.js rename to src/segmentQueryBuilder.js index 8533b9ee1..138a78757 100644 --- a/src/data/resolvers/queries/segmentQueryBuilder.js +++ b/src/segmentQueryBuilder.js @@ -1,38 +1,41 @@ import moment from 'moment'; -export default { - segments(segment, headSegment) { - const query = { $and: [] }; +export const segments = (segment, headSegment) => { + const query = { $and: [] }; + + const childQuery = { + [segment.connector === 'any' ? '$or' : '$and']: segment.conditions.map(condition => ({ + [condition.field]: convertConditionToQuery(condition), + })), + }; + + if (segment.conditions.length) { + query.$and.push(childQuery); + } + + // Fetching parent segment + const embeddedParentSegment = + typeof segment.getParentSegment === 'function' ? segment.getParentSegment() : null; + const parentSegment = headSegment || embeddedParentSegment; - const childQuery = { - [segment.connector === 'any' ? '$or' : '$and']: segment.conditions.map(condition => ({ + if (parentSegment) { + const parentQuery = { + [parentSegment.connector === 'any' + ? '$or' + : '$and']: parentSegment.conditions.map(condition => ({ [condition.field]: convertConditionToQuery(condition), })), }; - if (segment.conditions.length) { - query.$and.push(childQuery); + if (parentSegment.conditions.length) { + query.$and.push(parentQuery); } + } - // Fetching parent segment - const embeddedParentSegment = - typeof segment.getParentSegment === 'function' ? segment.getParentSegment() : null; - const parentSegment = headSegment || embeddedParentSegment; - - if (parentSegment) { - const parentQuery = { - [parentSegment.connector === 'any' - ? '$or' - : '$and']: parentSegment.conditions.map(condition => ({ - [condition.field]: convertConditionToQuery(condition), - })), - }; - if (parentSegment.conditions.length) { - query.$and.push(parentQuery); - } - } + return query.$and.length ? query : {}; +}; - return query.$and.length ? query : {}; - }, +export default { + segments, }; function convertConditionToQuery(condition) { From 0a93faabb9f7848c27c8b9411dab1acf238db8c2 Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 7 Nov 2017 11:33:52 +0800 Subject: [PATCH 209/318] Tag fix --- src/data/schema/tag.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/schema/tag.js b/src/data/schema/tag.js index 05919b69e..2017f169f 100644 --- a/src/data/schema/tag.js +++ b/src/data/schema/tag.js @@ -16,6 +16,6 @@ export const queries = ` export const mutations = ` tagsAdd(name: String!, type: String!, colorCode: String): Tag tagsEdit(_id: String!, name: String!, type: String!, colorCode: String): Tag - tagsRemove(ids: [String!]!): Tag + tagsRemove(ids: [String!]!): String tagsTag(type: String!, targetIds: [String!]!, tagIds: [String!]!): String `; From 46e146e645f8bfa75a1d6b33a7cf8f14a71809ae Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 8 Nov 2017 10:24:05 +0800 Subject: [PATCH 210/318] Add conversationsGetCurrent query --- src/data/resolvers/queries/conversations.js | 14 ++++++++++++++ src/data/schema/conversation.js | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/data/resolvers/queries/conversations.js b/src/data/resolvers/queries/conversations.js index 97c61ce08..7fabd7ed1 100644 --- a/src/data/resolvers/queries/conversations.js +++ b/src/data/resolvers/queries/conversations.js @@ -166,6 +166,20 @@ const conversationQueries = { return Conversations.find(qb.mainQuery()).count(); }, + + /** + * Get conversation by given id or return last conversation + * @return {Promise} - Conversation object + */ + async conversationsGetCurrent(root, { _id }) { + let conversation = await Conversations.findOne({ _id }); + + if (!conversation) { + conversation = Conversations.findOne({}).sort({ createdAt: -1 }); + } + + return conversation; + }, }; moduleRequireLogin(conversationQueries); diff --git a/src/data/schema/conversation.js b/src/data/schema/conversation.js index 865f5f0ae..6ae3427c1 100644 --- a/src/data/schema/conversation.js +++ b/src/data/schema/conversation.js @@ -82,10 +82,11 @@ export const types = ` `; export const queries = ` - conversations(params: ConversationListParams): [Conversation] + conversations(params: ConversationListParams!): [Conversation] conversationCounts(params: ConversationListParams): JSON conversationDetail(_id: String!): Conversation conversationsTotalCount(params: ConversationListParams): Int + conversationsGetCurrent(_id: String): Conversation `; export const mutations = ` From 4f3ef60bb018c3982ca6c26a59d53e00439c38fc Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 8 Nov 2017 10:51:41 +0800 Subject: [PATCH 211/318] Excluded currentUser from requireLogin --- src/data/resolvers/queries/users.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/data/resolvers/queries/users.js b/src/data/resolvers/queries/users.js index 51bcce8e1..b33164efd 100644 --- a/src/data/resolvers/queries/users.js +++ b/src/data/resolvers/queries/users.js @@ -1,5 +1,5 @@ import { Users } from '../../../db/models'; -import { moduleRequireLogin } from '../../permissions'; +import { requireLogin } from '../../permissions'; import { paginate } from './utils'; const userQueries = { @@ -42,10 +42,16 @@ const userQueries = { * @return {Promise} total count */ currentUser(root, args, { user }) { - return Users.findOne({ _id: user._id }); + if (user) { + return Users.findOne({ _id: user._id }); + } + + return null; }, }; -moduleRequireLogin(userQueries); +requireLogin(userQueries.users); +requireLogin(userQueries.userDetail); +requireLogin(userQueries.usersTotalCount); export default userQueries; From bd5fd5419129a064c57b30b9c8944a68fc94fa3a Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Wed, 8 Nov 2017 15:11:50 +0800 Subject: [PATCH 212/318] fixed a bug with ActivityLog.createdAt date not setting the right date, reverted convertConditionToQuery --- src/cronJobs/activityLogs.js | 20 +++++++++++++++----- src/db/models/ActivityLogs.js | 4 ++-- src/index.js | 4 +++- src/segmentQueryBuilder.js | 5 +++-- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/cronJobs/activityLogs.js b/src/cronJobs/activityLogs.js index e7b1fea09..1df64c89d 100644 --- a/src/cronJobs/activityLogs.js +++ b/src/cronJobs/activityLogs.js @@ -8,14 +8,24 @@ import QueryBuilder from '../segmentQueryBuilder'; export const createActivityLogsFromSegments = async () => { const segments = await Segments.find({}); + console.log('started running'); for (let segment of segments) { + console.log('segment._id : ', segment._id); + console.log('segment.name: ', segment.name); const selector = await QueryBuilder.segments(segment); + // console.log('segment: ', segment); + // console.log('selector: ', selector['$and'] && selector['$and'][0]['$or']); const customers = await Customers.find(selector); - for (let customer of customers) { - await ActivityLogs.createSegmentLog(segment, customer); + if (segment.contentType) { + for (let customer of customers) { + // console.log('customer: ', customer); + + await ActivityLogs.createSegmentLog(segment, customer); + } } } + console.log('stopped running (successful)'); }; /** @@ -30,9 +40,9 @@ export const createActivityLogsFromSegments = async () => { * └───────────────────────── second (0 - 59, OPTIONAL) */ // every 10 minutes -schedule.scheduleJob('*/10 * * * *', function() { - createActivityLogsFromSegments(); -}); +// schedule.scheduleJob('*/5 * * * *', function() { +// createActivityLogsFromSegments(); +// }); export default { createActivityLogsFromSegments, diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index c9e0df635..ae2701257 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -114,7 +114,7 @@ const ActivityLogSchema = mongoose.Schema({ createdAt: { type: Date, required: true, - default: Date.now(), + default: Date.now, }, }); @@ -169,7 +169,7 @@ class ActivityLog { */ static createConversationLog(conversation, user, customer) { if (user == null || (user && !user._id)) { - throw new Error(`'user' must be supplied when adding activity log for internal note`); + throw new Error(`'user' must be supplied when adding activity log for conversations`); } if (customer == null || (customer && !user._id)) { diff --git a/src/index.js b/src/index.js index 33a1fd0cb..e12bc5021 100755 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,7 @@ import { Customers } from './db/models'; import { connect } from './db/connection'; import { userMiddleware } from './auth'; import schema from './data'; -import './cronJobs'; +import cronJobs from './cronJobs'; // load environment variables dotenv.config(); @@ -85,3 +85,5 @@ if (process.env.NODE_ENV === 'development') { }), ); } + +cronJobs.createActivityLogsFromSegments(); diff --git a/src/segmentQueryBuilder.js b/src/segmentQueryBuilder.js index 138a78757..dc80e7fda 100644 --- a/src/segmentQueryBuilder.js +++ b/src/segmentQueryBuilder.js @@ -57,7 +57,6 @@ function convertConditionToQuery(condition) { switch (operator) { case 'e': - return { $regex: `^${escapeRegExp(transformedValue)}$`, $options: 'i' }; case 'et': default: return transformedValue; @@ -109,5 +108,7 @@ function convertConditionToQuery(condition) { } function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + // $& means the whole matched string + console.log('string: ', string); + return new String(string).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } From 4d55e38fdaba464606489dd553d9cc218f0f217a Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 8 Nov 2017 19:17:21 +0800 Subject: [PATCH 213/318] Add twitter integration --- src/__tests__/integrationDb.test.js | 32 ++++++ src/__tests__/integrationMutations.test.js | 81 ++++++++----- src/__tests__/userQueries.test.js | 3 +- src/data/resolvers/mutations/integrations.js | 33 ++++-- src/data/resolvers/queries/integrations.js | 9 ++ src/data/resolvers/queries/users.js | 6 +- src/data/schema/integration.js | 11 ++ src/db/models/Integrations.js | 115 ++++--------------- src/social/twitterTracker.js | 19 +-- 9 files changed, 159 insertions(+), 150 deletions(-) diff --git a/src/__tests__/integrationDb.test.js b/src/__tests__/integrationDb.test.js index 5182361e5..e2bbb5d3f 100644 --- a/src/__tests__/integrationDb.test.js +++ b/src/__tests__/integrationDb.test.js @@ -400,3 +400,35 @@ describe('save integration messenger configurations test', () => { ); }); }); + +describe('social integration test', () => { + let _brand; + + beforeEach(async () => { + _brand = await brandFactory({}); + }); + + afterEach(async () => { + await Brands.remove({}); + await Integrations.remove({}); + }); + + test('create twitter integration', async () => { + const doc = { + name: 'name', + brandId: _brand._id, + twitterData: { + id: 1, + token: 'token', + tokenSecret: 'tokenSecret', + }, + }; + + const integration = await Integrations.createTwitterIntegration(doc); + + expect(integration.name).toBe(doc.name); + expect(integration.brandId).toBe(doc.brandId); + expect(integration.kind).toBe(KIND_CHOICES.TWITTER); + expect(integration.twitterData.toJSON()).toEqual(doc.twitterData); + }); +}); diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js index c3a905600..b4d431d82 100644 --- a/src/__tests__/integrationMutations.test.js +++ b/src/__tests__/integrationMutations.test.js @@ -6,6 +6,7 @@ import { FORM_LOAD_TYPES, MESSENGER_DATA_AVAILABILITY } from '../data/constants' import { Integrations } from '../db/models'; import { ROLES } from '../data/constants'; import integrationMutations from '../data/resolvers/mutations/integrations'; +import { socUtils } from '../social/twitterTracker'; describe('mutations', () => { const _fakeBrandId = 'fakeBrandId'; @@ -16,36 +17,26 @@ describe('mutations', () => { const _adminUser = { _id: 'fakeId', role: ROLES.ADMIN }; test(`test if Error('Login required') exception is working as intended`, () => { - expect.assertions(7); + expect.assertions(9); // Login required ================== - expect(() => - integrationMutations.integrationsCreateMessengerIntegration(null, {}, {}), - ).toThrowError('Login required'); - - expect(() => - integrationMutations.integrationsEditMessengerIntegration(null, {}, {}), - ).toThrowError('Login required'); - - expect(() => integrationMutations.integrationsSaveMessengerConfigs(null, {}, {})).toThrowError( - 'Login required', - ); - - expect(() => - integrationMutations.integrationsSaveMessengerAppearanceData(null, {}, {}), - ).toThrowError('Login required'); - - expect(() => integrationMutations.integrationsCreateFormIntegration(null, {}, {})).toThrowError( - 'Login required', - ); - - expect(() => integrationMutations.integrationsEditFormIntegration(null, {}, {})).toThrowError( - 'Login required', - ); + const check = mutation => { + try { + mutation(null, {}, {}); + } catch (e) { + expect(e.message).toBe('Login required'); + } + }; - expect(() => integrationMutations.integrationsRemove(null, {}, {})).toThrowError( - 'Login required', - ); + check(integrationMutations.integrationsCreateMessengerIntegration); + check(integrationMutations.integrationsEditMessengerIntegration); + check(integrationMutations.integrationsSaveMessengerConfigs); + check(integrationMutations.integrationsSaveMessengerAppearanceData); + check(integrationMutations.integrationsCreateFormIntegration); + check(integrationMutations.integrationsEditFormIntegration); + check(integrationMutations.integrationsEditFormIntegration); + check(integrationMutations.integrationsRemove); + check(integrationMutations.integrationsCreateTwitterIntegration); }); test(`test if Error('Permission required') exception is working as intended`, async () => { @@ -217,4 +208,40 @@ describe('mutations', () => { expect(Integrations.removeIntegration).toBeCalledWith(_fakeIntegrationId); expect(Integrations.removeIntegration.mock.calls.length).toBe(1); }); + + test('create twitter integration', async () => { + Integrations.createTwitterIntegration = jest.fn(); + + const authenticateDoc = { + info: { + name: 'name', + id: 1, + }, + + tokens: { + auth: { + token: 'token', + tokenSecret: 'secret', + }, + }, + }; + + socUtils.authenticate = jest.fn(() => authenticateDoc); + + const doc = { + name: authenticateDoc.info.name, + brandId: 'brandId', + twitterData: { + id: authenticateDoc.info.id, + token: authenticateDoc.tokens.auth.token, + tokenSecret: authenticateDoc.tokens.auth.token_secret, + }, + }; + + await integrationMutations.integrationsCreateTwitterIntegration(null, doc, { + user: _adminUser, + }); + + expect(Integrations.createTwitterIntegration).toBeCalledWith(doc); + }); }); diff --git a/src/__tests__/userQueries.test.js b/src/__tests__/userQueries.test.js index 758c4fea9..f156e5c50 100644 --- a/src/__tests__/userQueries.test.js +++ b/src/__tests__/userQueries.test.js @@ -4,7 +4,7 @@ import userQueries from '../data/resolvers/queries/users'; describe('userQueries', () => { test(`test if Error('Login required') exception is working as intended`, async () => { - expect.assertions(4); + expect.assertions(3); const expectError = async func => { try { @@ -17,6 +17,5 @@ describe('userQueries', () => { expectError(userQueries.users); expectError(userQueries.userDetail); expectError(userQueries.usersTotalCount); - expectError(userQueries.currentUser); }); }); diff --git a/src/data/resolvers/mutations/integrations.js b/src/data/resolvers/mutations/integrations.js index 379201058..af52ae2bb 100644 --- a/src/data/resolvers/mutations/integrations.js +++ b/src/data/resolvers/mutations/integrations.js @@ -1,4 +1,5 @@ import { Integrations } from '../../../db/models'; +import { socUtils } from '../../../social/twitterTracker'; import { requireLogin, requireAdmin } from '../../permissions'; const integrationMutations = { @@ -8,8 +9,6 @@ const integrationMutations = { * @param {Object} doc - Integration main doc object * @param {string} doc.name - Integration name * @param {string} doc.brandId - Integration brand id - * @param {Object} object3 - The middleware data - * @param {Object} object3.user - The user making this action * @return {Promise} return Promise resolving Integration document */ integrationsCreateMessengerIntegration(root, doc) { @@ -23,8 +22,6 @@ const integrationMutations = { * @param {string} object2._id - Integration id * @param {string} object2.name - Integration name * @param {string} object2.brandId - Integration brand id - * @param {Object} object3 - The middleware data - * @param {Object} object3.user - The user making this action * @return {Promise} return Promise resolving Integration document */ integrationsEditMessengerIntegration(root, { _id, ...fields }) { @@ -40,8 +37,6 @@ const integrationMutations = { * @param {string} object2.color - MessengerUiOptions color * @param {string} object2.wallpaper - MessengerUiOptions wallpaper * @param {string} object2.logo - MessengerUiOptions logo - * @param {Object} object3 - The middleware data - * @param {Object} object3.user - The user making this action * @return {Promise} return Promise resolving Integration document */ integrationsSaveMessengerAppearanceData(root, { _id, uiOptions }) { @@ -55,8 +50,6 @@ const integrationMutations = { * @param {string} object2._id - Integration id * @param {MessengerData} object2.messengerData - MessengerData subdocument * object related to this integration - * @param {Object} object3 - The middleware data - * @param {Object} object3.user - The user making this action * @return {Promise} return Promise resolving Integration document */ integrationsSaveMessengerConfigs(root, { _id, messengerData }) { @@ -71,14 +64,33 @@ const integrationMutations = { * @param {string} doc.brandId - Integration brand id * @param {string} doc.formId - Integration form id * @param {FormData} doc.formData - Integration form data sumbdocument object - * @param {Object} object3 - The middleware data - * @param {Object} object3.user - The user making this action * @return {Promise} return Promise resolving Integration document */ integrationsCreateFormIntegration(root, doc) { return Integrations.createFormIntegration(doc); }, + /** + * Create a new twitter integration + * @param {Object} root + * @param {Object} queryParams - Url params + * @param {String} brandId - Integration brand id + * @return {Promise} return Promise resolving Integration document + */ + async integrationsCreateTwitterIntegration(root, { queryParams, brandId }) { + const data = await socUtils.authenticate(queryParams); + + return Integrations.createTwitterIntegration({ + name: data.info.name, + brandId, + twitterData: { + id: data.info.id, + token: data.tokens.auth.token, + tokenSecret: data.tokens.auth.token_secret, + }, + }); + }, + /** * Edit a form integration * @param {Object} @@ -116,6 +128,7 @@ requireLogin(integrationMutations, 'integrationsSaveMessengerAppearanceData'); requireLogin(integrationMutations, 'integrationsSaveMessengerConfigs'); requireLogin(integrationMutations, 'integrationsCreateFormIntegration'); requireLogin(integrationMutations, 'integrationsEditFormIntegration'); +requireLogin(integrationMutations, 'integrationsCreateTwitterIntegration'); requireAdmin(integrationMutations, 'integrationsRemove'); export default integrationMutations; diff --git a/src/data/resolvers/queries/integrations.js b/src/data/resolvers/queries/integrations.js index c1dd32429..cb2314ea3 100644 --- a/src/data/resolvers/queries/integrations.js +++ b/src/data/resolvers/queries/integrations.js @@ -1,4 +1,5 @@ import { Integrations } from '../../../db/models'; +import { socUtils } from '../../../social/twitterTracker'; import { moduleRequireLogin } from '../../permissions'; import { paginate } from './utils'; @@ -44,6 +45,14 @@ const integrationQueries = { return Integrations.find(query).count(); }, + + /** + * Generate twitter integration auth url using credentials in .env + * @return {Promise} - Generated url + */ + integrationGetTwitterAuthUrl() { + return socUtils.getTwitterAuthorizeUrl(); + }, }; moduleRequireLogin(integrationQueries); diff --git a/src/data/resolvers/queries/users.js b/src/data/resolvers/queries/users.js index b33164efd..725fc8192 100644 --- a/src/data/resolvers/queries/users.js +++ b/src/data/resolvers/queries/users.js @@ -50,8 +50,8 @@ const userQueries = { }, }; -requireLogin(userQueries.users); -requireLogin(userQueries.userDetail); -requireLogin(userQueries.usersTotalCount); +requireLogin(userQueries, 'users'); +requireLogin(userQueries, 'userDetail'); +requireLogin(userQueries, 'usersTotalCount'); export default userQueries; diff --git a/src/data/schema/integration.js b/src/data/schema/integration.js index 6a0a2f7e3..1f46dd3a1 100644 --- a/src/data/schema/integration.js +++ b/src/data/schema/integration.js @@ -30,6 +30,11 @@ export const types = ` redirectUrl: String } + input TwitterIntegrationAuthParams { + oauth_token: String! + oauth_verifier: String! + } + input MessengerOnlineHoursSchema { _id: String day: String @@ -60,6 +65,7 @@ export const queries = ` integrations(params: JSON): [Integration] integrationDetail(_id: String!): Integration integrationsTotalCount(kind: String): Int + integrationGetTwitterAuthUrl: String `; export const mutations = ` @@ -86,6 +92,11 @@ export const mutations = ` formId: String!, formData: IntegrationFormData!): Integration + integrationsCreateTwitterIntegration( + brandId: String!, + queryParams: TwitterIntegrationAuthParams! + ): Integration + integrationsEditFormIntegration( _id: String! name: String!, diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index 2f6c922ce..97e307257 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -123,7 +123,7 @@ class Integration { /** * Generate form integration data based on the given form data (formData) * and integration data (mainDoc) - * @param {Integration} mainDoc - Integration object without subdocuments + * @param {Integration} mainDoc - Integration doc without subdocuments * @param {FormData} formData - Integration forData subdocument * @return {Object} returns an integration object */ @@ -137,46 +137,7 @@ class Integration { /** * Create an integration, intended as a private method - * @param {Object} doc - Integration object - * @param {string} doc.kind - Integration kind - * @param {string} doc.name - Integration name - * @param {string} doc.brandId - Brand id of the related Brand - * @param {string} doc.formId - Form id (used in form integrations) - * @param {string} doc.formData.loadType - Load types for the embedded form - * @param {string} doc.formData.successAction - TODO: need more elaborate documentation - * @param {string} doc.formData.formEmail - TODO: need more elaborate documentation - * @param {string} doc.formData.userEmailTitle - TODO: need more elaborate documentation - * @param {string} doc.formData.userEmailContent - TODO: need more elaborate documentation - * @param {Array} doc.formData.adminEmails - TODO: need more elaborate documentation - * @param {string} doc.formData.adminEmailTitle - TODO: need more elaborate documentation - * @param {string} doc.formData.adminEmailContent - TODO: need more elaborate documentation - * @param {string} doc.formData.thankContent - TODO: need more elaborate documentation - * @param {string} doc.formData.redirectUrl - Form redirectUrl on submit - * TODO: need more elaborate documentation - * @param {Object} doc.messengerData - MessengerData object - * @param {Boolean} doc.messengerData.notifyCustomer - Identicates whether - * customer should be notified or not TODO: need more elaborate documentation - * @param {string} doc.messengerData.availabilityMethod - Sets messenger - * availability method as auto or manual TODO: need more elaborate documentation - * @param {Boolean} doc.messengerData.isOnline - Identicates whether messenger in online or not - * @param {Object[]} doc.messengerData.onlineHours - OnlineHours object array - * @param {string} doc.messengerData.onlineHours.day - OnlineHours day - * @param {string} doc.messengerData.onlineHours.from - OnlineHours from - * @param {string} doc.messengerData.onlineHours.to - OnlineHours to - * @param {string} doc.messengerData.timezone - Timezone - * @param {string} doc.messengerData.welcomeMessage - Message displayed on welcome - * TODO: need more elaborate documentation - * @param {string} doc.messengerData.awayMessage - Message displayed when status becomes away - * TODO: need more elaborate documentation - * @param {string} doc.messengerData.thankYouMessage - Thank you message - * TODO: need more elaborate documentation - * @param {string} doc.messengerData.uiOptions.color - Color of messenger - * @param {string} doc.messengerData.uiOptions.wallpaper - Wallpaper image for messenger - * @param {string} doc.messengerData.uiOptions.logo - Logo used in the embedded messenger - * @param {Object} doc.twitterData - Twitter data - * TODO: need more elaborate documentation - * @param {Object} doc.facebookData - Facebook data - * TODO: need more elaborate documentation + * @param {Object} doc - Integration doc * @return {Promise} returns integration document promise */ static createIntegration(doc) { @@ -185,7 +146,7 @@ class Integration { /** * Create a messenger kind integration - * @param {Object} object - Integration object + * @param {Object} object - Integration doc * @param {string} object.name - Integration name * @param {String} object.brandId - Integration brand id * @return {Promise} returns integration document promise @@ -198,12 +159,26 @@ class Integration { }); } + /** + * Create twitter integration + * @param {Object} doc - Integration doc + * @return {Promise} returns integration document promise + */ + static createTwitterIntegration({ name, brandId, twitterData }) { + return this.createIntegration({ + name, + brandId, + kind: KIND_CHOICES.TWITTER, + twitterData, + }); + } + /** * Update messenger integration document * @param {Object} object - Integration main doc object * @param {string} object.name - Integration name * @param {string} object.brandId - Integration brand id - * @return {Promise} returns Promise resolving updated Integration documetn + * @return {Promise} returns Promise resolving updated Integration document */ static async updateMessengerIntegration(_id, { name, brandId }) { await this.update({ _id }, { $set: { name, brandId } }, { runValidators: true }); @@ -213,10 +188,7 @@ class Integration { /** * Save messenger appearance data * @param {string} _id - * @param {Object} object - MessengerUiOptions object TODO: need more elaborate documentation - * @param {string} object.color - MessengerUiOptions color TODO: need more elaborate documentation - * @param {string} object.wallpaper - MessengerUiOptions wallpaper - * @param {string} object.logo - Messenger logo TODO: need more elaborate documentation + * @param {Object} object - MessengerUiOptions object * @return {Promise} returns Promise resolving updated Integration document */ static async saveMessengerAppearanceData(_id, { color, wallpaper, logo }) { @@ -232,25 +204,6 @@ class Integration { /** * Saves messenger data to integration document * @param {Object} doc.messengerData - MessengerData object - * @param {Boolean} doc.messengerData.notifyCustomer - Identicates whether - * customer should be notified or not TODO: need more elaborate documentation - * @param {string} doc.messengerData.availabilityMethod - Sets messenger - * availability method as auto or manual TODO: need more elaborate documentation - * @param {Boolean} doc.messengerData.isOnline - Identicates whether messenger in online or not - * @param {Object[]} doc.messengerData.onlineHours - OnlineHours object array - * @param {string} doc.messengerData.onlineHours.day - OnlineHours day - * @param {string} doc.messengerData.onlineHours.from - OnlineHours from - * @param {string} doc.messengerData.onlineHours.to - OnlineHours to - * @param {string} doc.messengerData.timezone - Timezone - * @param {string} doc.messengerData.welcomeMessage - Message displayed on welcome - * TODO: need more elaborate documentation - * @param {string} doc.messengerData.awayMessage - Message displayed when status becomes away - * TODO: need more elaborate documentation - * @param {string} doc.messengerData.thankYouMessage - Thank you message - * TODO: need more elaborate documentation - * @param {string} doc.messengerData.uiOptions.color - Color of messenger - * @param {string} doc.messengerData.uiOptions.wallpaper - Wallpaper image for messenger - * @param {string} doc.messengerData.uiOptions.logo - Logo used in the embedded messenger * @return {Promise} returns Promise resolving updated Integration document */ static async saveMessengerConfigs(_id, messengerData) { @@ -261,21 +214,7 @@ class Integration { /** * Create a form kind integration * @param {Object} args.formData - FormData object - * @param {string} doc.formData.loadType - Load types for the embedded form - * @param {string} doc.formData.successAction - TODO: need more elaborate documentation - * @param {string} doc.formData.formEmail - TODO: need more elaborate documentation - * @param {string} doc.formData.userEmailTitle - TODO: need more elaborate documentation - * @param {string} doc.formData.userEmailContent - TODO: need more elaborate documentation - * @param {Email[]} doc.formData.adminEmails - TODO: need more elaborate documentation - * @param {string} doc.formData.adminEmailTitle - TODO: need more elaborate documentation - * @param {string} doc.formData.adminEmailContent - TODO: need more elaborate documentation - * @param {string} doc.formData.thankContent - TODO: need more elaborate documentation - * @param {string} doc.formData.redirectUrl - Form redirectUrl on submit - * @param {string} args.mainDoc - Integration main document object - * @param {string} args.mainDoc.name - Integration name - * @param {string} args.mainDoc.brandId - Integration brand id - * @param {string} args.mainDoc.formId - Form id related to this integration - * @return {Promise} returns form integration document promise + * @return {Promise} returns form integration document promise * @throws {Exception} throws Exception if formData is notSupplied */ static createFormIntegration({ formData, ...mainDoc }) { @@ -292,20 +231,6 @@ class Integration { * Update form integration * @param {string} _id integration id * @param {Object} args.formData - FormData object - * @param {string} doc.formData.loadType - Load types for the embedded form - * @param {string} doc.formData.successAction - TODO: need more elaborate documentation - * @param {string} doc.formData.formEmail - TODO: need more elaborate documentation - * @param {string} doc.formData.userEmailTitle - TODO: need more elaborate documentation - * @param {string} doc.formData.userEmailContent - TODO: need more elaborate documentation - * @param {Email[]} doc.formData.adminEmails - TODO: need more elaborate documentation - * @param {string} doc.formData.adminEmailTitle - TODO: need more elaborate documentation - * @param {string} doc.formData.adminEmailContent - TODO: need more elaborate documentation - * @param {string} doc.formData.thankContent - TODO: need more elaborate documentation - * @param {string} doc.formData.redirectUrl - Form redirectUrl on submit - * @param {string} args.mainDoc - Integration main document object - * @param {string} args.mainDoc.name - Integration name - * @param {string} args.mainDoc.brandId - Integration brand id - * @param {string} args.mainDoc.formId - Form id related to this integration * @return {Promise} returns Promise resolving updated Integration document */ static async updateFormIntegration(_id, { formData, ...mainDoc }) { diff --git a/src/social/twitterTracker.js b/src/social/twitterTracker.js index 32e735d86..498f66af2 100644 --- a/src/social/twitterTracker.js +++ b/src/social/twitterTracker.js @@ -36,17 +36,10 @@ export const socTwitter = new soc.Twitter({ REDIRECT_URL: TWITTER_REDIRECT_URL, }); -export const authenticate = (queryParams, callback) => { - // after user clicked authenticate button - socTwitter.callback({ query: queryParams }).then(data => { - // return integration info - callback({ - name: data.info.name, - twitterData: { - id: data.info.id, - token: data.tokens.auth.token, - tokenSecret: data.tokens.auth.token_secret, - }, - }); - }); +export const authenticate = queryParams => socTwitter.callback({ query: queryParams }); + +// doing this to mock authenticate function in test +export const socUtils = { + authenticate, + getTwitterAuthorizeUrl: () => socTwitter.getAuthorizeUrl(), }; From 23720e10fb9cc96c72b6f50ab452d1cce23607b4 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 8 Nov 2017 19:39:27 +0800 Subject: [PATCH 214/318] Add twitter integration trackers --- src/data/resolvers/mutations/integrations.js | 9 +++++++-- src/index.js | 2 +- src/social/twitterTracker.js | 10 +++++++++- src/startup.js | 2 ++ 4 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 src/startup.js diff --git a/src/data/resolvers/mutations/integrations.js b/src/data/resolvers/mutations/integrations.js index af52ae2bb..460a192d9 100644 --- a/src/data/resolvers/mutations/integrations.js +++ b/src/data/resolvers/mutations/integrations.js @@ -1,5 +1,5 @@ import { Integrations } from '../../../db/models'; -import { socUtils } from '../../../social/twitterTracker'; +import { trackIntegration, socUtils } from '../../../social/twitterTracker'; import { requireLogin, requireAdmin } from '../../permissions'; const integrationMutations = { @@ -80,7 +80,7 @@ const integrationMutations = { async integrationsCreateTwitterIntegration(root, { queryParams, brandId }) { const data = await socUtils.authenticate(queryParams); - return Integrations.createTwitterIntegration({ + const integration = await Integrations.createTwitterIntegration({ name: data.info.name, brandId, twitterData: { @@ -89,6 +89,11 @@ const integrationMutations = { tokenSecret: data.tokens.auth.token_secret, }, }); + + // start tracking new twitter entries + trackIntegration(integration); + + return integration; }, /** diff --git a/src/index.js b/src/index.js index 33a1fd0cb..f1b986a61 100755 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,7 @@ import { Customers } from './db/models'; import { connect } from './db/connection'; import { userMiddleware } from './auth'; import schema from './data'; -import './cronJobs'; +import './startup'; // load environment variables dotenv.config(); diff --git a/src/social/twitterTracker.js b/src/social/twitterTracker.js index 498f66af2..5018e9534 100644 --- a/src/social/twitterTracker.js +++ b/src/social/twitterTracker.js @@ -1,7 +1,8 @@ import Twit from 'twit'; import soc from 'social-oauth-client'; - import { TwitMap, receiveTimeLineResponse, getOrCreateDirectMessageConversation } from './twitter'; +import { Integrations } from '../db/models'; +import { INTEGRATION_KIND_CHOICES } from '../data/constants'; const { TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_REDIRECT_URL } = process.env; @@ -43,3 +44,10 @@ export const socUtils = { authenticate, getTwitterAuthorizeUrl: () => socTwitter.getAuthorizeUrl(), }; + +// track all twitter integrations for the first time +Integrations.find({ kind: INTEGRATION_KIND_CHOICES.TWITTER }).then(integrations => { + for (let integration of integrations) { + trackIntegration(integration); + } +}); diff --git a/src/startup.js b/src/startup.js new file mode 100644 index 000000000..91a07c72a --- /dev/null +++ b/src/startup.js @@ -0,0 +1,2 @@ +import './cronJobs'; +import './social/twitterTracker'; From 485666e13d3f38f10de4e38615b8d85cc9c3e3d8 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 8 Nov 2017 19:46:46 +0800 Subject: [PATCH 215/318] Integration test fix --- src/__tests__/integrationMutations.test.js | 5 ++++- src/data/resolvers/mutations/integrations.js | 4 ++-- src/social/twitterTracker.js | 7 ++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js index b4d431d82..f927f2d90 100644 --- a/src/__tests__/integrationMutations.test.js +++ b/src/__tests__/integrationMutations.test.js @@ -210,7 +210,8 @@ describe('mutations', () => { }); test('create twitter integration', async () => { - Integrations.createTwitterIntegration = jest.fn(); + const integrationDoc = { _id: 'id', name: 'name' }; + Integrations.createTwitterIntegration = jest.fn(() => integrationDoc); const authenticateDoc = { info: { @@ -227,6 +228,7 @@ describe('mutations', () => { }; socUtils.authenticate = jest.fn(() => authenticateDoc); + socUtils.trackIntegration = jest.fn(); const doc = { name: authenticateDoc.info.name, @@ -243,5 +245,6 @@ describe('mutations', () => { }); expect(Integrations.createTwitterIntegration).toBeCalledWith(doc); + expect(socUtils.trackIntegration).toBeCalledWith(integrationDoc); }); }); diff --git a/src/data/resolvers/mutations/integrations.js b/src/data/resolvers/mutations/integrations.js index 460a192d9..eed86123e 100644 --- a/src/data/resolvers/mutations/integrations.js +++ b/src/data/resolvers/mutations/integrations.js @@ -1,5 +1,5 @@ import { Integrations } from '../../../db/models'; -import { trackIntegration, socUtils } from '../../../social/twitterTracker'; +import { socUtils } from '../../../social/twitterTracker'; import { requireLogin, requireAdmin } from '../../permissions'; const integrationMutations = { @@ -91,7 +91,7 @@ const integrationMutations = { }); // start tracking new twitter entries - trackIntegration(integration); + socUtils.trackIntegration(integration); return integration; }, diff --git a/src/social/twitterTracker.js b/src/social/twitterTracker.js index 5018e9534..0184bb62c 100644 --- a/src/social/twitterTracker.js +++ b/src/social/twitterTracker.js @@ -6,7 +6,7 @@ import { INTEGRATION_KIND_CHOICES } from '../data/constants'; const { TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_REDIRECT_URL } = process.env; -export const trackIntegration = integration => { +const trackIntegration = integration => { // Twit instance const twit = new Twit({ consumer_key: TWITTER_CONSUMER_KEY, @@ -31,17 +31,18 @@ export const trackIntegration = integration => { }; // twitter oauth =============== -export const socTwitter = new soc.Twitter({ +const socTwitter = new soc.Twitter({ CONSUMER_KEY: TWITTER_CONSUMER_KEY, CONSUMER_SECRET: TWITTER_CONSUMER_SECRET, REDIRECT_URL: TWITTER_REDIRECT_URL, }); -export const authenticate = queryParams => socTwitter.callback({ query: queryParams }); +const authenticate = queryParams => socTwitter.callback({ query: queryParams }); // doing this to mock authenticate function in test export const socUtils = { authenticate, + trackIntegration, getTwitterAuthorizeUrl: () => socTwitter.getAuthorizeUrl(), }; From c7053db5e341b88c9d1ae7e0e0baa8a6387f4ab9 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 8 Nov 2017 19:51:42 +0800 Subject: [PATCH 216/318] Add createFacebookIntegration db method --- src/__tests__/integrationDb.test.js | 18 ++++++++++++++++++ src/db/models/Integrations.js | 14 ++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/__tests__/integrationDb.test.js b/src/__tests__/integrationDb.test.js index e2bbb5d3f..906a22c65 100644 --- a/src/__tests__/integrationDb.test.js +++ b/src/__tests__/integrationDb.test.js @@ -431,4 +431,22 @@ describe('social integration test', () => { expect(integration.kind).toBe(KIND_CHOICES.TWITTER); expect(integration.twitterData.toJSON()).toEqual(doc.twitterData); }); + + test('create facebook integration', async () => { + const doc = { + name: 'name', + brandId: _brand._id, + facebookData: { + appId: '1', + pageIds: ['1'], + }, + }; + + const integration = await Integrations.createFacebookIntegration(doc); + + expect(integration.name).toBe(doc.name); + expect(integration.brandId).toBe(doc.brandId); + expect(integration.kind).toBe(KIND_CHOICES.FACEBOOK); + expect(integration.facebookData.toJSON()).toEqual(doc.facebookData); + }); }); diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index 97e307257..661e670c7 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -173,6 +173,20 @@ class Integration { }); } + /** + * Create facebook integration + * @param {Object} doc - Integration doc + * @return {Promise} returns integration document promise + */ + static createFacebookIntegration({ name, brandId, facebookData }) { + return this.createIntegration({ + name, + brandId, + kind: KIND_CHOICES.FACEBOOK, + facebookData, + }); + } + /** * Update messenger integration document * @param {Object} object - Integration main doc object From cff2a8573651b2a4567d22c1d1a0ee8e4e48a184 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 8 Nov 2017 19:59:12 +0800 Subject: [PATCH 217/318] Add facebook integreation mutation --- src/__tests__/integrationMutations.test.js | 27 +++++++++++++++++++- src/data/resolvers/mutations/integrations.js | 21 +++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/__tests__/integrationMutations.test.js b/src/__tests__/integrationMutations.test.js index f927f2d90..835c69ee1 100644 --- a/src/__tests__/integrationMutations.test.js +++ b/src/__tests__/integrationMutations.test.js @@ -17,7 +17,7 @@ describe('mutations', () => { const _adminUser = { _id: 'fakeId', role: ROLES.ADMIN }; test(`test if Error('Login required') exception is working as intended`, () => { - expect.assertions(9); + expect.assertions(10); // Login required ================== const check = mutation => { @@ -37,6 +37,7 @@ describe('mutations', () => { check(integrationMutations.integrationsEditFormIntegration); check(integrationMutations.integrationsRemove); check(integrationMutations.integrationsCreateTwitterIntegration); + check(integrationMutations.integrationsCreateFacebookIntegration); }); test(`test if Error('Permission required') exception is working as intended`, async () => { @@ -247,4 +248,28 @@ describe('mutations', () => { expect(Integrations.createTwitterIntegration).toBeCalledWith(doc); expect(socUtils.trackIntegration).toBeCalledWith(integrationDoc); }); + + test('create facebook integration', async () => { + Integrations.createFacebookIntegration = jest.fn(); + + const doc = { + name: 'name', + brandId: 'brandId', + appId: '1', + pageIds: ['1'], + }; + + await integrationMutations.integrationsCreateFacebookIntegration(null, doc, { + user: _adminUser, + }); + + expect(Integrations.createFacebookIntegration).toBeCalledWith({ + name: 'name', + brandId: 'brandId', + facebookData: { + appId: '1', + pageIds: ['1'], + }, + }); + }); }); diff --git a/src/data/resolvers/mutations/integrations.js b/src/data/resolvers/mutations/integrations.js index eed86123e..dcb892c98 100644 --- a/src/data/resolvers/mutations/integrations.js +++ b/src/data/resolvers/mutations/integrations.js @@ -96,6 +96,26 @@ const integrationMutations = { return integration; }, + /** + * Create a new facebook integration + * @param {Object} root + * @param {String} brandId - Integration brand id + * @param {String} name - Integration name + * @param {String} appId - Facebook app id in .env + * @param {String} pageIds - Selected facebook page ids + * @return {Promise} return Promise resolving Integration document + */ + async integrationsCreateFacebookIntegration(root, { name, brandId, appId, pageIds }) { + return await Integrations.createFacebookIntegration({ + name, + brandId, + facebookData: { + appId, + pageIds, + }, + }); + }, + /** * Edit a form integration * @param {Object} @@ -134,6 +154,7 @@ requireLogin(integrationMutations, 'integrationsSaveMessengerConfigs'); requireLogin(integrationMutations, 'integrationsCreateFormIntegration'); requireLogin(integrationMutations, 'integrationsEditFormIntegration'); requireLogin(integrationMutations, 'integrationsCreateTwitterIntegration'); +requireLogin(integrationMutations, 'integrationsCreateFacebookIntegration'); requireAdmin(integrationMutations, 'integrationsRemove'); export default integrationMutations; From 7b2ec30d259c79c28cde3060039d1e86cdb1d75a Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 9 Nov 2017 00:30:00 +0800 Subject: [PATCH 218/318] Add facebook integration mutations, queries --- .env.sample | 6 +++++ src/data/resolvers/queries/integrations.js | 26 ++++++++++++++++++++++ src/data/schema/integration.js | 9 ++++++++ src/social/facebook.js | 14 ++++++++---- src/social/facebookTracker.js | 11 +++++++-- 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/.env.sample b/.env.sample index 0f160e675..a9f2288ea 100644 --- a/.env.sample +++ b/.env.sample @@ -7,3 +7,9 @@ COMPANY_EMAIL_FROM='noreply@erxes.io' MAIL_SERVICE='sendgrid' MAIL_USER='apikey' MAIL_PASS='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' + +TWITTER_CONSUMER_KEY='xQlhyI9cAkRT5LOwGEpM1c2WU' +TWITTER_CONSUMER_SECRET='zGn2j60tBAv0tsClKOSeS3j4HXywCH01DJxOQna32bdcrGMee0' +TWITTER_REDIRECT_URL='http://5e574edb.ngrok.io/service/oauth/twitter_callback' + +FACEBOOK='[{"id":"","name":"","verifyToken":"","accessToken":""}]' diff --git a/src/data/resolvers/queries/integrations.js b/src/data/resolvers/queries/integrations.js index cb2314ea3..5fe2f749f 100644 --- a/src/data/resolvers/queries/integrations.js +++ b/src/data/resolvers/queries/integrations.js @@ -1,5 +1,6 @@ import { Integrations } from '../../../db/models'; import { socUtils } from '../../../social/twitterTracker'; +import { getConfig, getPageList } from '../../../social/facebook'; import { moduleRequireLogin } from '../../permissions'; import { paginate } from './utils'; @@ -53,6 +54,31 @@ const integrationQueries = { integrationGetTwitterAuthUrl() { return socUtils.getTwitterAuthorizeUrl(); }, + + /** + * Get facebook app list .env + * @return {Promise} - Apps list + */ + integrationFacebookAppsList() { + return getConfig().map(app => ({ + id: app.id, + name: app.name, + })); + }, + + /** + * Get facebook pages by appId + * @return {Promise} - Page list + */ + async integrationFacebookPagesList(root, { appId }) { + const app = getConfig().find(app => app.id === appId); + + if (!app) { + return []; + } + + return getPageList(app.accessToken); + }, }; moduleRequireLogin(integrationQueries); diff --git a/src/data/schema/integration.js b/src/data/schema/integration.js index 1f46dd3a1..38ac0f145 100644 --- a/src/data/schema/integration.js +++ b/src/data/schema/integration.js @@ -66,6 +66,8 @@ export const queries = ` integrationDetail(_id: String!): Integration integrationsTotalCount(kind: String): Int integrationGetTwitterAuthUrl: String + integrationFacebookAppsList: [JSON] + integrationFacebookPagesList(appId: Float): [JSON] `; export const mutations = ` @@ -97,6 +99,13 @@ export const mutations = ` queryParams: TwitterIntegrationAuthParams! ): Integration + integrationsCreateFacebookIntegration( + brandId: String!, + name: String!, + appId: String!, + pageIds: [String!]!, + ): Integration + integrationsEditFormIntegration( _id: String! name: String!, diff --git a/src/social/facebook.js b/src/social/facebook.js index 444700682..0f656584d 100755 --- a/src/social/facebook.js +++ b/src/social/facebook.js @@ -13,8 +13,8 @@ import { graphRequest } from './facebookTracker'; * @param {String} accessToken - App access token * @return {[Object]} - page list */ -export const getPageList = accessToken => { - const response = graphRequest.get('/me/accounts?limit=100', accessToken); +export const getPageList = async accessToken => { + const response = await graphRequest.get('/me/accounts?limit=100', accessToken); return response.data.map(page => ({ id: page.id, @@ -382,13 +382,13 @@ export const receiveWebhookResponse = async (app, data) => { * @param {String} messageId - Conversation message id */ export const facebookReply = async (conversation, text, messageId) => { - const { FACEBOOK } = process.env; + const FACEBOOK_APPS = getConfig(); const integration = await Integrations.findOne({ _id: conversation.integrationId, }); - const app = JSON.parse(FACEBOOK).find(a => a.id === integration.facebookData.appId); + const app = FACEBOOK_APPS.find(a => a.id === integration.facebookData.appId); // page access token const response = graphRequest.get( @@ -428,3 +428,9 @@ export const facebookReply = async (conversation, text, messageId) => { return null; }; + +export const getConfig = () => { + const { FACEBOOK } = process.env; + + return JSON.parse(FACEBOOK); +}; diff --git a/src/social/facebookTracker.js b/src/social/facebookTracker.js index 2d9d31b52..f24715e49 100644 --- a/src/social/facebookTracker.js +++ b/src/social/facebookTracker.js @@ -10,8 +10,15 @@ export const graphRequest = { graph.setAccessToken(accessToken); try { - // TODO - return graph[method](otherParams); + return new Promise((resolve, reject) => { + graph[method](path, ...otherParams, (error, response) => { + if (error) { + return reject(error); + } + + return resolve(response); + }); + }); // catch session expired or some other error } catch (e) { From f869b61a341595f6629e316d31275bb01c3c3ad5 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 9 Nov 2017 00:35:37 +0800 Subject: [PATCH 219/318] Some test fix --- src/__tests__/social/facebook.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/__tests__/social/facebook.test.js b/src/__tests__/social/facebook.test.js index 2f1b677d3..3bf50bfff 100644 --- a/src/__tests__/social/facebook.test.js +++ b/src/__tests__/social/facebook.test.js @@ -29,7 +29,7 @@ describe('facebook integration common tests', () => { test('get page list', async () => { sinon.stub(graphRequest, 'get').callsFake(() => ({ data: pages })); - expect(getPageList()).toEqual(pages); + expect(await getPageList()).toEqual(pages); graphRequest.get.restore(); // unwraps the spy }); @@ -37,8 +37,8 @@ describe('facebook integration common tests', () => { test('graph request', async () => { sinon.stub(graphRequest, 'base').callsFake(() => {}); - graphRequest.get(); - graphRequest.post(); + await graphRequest.get(); + await graphRequest.post(); graphRequest.base.restore(); // unwraps the spy }); From 56a99e0814507894cec69b35966bb5211a7a158e Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 9 Nov 2017 01:11:59 +0800 Subject: [PATCH 220/318] Add facebook integration tracker --- src/index.js | 5 ++++- src/social/facebookTracker.js | 31 +++++++++++++++++++++++++++++++ src/social/twitterTracker.js | 16 ++++++++++------ src/startup.js | 8 +++++++- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index f1b986a61..f363f7270 100755 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,7 @@ import { Customers } from './db/models'; import { connect } from './db/connection'; import { userMiddleware } from './auth'; import schema from './data'; -import './startup'; +import { init } from './startup'; // load environment variables dotenv.config(); @@ -42,6 +42,9 @@ const { PORT } = process.env; server.listen(PORT, () => { console.log(`GraphQL Server is now running on ${PORT}`); + // execute startup actions + init(app); + // Set up the WebSocket for handling GraphQL subscriptions new SubscriptionServer( { diff --git a/src/social/facebookTracker.js b/src/social/facebookTracker.js index f24715e49..c29442cb6 100644 --- a/src/social/facebookTracker.js +++ b/src/social/facebookTracker.js @@ -1,4 +1,5 @@ import graph from 'fbgraph'; +import { getConfig, receiveWebhookResponse } from './facebook'; /* * Common graph api request wrapper @@ -35,3 +36,33 @@ export const graphRequest = { return this.base('post', ...args); }, }; + +/* + * Listen for facebook webhook response + */ +export const trackIntegrations = expressApp => { + for (let app of getConfig()) { + expressApp.get(`/service/facebook/${app.id}/webhook-callback`, (req, res) => { + const query = req.query; + + // when the endpoint is registered as a webhook, it must echo back + // the 'hub.challenge' value it receives in the query arguments + if (query['hub.mode'] === 'subscribe' && query['hub.challenge']) { + if (query['hub.verify_token'] !== app.verifyToken) { + res.end('Verification token mismatch'); + } + + res.end(query['hub.challenge']); + } + }); + + expressApp.post(`/service/facebook/${app.id}/webhook-callback`, (req, res) => { + res.statusCode = 200; + + // receive per app webhook response + receiveWebhookResponse(app, req.body); + + res.end('success'); + }); + } +}; diff --git a/src/social/twitterTracker.js b/src/social/twitterTracker.js index 0184bb62c..3e53be33b 100644 --- a/src/social/twitterTracker.js +++ b/src/social/twitterTracker.js @@ -46,9 +46,13 @@ export const socUtils = { getTwitterAuthorizeUrl: () => socTwitter.getAuthorizeUrl(), }; -// track all twitter integrations for the first time -Integrations.find({ kind: INTEGRATION_KIND_CHOICES.TWITTER }).then(integrations => { - for (let integration of integrations) { - trackIntegration(integration); - } -}); +/* + * Track all twitter integrations for the first time + */ +export const trackIntegrations = () => { + Integrations.find({ kind: INTEGRATION_KIND_CHOICES.TWITTER }).then(integrations => { + for (let integration of integrations) { + trackIntegration(integration); + } + }); +}; diff --git a/src/startup.js b/src/startup.js index 91a07c72a..0f6c35270 100644 --- a/src/startup.js +++ b/src/startup.js @@ -1,2 +1,8 @@ import './cronJobs'; -import './social/twitterTracker'; +import { trackIntegrations as trackTwitters } from './social/twitterTracker'; +import { trackIntegrations as trackFacebooks } from './social/facebookTracker'; + +export const init = app => { + trackTwitters(); + trackFacebooks(app); +}; From 4eac43b080477d905257258c36fc6a8643e3e7e5 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 9 Nov 2017 03:13:39 +0800 Subject: [PATCH 221/318] #22 Add types for ActivityLogs --- src/data/index.js | 4 ++-- src/data/resolvers/mutations/customers.js | 6 +++-- src/data/schema/activityLog.js | 29 +++++++++++++++++++++++ src/data/schema/customer.js | 1 + src/data/schema/index.js | 7 ++++++ src/db/models/ActivityLogs.js | 20 +++++++++++++++- 6 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 src/data/schema/activityLog.js diff --git a/src/data/index.js b/src/data/index.js index f74730478..2d8618ef6 100644 --- a/src/data/index.js +++ b/src/data/index.js @@ -1,8 +1,8 @@ import { makeExecutableSchema } from 'graphql-tools'; import resolvers from './resolvers'; -import { types, queries, mutations, subscriptions } from './schema'; +import { inputs, types, queries, mutations, subscriptions } from './schema'; export default makeExecutableSchema({ - typeDefs: [types, queries, mutations, subscriptions], + typeDefs: [inputs, types, queries, mutations, subscriptions], resolvers, }); diff --git a/src/data/resolvers/mutations/customers.js b/src/data/resolvers/mutations/customers.js index 395846779..3c1f69f8a 100644 --- a/src/data/resolvers/mutations/customers.js +++ b/src/data/resolvers/mutations/customers.js @@ -1,4 +1,4 @@ -import { Customers } from '../../../db/models'; +import { Customers, ActivityLogs } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; @@ -28,7 +28,9 @@ const customerMutations = { * @return {Promise} newly created customer */ async customersAddCompany(root, args) { - return Customers.addCompany(args); + const customer = Customers.addCompany(args); + await ActivityLogs.createCustomerLog(customer); + return customer; }, }; diff --git a/src/data/schema/activityLog.js b/src/data/schema/activityLog.js new file mode 100644 index 000000000..fb8f386f7 --- /dev/null +++ b/src/data/schema/activityLog.js @@ -0,0 +1,29 @@ +export const inputs = ` + input ActivityLogSortDoc { + createdAt: String! + } +`; + +export const types = ` + type YearMonthDoc { + type: String + month: Int + } + + type ActivityContent { + name: String + } + + type ActivityLogForMonth { + date: YearMonthDoc! + list: [ActivityLog] + } + + type ActivityLog { + type: String! + action: String! + id: String! + createdAt: Date! + content: ActivityContent! + } +`; diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index d39355954..40776f3a4 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -37,6 +37,7 @@ export const queries = ` customers(params: CustomerListParams): [Customer] customerCounts(params: CustomerListParams): JSON customerDetail(_id: String!): Customer + customerActivityLog(_id: String!, sortDoc: ActivityLogSortDoc): [ActivityLog] customerListForSegmentPreview(segment: JSON, limit: Int): [Customer] customersTotalCount: Int `; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index 53a910dc1..68213aecb 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -82,12 +82,19 @@ import { mutations as ConversationMutations, } from './conversation'; +import { inputs as ActivityLogInputs, types as ActivityLogTypes } from './activityLog'; + +export const inputs = ` + ${ActivityLogInputs} +`; + export const types = ` scalar JSON scalar Date ${UserTypes} ${InternalNoteTypes} + ${ActivityLogTypes} ${CompanyTypes} ${ChannelTypes} ${BrandTypes} diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index ae2701257..b2011e8d8 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -1,4 +1,4 @@ -import mongoose from 'mongoose'; +import mongoose, { SchemaTypes } from 'mongoose'; import Random from 'meteor-random'; import { CUSTOMER_CONTENT_TYPES } from '../../data/constants'; @@ -76,6 +76,7 @@ const Activity = mongoose.Schema( required: true, enum: ACTIVITY_ACTIONS.ALL, }, + content: SchemaTypes.Mixed, id: { type: String, }, @@ -209,6 +210,9 @@ class ActivityLog { activity: { type: ACTIVITY_TYPES.SEGMENT, action: ACTIVITY_ACTIONS.CREATE, + content: { + name: segment.name, + }, id: segment._id, }, customer: { @@ -217,6 +221,20 @@ class ActivityLog { }, }); } + + /** + * Creates a customer or company registration log + */ + static createCustomerLog(customer) { + return this.createDoc({ + activity: { + type: ACTIVITY_TYPES.CUSTOMER, + action: ACTIVITY_ACTIONS.CREATE, + id: customer._id, + }, + customer, + }); + } } ActivityLogSchema.loadClass(ActivityLog); From edbb758ab7de61ba067a2c781615079de9f4c768 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 9 Nov 2017 06:08:37 +0800 Subject: [PATCH 222/318] Addcustomer activity log query --- src/__tests__/activityLogUtils.test.js | 16 +++++ src/data/resolvers/activityLog.js | 9 +++ src/data/resolvers/activityLogForMonth.js | 18 +++++ src/data/resolvers/index.js | 5 ++ src/data/resolvers/queries/customers.js | 26 ++++++++ src/data/schema/activityLog.js | 3 +- src/data/schema/customer.js | 2 +- src/data/utils.js | 80 +++++++++++++++++++++++ src/db/models/ActivityLogs.js | 5 +- src/index.js | 3 + src/segmentQueryBuilder.js | 1 - 11 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/activityLogUtils.test.js create mode 100644 src/data/resolvers/activityLog.js create mode 100644 src/data/resolvers/activityLogForMonth.js diff --git a/src/__tests__/activityLogUtils.test.js b/src/__tests__/activityLogUtils.test.js new file mode 100644 index 000000000..96b4cd57a --- /dev/null +++ b/src/__tests__/activityLogUtils.test.js @@ -0,0 +1,16 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { MonthActivityLogBuilder } from '../data/utils'; + +describe('activityLogUtils', () => { + test('MonthActivityLogBuilder', () => { + const customer = { + _id: 'customerId', + name: 'test customer name', + }; + + const monthActivityLogBuilder = new MonthActivityLogBuilder(customer); + console.log('aa: ', monthActivityLogBuilder.build()); + }); +}); diff --git a/src/data/resolvers/activityLog.js b/src/data/resolvers/activityLog.js new file mode 100644 index 000000000..5ce6f6fd6 --- /dev/null +++ b/src/data/resolvers/activityLog.js @@ -0,0 +1,9 @@ +export default { + action(obj) { + return `${obj.type}-${obj.action}`; + }, + + content(obj) { + return obj.activity.content; + }, +}; diff --git a/src/data/resolvers/activityLogForMonth.js b/src/data/resolvers/activityLogForMonth.js new file mode 100644 index 000000000..8f7cd1883 --- /dev/null +++ b/src/data/resolvers/activityLogForMonth.js @@ -0,0 +1,18 @@ +import { ActivityLogs } from '../../db/models'; + +export default { + date(obj) { + return obj.date.yearMonth; + }, + + list(obj) { + return ActivityLogs.find({ + 'customer.type': obj.customerType, + 'customer.id': obj.customer._id, + createdAt: { + $gte: obj.date.interval.start, + $lte: obj.date.interval.end, + }, + }); + }, +}; diff --git a/src/data/resolvers/index.js b/src/data/resolvers/index.js index e27aaae32..7a16111ad 100644 --- a/src/data/resolvers/index.js +++ b/src/data/resolvers/index.js @@ -14,6 +14,8 @@ import Conversation from './conversation'; import ConversationMessage from './conversationMessage'; import KnowledgeBaseCategory from './knowledgeBaseCategory'; import KnowledgeBaseTopic from './knowledgeBaseTopic'; +import ActivityLog from './activityLog'; +import ActivityLogForMonth from './activityLogForMonth'; export default { ...customScalars, @@ -35,4 +37,7 @@ export default { KnowledgeBaseCategory, KnowledgeBaseTopic, + + ActivityLog, + ActivityLogForMonth, }; diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index d2e091fdd..75a14a8ed 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -4,6 +4,7 @@ import { TAG_TYPES, INTEGRATION_KIND_CHOICES, CUSTOMER_CONTENT_TYPES } from '../ import QueryBuilder from '../../../segmentQueryBuilder.js'; import { moduleRequireLogin } from '../../permissions'; import { paginate } from './utils'; +import { CustomerMonthActivityLogBuilder } from '../../utils'; const listQuery = async params => { const selector = {}; @@ -147,6 +148,31 @@ const customerQueries = { customerDetail(root, { _id }) { return Customers.findOne({ _id }); }, + + /** + * Get activity log for customer + * @param {Object} root + * @param {Object} object2 - Graphql input data + * @param {string} object._id - Customer id + * @param {string} object.sortDoc - Graphql ActivityLogSort doc object + * @return {Promise} found customer + */ + async customerActivityLog(root, { _id }) { + const customer = await Customers.findOne({ _id }); + + const m = new CustomerMonthActivityLogBuilder(customer); + return m.build(); + // const cursor = ActivityLogs.find({ + // 'customer.type': CUSTOMER_CONTENT_TYPES.customer, + // 'customer.id': _id, + // }); + // + // if (sortDoc) { + // cursor.sort(sortDoc); + // } + + // return customerActivityLog; + }, }; moduleRequireLogin(customerQueries); diff --git a/src/data/schema/activityLog.js b/src/data/schema/activityLog.js index fb8f386f7..b057b9308 100644 --- a/src/data/schema/activityLog.js +++ b/src/data/schema/activityLog.js @@ -6,7 +6,7 @@ export const inputs = ` export const types = ` type YearMonthDoc { - type: String + year: Int month: Int } @@ -20,7 +20,6 @@ export const types = ` } type ActivityLog { - type: String! action: String! id: String! createdAt: Date! diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index 42e4025c5..96f3bab06 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -37,7 +37,7 @@ export const queries = ` customers(params: CustomerListParams): [Customer] customerCounts(params: CustomerListParams): JSON customerDetail(_id: String!): Customer - customerActivityLog(_id: String!, sortDoc: ActivityLogSortDoc): [ActivityLog] + customerActivityLog(_id: String!, sortDoc: ActivityLogSortDoc): [ActivityLogForMonth] customerListForSegmentPreview(segment: JSON, limit: Int): [Customer] `; diff --git a/src/data/utils.js b/src/data/utils.js index afe76b56f..e346b42bf 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -2,6 +2,7 @@ import fs from 'fs'; import nodemailer from 'nodemailer'; import Handlebars from 'handlebars'; import { Notifications, Users } from '../db/models'; +import { CUSTOMER_CONTENT_TYPES } from './constants'; /** * Read contents of a file @@ -142,9 +143,88 @@ export const sendNotification = async ({ createdUser, receivers, ...doc }) => { }); }; +const START_DATE = { + year: 2017, + month: 0, +}; + +class BaseMonthActivityBuilder { + constructor(customer) { + this.customer = customer; + } + + getDaysInMonth(year, month) { + return new Date(year, month, 0).getDate(); + } + + generateDates() { + const now = new Date(); + + const endYear = now.getFullYear(); + const endMonth = now.getMonth(); + + const monthIntervals = []; + + for ( + let year = START_DATE.year, month = START_DATE.month; + year < endYear || (year === endYear && month <= endMonth); + month++ + ) { + monthIntervals.push({ + yearMonth: { + year, + month, + }, + interval: { + start: new Date(year, month, 1), + end: new Date(year, month, this.getDaysInMonth(year, month)), + }, + }); + + if ((month + 1) % 12 == 0) { + month = 0; + year++; + } + } + + return monthIntervals; + } + + build() { + const dates = this.generateDates(); + const list = []; + + for (let date of dates) { + list.push({ + customer: this.customer, + customerType: this.customerType, + date, + }); + } + + return list; + } +} + +export class CustomerMonthActivityLogBuilder extends BaseMonthActivityBuilder { + constructor(customer) { + super(customer); + this.customerType = CUSTOMER_CONTENT_TYPES.CUSTOMER; + } +} + +export class CompanyMonthActivityLogBuilder extends BaseMonthActivityBuilder { + constructor(customer) { + super(customer); + this.customerType = CUSTOMER_CONTENT_TYPES.COMPANY; + } +} + export default { sendEmail, sendNotification, readFile, createTransporter, + CustomerMonthActivityLogBuilder, + CompanyMonthActivityLogBuilder, }; diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index b2011e8d8..89e809485 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -76,7 +76,10 @@ const Activity = mongoose.Schema( required: true, enum: ACTIVITY_ACTIONS.ALL, }, - content: SchemaTypes.Mixed, + content: { + type: SchemaTypes.Mixed, + default: {}, + }, id: { type: String, }, diff --git a/src/index.js b/src/index.js index f363f7270..4a6775482 100755 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ import { connect } from './db/connection'; import { userMiddleware } from './auth'; import schema from './data'; import { init } from './startup'; +import cronJobs from './cronJobs'; // load environment variables dotenv.config(); @@ -88,3 +89,5 @@ if (process.env.NODE_ENV === 'development') { }), ); } + +cronJobs.createActivityLogsFromSegments(); diff --git a/src/segmentQueryBuilder.js b/src/segmentQueryBuilder.js index dc80e7fda..6cea24e72 100644 --- a/src/segmentQueryBuilder.js +++ b/src/segmentQueryBuilder.js @@ -109,6 +109,5 @@ function convertConditionToQuery(condition) { function escapeRegExp(string) { // $& means the whole matched string - console.log('string: ', string); return new String(string).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } From a6b834b3f1ae78f3e5246f56d5c36b5d6771f95e Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 9 Nov 2017 06:30:19 +0800 Subject: [PATCH 223/318] add customer log query --- src/data/index.js | 4 ++-- src/data/resolvers/activityLog.js | 2 +- src/data/schema/activityLog.js | 8 +------- src/data/schema/customer.js | 2 +- src/data/schema/index.js | 6 +----- src/data/utils.js | 2 +- 6 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/data/index.js b/src/data/index.js index 2d8618ef6..f74730478 100644 --- a/src/data/index.js +++ b/src/data/index.js @@ -1,8 +1,8 @@ import { makeExecutableSchema } from 'graphql-tools'; import resolvers from './resolvers'; -import { inputs, types, queries, mutations, subscriptions } from './schema'; +import { types, queries, mutations, subscriptions } from './schema'; export default makeExecutableSchema({ - typeDefs: [inputs, types, queries, mutations, subscriptions], + typeDefs: [types, queries, mutations, subscriptions], resolvers, }); diff --git a/src/data/resolvers/activityLog.js b/src/data/resolvers/activityLog.js index 5ce6f6fd6..12b19dcb8 100644 --- a/src/data/resolvers/activityLog.js +++ b/src/data/resolvers/activityLog.js @@ -1,6 +1,6 @@ export default { action(obj) { - return `${obj.type}-${obj.action}`; + return `${obj.activity.type}-${obj.activity.action}`; }, content(obj) { diff --git a/src/data/schema/activityLog.js b/src/data/schema/activityLog.js index b057b9308..6fe6d30f4 100644 --- a/src/data/schema/activityLog.js +++ b/src/data/schema/activityLog.js @@ -1,9 +1,3 @@ -export const inputs = ` - input ActivityLogSortDoc { - createdAt: String! - } -`; - export const types = ` type YearMonthDoc { year: Int @@ -16,7 +10,7 @@ export const types = ` type ActivityLogForMonth { date: YearMonthDoc! - list: [ActivityLog] + list: [ActivityLog]! } type ActivityLog { diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index 96f3bab06..93fbb741b 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -37,7 +37,7 @@ export const queries = ` customers(params: CustomerListParams): [Customer] customerCounts(params: CustomerListParams): JSON customerDetail(_id: String!): Customer - customerActivityLog(_id: String!, sortDoc: ActivityLogSortDoc): [ActivityLogForMonth] + customerActivityLog(_id: String!): [ActivityLogForMonth] customerListForSegmentPreview(segment: JSON, limit: Int): [Customer] `; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index 68213aecb..57292dd1e 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -82,11 +82,7 @@ import { mutations as ConversationMutations, } from './conversation'; -import { inputs as ActivityLogInputs, types as ActivityLogTypes } from './activityLog'; - -export const inputs = ` - ${ActivityLogInputs} -`; +import { types as ActivityLogTypes } from './activityLog'; export const types = ` scalar JSON diff --git a/src/data/utils.js b/src/data/utils.js index e346b42bf..35a46c6a5 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -195,7 +195,7 @@ class BaseMonthActivityBuilder { const list = []; for (let date of dates) { - list.push({ + list.unshift({ customer: this.customer, customerType: this.customerType, date, From 7d02b5f701a179f469fd0960789b5269ec3bc7f8 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 9 Nov 2017 10:36:38 +0800 Subject: [PATCH 224/318] #22 refactor, test fix --- src/__tests__/activityLogCronJob.test.js | 17 +++++++++++++++-- src/__tests__/activityLogDb.test.js | 2 +- src/__tests__/activityLogUtils.test.js | 5 ++--- src/cronJobs/activityLogs.js | 4 ---- src/index.js | 4 +--- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/__tests__/activityLogCronJob.test.js b/src/__tests__/activityLogCronJob.test.js index 302c789e3..ef77aa120 100644 --- a/src/__tests__/activityLogCronJob.test.js +++ b/src/__tests__/activityLogCronJob.test.js @@ -10,12 +10,13 @@ import ActivityLogs, { ACTIVITY_ACTIONS, ACTION_PERFORMER_TYPES, } from '../db/models/ActivityLogs'; +import { Customers } from '../db/models'; beforeAll(() => connect()); afterAll(() => disconnect()); describe('test activityLogsCronJob', () => { - test('1', async () => { + test('test if it is working as intended', async () => { const nameEqualsConditions = [ { type: 'string', @@ -26,7 +27,7 @@ describe('test activityLogsCronJob', () => { }, ]; - const customer = await customerFactory({ name: 'John Smith' }); + const customer = await customerFactory({ name: 'john smith' }); const segment = await segmentFactory({ contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, conditions: nameEqualsConditions, @@ -34,10 +35,16 @@ describe('test activityLogsCronJob', () => { await cronJobs.createActivityLogsFromSegments(); + expect(await ActivityLogs.find().count()).toBe(1); + const aLog = await ActivityLogs.findOne(); + expect(aLog.activity.toObject()).toEqual({ type: ACTIVITY_TYPES.SEGMENT, action: ACTIVITY_ACTIONS.CREATE, + content: { + name: segment.name, + }, id: segment._id, }); expect(aLog.customer.toObject()).toEqual({ @@ -47,5 +54,11 @@ describe('test activityLogsCronJob', () => { expect(aLog.performedBy.toObject()).toEqual({ type: ACTION_PERFORMER_TYPES.SYSTEM, }); + + await Customers.updateCustomer(customer._id, { name: 'jane smith' }); + + await cronJobs.createActivityLogsFromSegments(); + + expect(await ActivityLogs.find().count()).toBe(1); }); }); diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index 5fc8b842a..3107512e4 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -99,7 +99,7 @@ describe('ActivityLogs model methods', () => { try { await ActivityLogs.createConversationLog(conversation, null, customer); } catch (e) { - expect(e.message).toBe(`'user' must be supplied when adding activity log for internal note`); + expect(e.message).toBe(`'user' must be supplied when adding activity log for conversations`); } try { diff --git a/src/__tests__/activityLogUtils.test.js b/src/__tests__/activityLogUtils.test.js index 96b4cd57a..1f0eac293 100644 --- a/src/__tests__/activityLogUtils.test.js +++ b/src/__tests__/activityLogUtils.test.js @@ -1,7 +1,7 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ -import { MonthActivityLogBuilder } from '../data/utils'; +import { CustomerMonthActivityLogBuilder } from '../data/utils'; describe('activityLogUtils', () => { test('MonthActivityLogBuilder', () => { @@ -10,7 +10,6 @@ describe('activityLogUtils', () => { name: 'test customer name', }; - const monthActivityLogBuilder = new MonthActivityLogBuilder(customer); - console.log('aa: ', monthActivityLogBuilder.build()); + new CustomerMonthActivityLogBuilder(customer); }); }); diff --git a/src/cronJobs/activityLogs.js b/src/cronJobs/activityLogs.js index 1df64c89d..e6b795889 100644 --- a/src/cronJobs/activityLogs.js +++ b/src/cronJobs/activityLogs.js @@ -8,10 +8,7 @@ import QueryBuilder from '../segmentQueryBuilder'; export const createActivityLogsFromSegments = async () => { const segments = await Segments.find({}); - console.log('started running'); for (let segment of segments) { - console.log('segment._id : ', segment._id); - console.log('segment.name: ', segment.name); const selector = await QueryBuilder.segments(segment); // console.log('segment: ', segment); // console.log('selector: ', selector['$and'] && selector['$and'][0]['$or']); @@ -25,7 +22,6 @@ export const createActivityLogsFromSegments = async () => { } } } - console.log('stopped running (successful)'); }; /** diff --git a/src/index.js b/src/index.js index 4a6775482..d2930bc9b 100755 --- a/src/index.js +++ b/src/index.js @@ -13,7 +13,7 @@ import { connect } from './db/connection'; import { userMiddleware } from './auth'; import schema from './data'; import { init } from './startup'; -import cronJobs from './cronJobs'; +import './cronJobs'; // load environment variables dotenv.config(); @@ -89,5 +89,3 @@ if (process.env.NODE_ENV === 'development') { }), ); } - -cronJobs.createActivityLogsFromSegments(); From 5a10fb98b8910cb56681a07bcdd52d184979c18f Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 9 Nov 2017 11:20:05 +0800 Subject: [PATCH 225/318] Make ActivityLog model test coverage 100% --- src/__tests__/activityLogCronJob.test.js | 23 +++++- src/__tests__/activityLogDb.test.js | 90 ++++++++++++++++++++++-- src/db/models/ActivityLogs.js | 29 ++++++-- 3 files changed, 127 insertions(+), 15 deletions(-) diff --git a/src/__tests__/activityLogCronJob.test.js b/src/__tests__/activityLogCronJob.test.js index ef77aa120..01ee875e9 100644 --- a/src/__tests__/activityLogCronJob.test.js +++ b/src/__tests__/activityLogCronJob.test.js @@ -10,13 +10,13 @@ import ActivityLogs, { ACTIVITY_ACTIONS, ACTION_PERFORMER_TYPES, } from '../db/models/ActivityLogs'; -import { Customers } from '../db/models'; beforeAll(() => connect()); afterAll(() => disconnect()); describe('test activityLogsCronJob', () => { test('test if it is working as intended', async () => { + // check if the activity log is being created ================== const nameEqualsConditions = [ { type: 'string', @@ -55,10 +55,27 @@ describe('test activityLogsCronJob', () => { type: ACTION_PERFORMER_TYPES.SYSTEM, }); - await Customers.updateCustomer(customer._id, { name: 'jane smith' }); + // check if the second activity log is being created + // also check if the duplicate activity log is + // not being created for the former customer ================ + const nameEqualsConditions2 = [ + { + type: 'string', + dateUnit: 'days', + value: 'jane smith', + operator: 'e', + field: 'name', + }, + ]; + + await customerFactory({ name: 'jane smith' }); + await segmentFactory({ + contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + conditions: nameEqualsConditions2, + }); await cronJobs.createActivityLogsFromSegments(); - expect(await ActivityLogs.find().count()).toBe(1); + expect(await ActivityLogs.find().count()).toBe(2); }); }); diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index 3107512e4..f629510b5 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -14,6 +14,7 @@ import { internalNoteFactory, customerFactory, conversationFactory, + segmentFactory, } from '../db/factories'; beforeAll(() => connect()); @@ -47,7 +48,7 @@ describe('ActivityLogs model methods', () => { }); test(`check if exception is being thrown when calling - createInternalNoteLog without setting 'actionPerformedBy'`, async () => { + createInternalNoteLog without setting 'user'`, async () => { const customer = await customerFactory(); const internalNote = await internalNoteFactory({ @@ -62,7 +63,7 @@ describe('ActivityLogs model methods', () => { } }); - test(`createInternalNoteLog with setting 'actionPerformedBy'`, async () => { + test(`createInternalNoteLog with setting 'user'`, async () => { const user = await userFactory({}); const customer = await customerFactory(); @@ -85,14 +86,57 @@ describe('ActivityLogs model methods', () => { }); }); - // TODO: write this test test(`check if exception is being thrown when calling - createSegmentLog without setting 'actionPerformedBy'`, async () => {}); + createSegmentLog without setting 'customer'`, async () => { + expect.assertions(1); - // TODO: write this test - test(`createSegmentLog with setting 'actionPerformedBy'`, async () => {}); + const segment = segmentFactory({}); + try { + await ActivityLogs.createSegmentLog(segment, null); + } catch (e) { + expect(e.message).toBe('customer must be supplied'); + } + }); + + test(`createSegmentLog with setting 'customer'`, async () => { + // check if the activity log is being created ================== + const nameEqualsConditions = [ + { + type: 'string', + dateUnit: 'days', + value: 'John Smith', + operator: 'e', + field: 'name', + }, + ]; + + const customer = await customerFactory({ name: 'john smith' }); + const segment = await segmentFactory({ + contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + conditions: nameEqualsConditions, + }); + + const segmentLog = await ActivityLogs.createSegmentLog(segment, customer); + + expect(segmentLog.activity.toObject()).toEqual({ + type: ACTIVITY_TYPES.SEGMENT, + action: ACTIVITY_ACTIONS.CREATE, + content: { + name: segment.name, + }, + id: segment._id, + }); + expect(segmentLog.customer.toObject()).toEqual({ + type: segment.contentType, + id: customer._id, + }); + expect(segmentLog.performedBy.toObject()).toEqual({ + type: ACTION_PERFORMER_TYPES.SYSTEM, + }); + }); test(`check if exceptions are being thrown as intended when calling createConversationLog`, async () => { + expect.assertions(3); const conversation = await conversationFactory({}); const customer = await customerFactory({}); @@ -109,6 +153,14 @@ describe('ActivityLogs model methods', () => { `'customer' must be supplied when adding activity log for conversations`, ); } + + try { + await ActivityLogs.createConversationLog(conversation, conversation, {}); + } catch (e) { + expect(e.message).toBe( + `'customer' must be supplied when adding activity log for conversations`, + ); + } }); test(`check if createConversationLog is working as intended`, async () => { @@ -116,7 +168,7 @@ describe('ActivityLogs model methods', () => { const customer = await customerFactory({}); const customerDoc = { type: CUSTOMER_CONTENT_TYPES.CUSTOMER, - id: customer._id, + _id: customer._id, }; const user = await userFactory({}); @@ -131,4 +183,28 @@ describe('ActivityLogs model methods', () => { id: conversation._id, }); }); + + test(`createCustomerLog`, async () => { + const customer = await customerFactory({}); + const user = await userFactory({}); + + const aLog = await ActivityLogs.createCustomerLog(customer, user); + + expect(aLog.performedBy.toObject()).toEqual({ + type: ACTION_PERFORMER_TYPES.USER, + id: user._id, + }); + expect(aLog.activity.toObject()).toEqual({ + type: ACTIVITY_TYPES.CUSTOMER, + action: ACTIVITY_ACTIONS.CREATE, + content: { + name: customer.name, + }, + id: customer._id, + }); + expect(aLog.customer.toObject()).toEqual({ + type: CUSTOMER_CONTENT_TYPES.CUSTOMER, + id: customer._id, + }); + }); }); diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 89e809485..8418808c3 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -176,7 +176,7 @@ class ActivityLog { throw new Error(`'user' must be supplied when adding activity log for conversations`); } - if (customer == null || (customer && !user._id)) { + if (customer == null || (customer && !customer._id)) { throw new Error(`'customer' must be supplied when adding activity log for conversations`); } @@ -187,10 +187,19 @@ class ActivityLog { id: conversation._id, }, performedBy: user, - customer, + customer: { + type: conversation.contentType, + id: customer._id, + }, }); } + /** + * Create a customer or company segment log + * @param {Segment} segment - Segment document + * @param {Customer} customer - Related customer or company + * @return {Promise} return Promise resolving created Segment + */ static async createSegmentLog(segment, customer) { if (!customer) { throw new Error('customer must be supplied'); @@ -226,16 +235,26 @@ class ActivityLog { } /** - * Creates a customer or company registration log + * Creates a customer registration log + * @param {Customer} customer - Customer document + * @param {user} user - user document + * @return {Promise} return Promise resolving created ActivityLog */ - static createCustomerLog(customer) { + static createCustomerLog(customer, user) { return this.createDoc({ activity: { type: ACTIVITY_TYPES.CUSTOMER, action: ACTIVITY_ACTIONS.CREATE, + content: { + name: customer.name, + }, id: customer._id, }, - customer, + customer: { + type: CUSTOMER_CONTENT_TYPES.CUSTOMER, + id: customer._id, + }, + performedBy: user, }); } } From 57fddc05073cf2efa19708fa620b79e50e2976b6 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 9 Nov 2017 12:17:30 +0800 Subject: [PATCH 226/318] #22 Update tests --- src/__tests__/activityLogDb.test.js | 86 +++++++++++++++++++--- src/__tests__/activityLogMutations.test.js | 25 +++++++ src/cronJobs/activityLogs.js | 7 +- src/data/resolvers/mutations/customers.js | 14 ++-- src/db/factories.js | 1 + src/db/models/ActivityLogs.js | 49 +++++++++++- 6 files changed, 158 insertions(+), 24 deletions(-) create mode 100644 src/__tests__/activityLogMutations.test.js diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index f629510b5..9df532f5e 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -13,6 +13,7 @@ import { userFactory, internalNoteFactory, customerFactory, + companyFactory, conversationFactory, segmentFactory, } from '../db/factories'; @@ -21,6 +22,10 @@ beforeAll(() => connect()); afterAll(() => disconnect()); describe('ActivityLogs model methods', () => { + afterEach(async () => { + await ActivityLogs.remove({}); + }); + test(`check whether not setting 'user' is setting expected values in the collection or not`, async () => { const activityDoc = { @@ -91,6 +96,7 @@ describe('ActivityLogs model methods', () => { expect.assertions(1); const segment = segmentFactory({}); + try { await ActivityLogs.createSegmentLog(segment, null); } catch (e) { @@ -165,30 +171,64 @@ describe('ActivityLogs model methods', () => { test(`check if createConversationLog is working as intended`, async () => { const conversation = await conversationFactory({}); - const customer = await customerFactory({}); - const customerDoc = { - type: CUSTOMER_CONTENT_TYPES.CUSTOMER, - _id: customer._id, - }; + const companyA = await companyFactory({}); + const companyB = await companyFactory({}); + const customer = await customerFactory({ companyIds: [companyA._id, companyB._id] }); + + console.log('customer: ', customer); const user = await userFactory({}); - const aLog = await ActivityLogs.createConversationLog(conversation, user, customerDoc); + let aLog = await ActivityLogs.createConversationLog(conversation, user, customer); - expect(aLog.performedBy.type).toBe(ACTION_PERFORMER_TYPES.USER); - expect(aLog.performedBy.id).toBe(user._id); - expect(aLog.customer.toObject()).toEqual(customerDoc); + // check customer conversation log + expect(aLog.performedBy.toObject()).toEqual({ + type: ACTION_PERFORMER_TYPES.USER, + id: user._id, + }); + expect(aLog.customer.toObject()).toEqual({ + type: CUSTOMER_CONTENT_TYPES.CUSTOMER, + id: customer._id, + }); expect(aLog.activity.toObject()).toEqual({ type: ACTIVITY_TYPES.CONVERSATION, action: ACTIVITY_ACTIONS.CREATE, id: conversation._id, }); + + console.log('ActivityLogs: ', await ActivityLogs.find({})); + // check company conversation logs ===================================== + aLog = await ActivityLogs.findOne({ + 'activity.type': ACTIVITY_TYPES.CONVERSATION, + 'activity.action': ACTIVITY_ACTIONS.CREATE, + 'activity.id': conversation._id, + 'performedBy.type': ACTION_PERFORMER_TYPES.USER, + 'performedBy.id': user._id, + 'customer.type': CUSTOMER_CONTENT_TYPES.COMPANY, + 'customer.id': companyA._id, + }); + + expect(aLog).toBeDefined(); + expect(aLog.customer.id).toBe(companyA._id); + + aLog = await ActivityLogs.findOne({ + 'activity.type': ACTIVITY_TYPES.CONVERSATION, + 'activity.action': ACTIVITY_ACTIONS.CREATE, + 'activity.id': conversation._id, + 'performedBy.type': ACTION_PERFORMER_TYPES.USER, + 'performedBy.id': user._id, + 'customer.type': CUSTOMER_CONTENT_TYPES.COMPANY, + 'customer.id': companyB._id, + }); + + expect(aLog).toBeDefined(); + expect(aLog.customer.id).toBe(companyB._id); }); - test(`createCustomerLog`, async () => { + test(`createCustomerRegistrationLog`, async () => { const customer = await customerFactory({}); const user = await userFactory({}); - const aLog = await ActivityLogs.createCustomerLog(customer, user); + const aLog = await ActivityLogs.createCustomerRegistrationLog(customer, user); expect(aLog.performedBy.toObject()).toEqual({ type: ACTION_PERFORMER_TYPES.USER, @@ -207,4 +247,28 @@ describe('ActivityLogs model methods', () => { id: customer._id, }); }); + + test(`createCompanyRegistrationLog`, async () => { + const company = await companyFactory({}); + const user = await userFactory({}); + + const aLog = await ActivityLogs.createCompanyRegistrationLog(company, user); + + expect(aLog.performedBy.toObject()).toEqual({ + type: ACTION_PERFORMER_TYPES.USER, + id: user._id, + }); + expect(aLog.activity.toObject()).toEqual({ + type: ACTIVITY_TYPES.COMPANY, + action: ACTIVITY_ACTIONS.CREATE, + content: { + name: company.name, + }, + id: company._id, + }); + expect(aLog.customer.toObject()).toEqual({ + type: CUSTOMER_CONTENT_TYPES.COMPANY, + id: company._id, + }); + }); }); diff --git a/src/__tests__/activityLogMutations.test.js b/src/__tests__/activityLogMutations.test.js new file mode 100644 index 000000000..fdfcbf596 --- /dev/null +++ b/src/__tests__/activityLogMutations.test.js @@ -0,0 +1,25 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import customerMutations from '../data/resolvers/mutations/customers'; +import { ROLES } from '../data/constants'; +import { ActivityLogs } from '../db/models'; +import { userFactory } from '../db/factories'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('ActivityLog creation on Customer creation', () => { + test(`createCompanyRegistrationLog`, async () => { + const customerDoc = { + name: 'Reggina', + }; + + const user = userFactory({ role: ROLES.CONTRIBUTOR }); + + await customerMutations.customersAdd(null, customerDoc, { user }); + + expect(await ActivityLogs.find({}).count()).toBe(1); + }); +}); diff --git a/src/cronJobs/activityLogs.js b/src/cronJobs/activityLogs.js index e6b795889..2ead05c88 100644 --- a/src/cronJobs/activityLogs.js +++ b/src/cronJobs/activityLogs.js @@ -35,10 +35,9 @@ export const createActivityLogsFromSegments = async () => { * │ └──────────────────── minute (0 - 59) * └───────────────────────── second (0 - 59, OPTIONAL) */ -// every 10 minutes -// schedule.scheduleJob('*/5 * * * *', function() { -// createActivityLogsFromSegments(); -// }); +schedule.scheduleJob('* * * * *', function() { + createActivityLogsFromSegments(); +}); export default { createActivityLogsFromSegments, diff --git a/src/data/resolvers/mutations/customers.js b/src/data/resolvers/mutations/customers.js index 3c1f69f8a..deecab4d4 100644 --- a/src/data/resolvers/mutations/customers.js +++ b/src/data/resolvers/mutations/customers.js @@ -7,8 +7,10 @@ const customerMutations = { * Create new customer * @return {Promise} customer object */ - customersAdd(root, doc) { - return Customers.createCustomer(doc); + async customersAdd(root, doc, { user }) { + const customer = await Customers.createCustomer(doc); + await ActivityLogs.createCustomerRegistrationLog(customer, user); + return customer; }, /** @@ -27,10 +29,10 @@ const customerMutations = { * @param {String} args.website - Company website * @return {Promise} newly created customer */ - async customersAddCompany(root, args) { - const customer = Customers.addCompany(args); - await ActivityLogs.createCustomerLog(customer); - return customer; + async customersAddCompany(root, args, { user }) { + const company = await Customers.addCompany(args); + await ActivityLogs.createCompanyRegistrationLog(company, { user }); + return company; }, }; diff --git a/src/db/factories.js b/src/db/factories.js index 2988368e5..519605880 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -168,6 +168,7 @@ export const customerFactory = (params = {}) => { phone: params.phone || faker.random.word(), messengerData: params.messengerData || {}, customFieldsData: params.customFieldsData || {}, + companyIds: params.companyIds || null, }); return customer.save(); diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 8418808c3..2863cae8d 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -163,6 +163,8 @@ class ActivityLog { } /** + * Create conversation log for a given customer, if the customer is related to companies, + * then create conversation log with all related companies * @param {Object} conversation - Conversation object * @param {string} conversation._id - Conversation document id * @param {Object} user - User object @@ -171,7 +173,7 @@ class ActivityLog { * @param {string} customer.type - One of CUSTOMER_CONTENT_TYPES choices * @param {string} customer.id - Customer document id */ - static createConversationLog(conversation, user, customer) { + static async createConversationLog(conversation, user, customer) { if (user == null || (user && !user._id)) { throw new Error(`'user' must be supplied when adding activity log for conversations`); } @@ -180,6 +182,23 @@ class ActivityLog { throw new Error(`'customer' must be supplied when adding activity log for conversations`); } + if (customer.companyIds && customer.companyIds.length > 0) { + for (let companyId of customer.companyIds) { + await this.createDoc({ + activity: { + type: ACTIVITY_TYPES.CONVERSATION, + action: ACTIVITY_ACTIONS.CREATE, + id: conversation._id, + }, + performedBy: user, + customer: { + type: CUSTOMER_CONTENT_TYPES.COMPANY, + id: companyId, + }, + }); + } + } + return this.createDoc({ activity: { type: ACTIVITY_TYPES.CONVERSATION, @@ -188,7 +207,7 @@ class ActivityLog { }, performedBy: user, customer: { - type: conversation.contentType, + type: CUSTOMER_CONTENT_TYPES.CUSTOMER, id: customer._id, }, }); @@ -240,7 +259,7 @@ class ActivityLog { * @param {user} user - user document * @return {Promise} return Promise resolving created ActivityLog */ - static createCustomerLog(customer, user) { + static createCustomerRegistrationLog(customer, user) { return this.createDoc({ activity: { type: ACTIVITY_TYPES.CUSTOMER, @@ -257,6 +276,30 @@ class ActivityLog { performedBy: user, }); } + + /** + * Creates a customer company registration log + * @param {Company} company - Company document + * @param {user} user - user document + * @return {Promise} return Promise resolving created ActivityLog + */ + static createCompanyRegistrationLog(company, user) { + return this.createDoc({ + activity: { + type: ACTIVITY_TYPES.COMPANY, + action: ACTIVITY_ACTIONS.CREATE, + content: { + name: company.name, + }, + id: company._id, + }, + customer: { + type: CUSTOMER_CONTENT_TYPES.COMPANY, + id: company._id, + }, + performedBy: user, + }); + } } ActivityLogSchema.loadClass(ActivityLog); From debf38da87d29d018743050eda23687e22297208 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 9 Nov 2017 14:44:25 +0800 Subject: [PATCH 227/318] Add mutation test on internal note, customers, companies, didn't add conversation log --- src/__tests__/activityLogDb.test.js | 12 +- src/__tests__/activityLogMutations.test.js | 71 +++++++++++- src/__tests__/customerMutations.test.js | 14 ++- src/data/resolvers/mutations/customers.js | 2 +- src/data/resolvers/mutations/internalNotes.js | 8 +- src/db/factories.js | 2 +- src/db/models/ActivityLogs.js | 108 +++++++++++------- 7 files changed, 161 insertions(+), 56 deletions(-) diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index 9df532f5e..8e73b183e 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -3,7 +3,7 @@ import { connect, disconnect } from '../db/connection'; import { CUSTOMER_CONTENT_TYPES } from '../data/constants'; -import { ActivityLogs } from '../db/models'; +import { ActivityLogs, Conversations } from '../db/models'; import { ACTION_PERFORMER_TYPES, ACTIVITY_TYPES, @@ -24,6 +24,7 @@ afterAll(() => disconnect()); describe('ActivityLogs model methods', () => { afterEach(async () => { await ActivityLogs.remove({}); + await Conversations.remove({}); }); test(`check whether not setting 'user' @@ -175,7 +176,6 @@ describe('ActivityLogs model methods', () => { const companyB = await companyFactory({}); const customer = await customerFactory({ companyIds: [companyA._id, companyB._id] }); - console.log('customer: ', customer); const user = await userFactory({}); let aLog = await ActivityLogs.createConversationLog(conversation, user, customer); @@ -195,7 +195,6 @@ describe('ActivityLogs model methods', () => { id: conversation._id, }); - console.log('ActivityLogs: ', await ActivityLogs.find({})); // check company conversation logs ===================================== aLog = await ActivityLogs.findOne({ 'activity.type': ACTIVITY_TYPES.CONVERSATION, @@ -222,6 +221,13 @@ describe('ActivityLogs model methods', () => { expect(aLog).toBeDefined(); expect(aLog.customer.id).toBe(companyB._id); + + expect(await ActivityLogs.find({}).count()).toBe(3); + + // test whether activity logs for this conversation is being duplicated or not ======== + await ActivityLogs.createConversationLog(conversation, user, customer); + + expect(await ActivityLogs.find({}).count()).toBe(3); }); test(`createCustomerRegistrationLog`, async () => { diff --git a/src/__tests__/activityLogMutations.test.js b/src/__tests__/activityLogMutations.test.js index fdfcbf596..55d388a0c 100644 --- a/src/__tests__/activityLogMutations.test.js +++ b/src/__tests__/activityLogMutations.test.js @@ -2,24 +2,83 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import customerMutations from '../data/resolvers/mutations/customers'; +import mutations from '../data/resolvers/mutations'; import { ROLES } from '../data/constants'; -import { ActivityLogs } from '../db/models'; -import { userFactory } from '../db/factories'; +import ActivityLogs, { ACTIVITY_TYPES } from '../db/models/ActivityLogs'; +import { userFactory, customerFactory } from '../db/factories'; +import { CUSTOMER_CONTENT_TYPES } from '../data/constants'; beforeAll(() => connect()); afterAll(() => disconnect()); describe('ActivityLog creation on Customer creation', () => { + afterEach(async () => { + await ActivityLogs.remove({}); + }); + test(`createCompanyRegistrationLog`, async () => { const customerDoc = { name: 'Reggina', }; - const user = userFactory({ role: ROLES.CONTRIBUTOR }); + const user = await userFactory({ role: ROLES.CONTRIBUTOR }); + + const customer = await mutations.customersAdd(null, customerDoc, { user }); + + expect(await ActivityLogs.find().count()).toBe(1); + const aLog = await ActivityLogs.findOne({}); + expect(aLog).toBeDefined(); + + expect(aLog.activity.type).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); + expect(aLog.activity.id).toBe(customer._id); + expect(aLog.customer.type).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); + expect(aLog.customer.id).toBe(customer._id); + }); + + test(`createCompanyRegistrationLog`, async () => { + const customer = await customerFactory({}); + + const addCompanyDoc = { + _id: customer._id, + name: 'Reggina', + website: 'http://www.test.com', + }; + + const user = await userFactory({ role: ROLES.CONTRIBUTOR }); + + const company = await mutations.customersAddCompany(null, addCompanyDoc, { user }); + + expect(await ActivityLogs.find().count()).toBe(1); + const aLog = await ActivityLogs.findOne({}); + expect(aLog).toBeDefined(); + + expect(aLog.activity.type).toBe(CUSTOMER_CONTENT_TYPES.COMPANY); + expect(aLog.activity.id).toBe(company._id); + expect(aLog.customer.type).toBe(CUSTOMER_CONTENT_TYPES.COMPANY); + expect(aLog.customer.id).toBe(company._id); + }); + + test(`createInternalNote`, async () => { + const user = await userFactory({ role: ROLES.CONTRIBUTOR }); + const customer = await customerFactory({}); + + const internalNote = await mutations.internalNotesAdd( + null, + { + contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + contentTypeId: customer._id, + content: 'test string', + }, + { user }, + ); - await customerMutations.customersAdd(null, customerDoc, { user }); + expect(await ActivityLogs.find().count()).toBe(1); + const aLog = await ActivityLogs.findOne({}); + expect(aLog).toBeDefined(); - expect(await ActivityLogs.find({}).count()).toBe(1); + expect(aLog.activity.type).toBe(ACTIVITY_TYPES.INTERNAL_NOTE); + expect(aLog.activity.id).toBe(internalNote._id); + expect(aLog.customer.type).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); + expect(aLog.customer.id).toBe(customer._id); }); }); diff --git a/src/__tests__/customerMutations.test.js b/src/__tests__/customerMutations.test.js index 27157c748..a0b69f0e5 100644 --- a/src/__tests__/customerMutations.test.js +++ b/src/__tests__/customerMutations.test.js @@ -48,7 +48,12 @@ describe('Customers mutations', () => { }); test('Create customer', async () => { - Customers.createCustomer = jest.fn(); + Customers.createCustomer = jest.fn(() => { + return { + name: 'name', + _id: 'fakeCustomerId', + }; + }); const doc = { name: 'name', email: 'dombo@yahoo.com' }; @@ -72,7 +77,12 @@ describe('Customers mutations', () => { }); test('Add company', async () => { - Customers.addCompany = jest.fn(); + Customers.addCompany = jest.fn(() => { + return { + name: 'name', + _id: 'fakeCustomerId', + }; + }); const doc = { name: 'name', website: 'http://company.com' }; diff --git a/src/data/resolvers/mutations/customers.js b/src/data/resolvers/mutations/customers.js index deecab4d4..15f9b8198 100644 --- a/src/data/resolvers/mutations/customers.js +++ b/src/data/resolvers/mutations/customers.js @@ -31,7 +31,7 @@ const customerMutations = { */ async customersAddCompany(root, args, { user }) { const company = await Customers.addCompany(args); - await ActivityLogs.createCompanyRegistrationLog(company, { user }); + await ActivityLogs.createCompanyRegistrationLog(company, user); return company; }, }; diff --git a/src/data/resolvers/mutations/internalNotes.js b/src/data/resolvers/mutations/internalNotes.js index 3c7a358f6..c32adb0a8 100644 --- a/src/data/resolvers/mutations/internalNotes.js +++ b/src/data/resolvers/mutations/internalNotes.js @@ -1,4 +1,4 @@ -import { InternalNotes } from '../../../db/models'; +import { InternalNotes, ActivityLogs } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; const internalNoteMutations = { @@ -6,8 +6,10 @@ const internalNoteMutations = { * Adds internalNote object * @return {Promise} */ - internalNotesAdd(root, args, { user }) { - return InternalNotes.createInternalNote(args, user); + async internalNotesAdd(root, args, { user }) { + const internalNote = await InternalNotes.createInternalNote(args, user); + await ActivityLogs.createInternalNoteLog(internalNote, user); + return internalNote; }, /** diff --git a/src/db/factories.js b/src/db/factories.js index 519605880..5c4cf4df2 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -168,7 +168,7 @@ export const customerFactory = (params = {}) => { phone: params.phone || faker.random.word(), messengerData: params.messengerData || {}, customFieldsData: params.customFieldsData || {}, - companyIds: params.companyIds || null, + companyIds: params.companyIds || [], }); return customer.save(); diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 2863cae8d..46fdfbf0a 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -46,24 +46,26 @@ const ActionPerformer = mongoose.Schema( { _id: false }, ); -// The action that is being performed -// ex1: A user writes an internal note -// in this case: type is InternalNote -// action is create (write) -// id is the InternalNote id -// ex2: Sales manager registers a new customer -// in this case: type is customer -// action is create (register) -// id is Customer id -// customer and activity contentTypes are the same in this case -// ex3: Cronjob runs and a customer is found to be suitable for a particular segment -// action is create: a new segment user -// type is segment -// id is Segment id -// ex4: An internalNote concerning a customer was updated -// action is update -// type is InternalNote -// id is InternalNote id +/* + * The action that is being performed + * ex1: A user writes an internal note + * in this case: type is InternalNote + * action is create (write) + * id is the InternalNote id + * ex2: Sales manager registers a new customer + * in this case: type is customer + * action is create (register) + * id is Customer id + * customer and activity contentTypes are the same in this case + * ex3: Cronjob runs and a customer is found to be suitable for a particular segment + * action is create: a new segment user + * type is segment + * id is Segment id + * ex4: An internalNote concerning a customer was updated + * action is update + * type is InternalNote + * id is InternalNote id + */ const Activity = mongoose.Schema( { type: { @@ -184,33 +186,59 @@ class ActivityLog { if (customer.companyIds && customer.companyIds.length > 0) { for (let companyId of customer.companyIds) { - await this.createDoc({ - activity: { - type: ACTIVITY_TYPES.CONVERSATION, - action: ACTIVITY_ACTIONS.CREATE, - id: conversation._id, - }, - performedBy: user, - customer: { - type: CUSTOMER_CONTENT_TYPES.COMPANY, - id: companyId, - }, + // check against duplication + const foundLog = await this.findOne({ + 'activity.type': ACTIVITY_TYPES.CONVERSATION, + 'activity.action': ACTIVITY_ACTIONS.CREATE, + 'activity.id': conversation._id, + 'performedBy.type': ACTION_PERFORMER_TYPES.USER, + 'performedBy.id': user._id, + 'customer.type': CUSTOMER_CONTENT_TYPES.COMPANY, + 'customer.id': companyId, }); + + if (!foundLog) { + await this.createDoc({ + activity: { + type: ACTIVITY_TYPES.CONVERSATION, + action: ACTIVITY_ACTIONS.CREATE, + id: conversation._id, + }, + performedBy: user, + customer: { + type: CUSTOMER_CONTENT_TYPES.COMPANY, + id: companyId, + }, + }); + } } } - return this.createDoc({ - activity: { - type: ACTIVITY_TYPES.CONVERSATION, - action: ACTIVITY_ACTIONS.CREATE, - id: conversation._id, - }, - performedBy: user, - customer: { - type: CUSTOMER_CONTENT_TYPES.CUSTOMER, - id: customer._id, - }, + // check against duplication ====== + const foundLog = await this.findOne({ + 'activity.type': ACTIVITY_TYPES.CONVERSATION, + 'activity.action': ACTIVITY_ACTIONS.CREATE, + 'activity.id': conversation._id, + 'performedBy.type': ACTION_PERFORMER_TYPES.USER, + 'performedBy.id': user._id, + 'customer.type': CUSTOMER_CONTENT_TYPES.CUSTOMER, + 'customer.id': customer._id, }); + + if (!foundLog) { + return this.createDoc({ + activity: { + type: ACTIVITY_TYPES.CONVERSATION, + action: ACTIVITY_ACTIONS.CREATE, + id: conversation._id, + }, + performedBy: user, + customer: { + type: CUSTOMER_CONTENT_TYPES.CUSTOMER, + id: customer._id, + }, + }); + } } /** From aa6ba2c9f2ee528e140dae6163c8b6c555b97e30 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Thu, 9 Nov 2017 15:14:41 +0800 Subject: [PATCH 228/318] Refactor --- src/__tests__/internalNoteMutations.test.js | 6 ++- src/cronJobs/activityLogs.js | 4 +- src/data/resolvers/mutations/engageUtils.js | 2 +- src/data/resolvers/queries/companies.js | 2 +- src/data/resolvers/queries/customers.js | 2 +- src/{ => data}/segmentQueryBuilder.js | 0 src/db/models/ActivityLogs.js | 58 +++++++++++---------- 7 files changed, 40 insertions(+), 34 deletions(-) rename src/{ => data}/segmentQueryBuilder.js (100%) diff --git a/src/__tests__/internalNoteMutations.test.js b/src/__tests__/internalNoteMutations.test.js index 0a5b4b7f2..fe5ffffdf 100644 --- a/src/__tests__/internalNoteMutations.test.js +++ b/src/__tests__/internalNoteMutations.test.js @@ -58,7 +58,11 @@ describe('InternalNotes mutations', () => { }); test('Create internalNote', async () => { - InternalNotes.createInternalNote = jest.fn(); + InternalNotes.createInternalNote = jest.fn(() => ({ + _id: 'testInternalNoteId', + contentType: 'customer', + contentTypeId: 'customer', + })); await internalNoteMutations.internalNotesAdd({}, doc, { user: _user }); diff --git a/src/cronJobs/activityLogs.js b/src/cronJobs/activityLogs.js index 2ead05c88..c82942fb2 100644 --- a/src/cronJobs/activityLogs.js +++ b/src/cronJobs/activityLogs.js @@ -1,6 +1,6 @@ import schedule from 'node-schedule'; import { Segments, Customers, ActivityLogs } from '../db/models'; -import QueryBuilder from '../segmentQueryBuilder'; +import QueryBuilder from '../data/segmentQueryBuilder'; /** * Send conversation messages to customer @@ -35,7 +35,7 @@ export const createActivityLogsFromSegments = async () => { * │ └──────────────────── minute (0 - 59) * └───────────────────────── second (0 - 59, OPTIONAL) */ -schedule.scheduleJob('* * * * *', function() { +schedule.scheduleJob('* 45 23 * *', function() { createActivityLogsFromSegments(); }); diff --git a/src/data/resolvers/mutations/engageUtils.js b/src/data/resolvers/mutations/engageUtils.js index 31444b8e6..190e851ab 100644 --- a/src/data/resolvers/mutations/engageUtils.js +++ b/src/data/resolvers/mutations/engageUtils.js @@ -15,7 +15,7 @@ import { INTEGRATION_KIND_CHOICES, } from '../../constants'; import Random from 'meteor-random'; -import QueryBuilder from '../../../segmentQueryBuilder'; +import QueryBuilder from '../../segmentQueryBuilder'; import { createTransporter } from '../../utils'; /** diff --git a/src/data/resolvers/queries/companies.js b/src/data/resolvers/queries/companies.js index ae11258d9..1e4c0013a 100644 --- a/src/data/resolvers/queries/companies.js +++ b/src/data/resolvers/queries/companies.js @@ -1,5 +1,5 @@ import { Companies, Segments } from '../../../db/models'; -import QueryBuilder from '../../../segmentQueryBuilder'; +import QueryBuilder from '../../segmentQueryBuilder'; import { CUSTOMER_CONTENT_TYPES } from '../../constants'; import { moduleRequireLogin } from '../../permissions'; import { paginate } from './utils'; diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index 75a14a8ed..1bef412eb 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import { Brands, Tags, Integrations, Customers, Segments } from '../../../db/models'; import { TAG_TYPES, INTEGRATION_KIND_CHOICES, CUSTOMER_CONTENT_TYPES } from '../../constants'; -import QueryBuilder from '../../../segmentQueryBuilder.js'; +import QueryBuilder from '../../segmentQueryBuilder'; import { moduleRequireLogin } from '../../permissions'; import { paginate } from './utils'; import { CustomerMonthActivityLogBuilder } from '../../utils'; diff --git a/src/segmentQueryBuilder.js b/src/data/segmentQueryBuilder.js similarity index 100% rename from src/segmentQueryBuilder.js rename to src/data/segmentQueryBuilder.js diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 46fdfbf0a..bcd4b33ca 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -27,10 +27,10 @@ export const ACTION_PERFORMER_TYPES = { ALL: ['SYSTEM', 'USER'], }; -// Performer of the action: -// *system* cron job, user -// ex: Sales manager that has registered a new customer -// Sales manager is the action performer +/* Performer of the action: + *system* cron job, user + ex: Sales manager that has registered a new customer + Sales manager is the action performer */ const ActionPerformer = mongoose.Schema( { type: { @@ -47,24 +47,24 @@ const ActionPerformer = mongoose.Schema( ); /* - * The action that is being performed - * ex1: A user writes an internal note - * in this case: type is InternalNote - * action is create (write) - * id is the InternalNote id - * ex2: Sales manager registers a new customer - * in this case: type is customer - * action is create (register) - * id is Customer id - * customer and activity contentTypes are the same in this case - * ex3: Cronjob runs and a customer is found to be suitable for a particular segment - * action is create: a new segment user - * type is segment - * id is Segment id - * ex4: An internalNote concerning a customer was updated - * action is update - * type is InternalNote - * id is InternalNote id + The action that is being performed + ex1: A user writes an internal note + in this case: type is InternalNote + action is create (write) + id is the InternalNote id + ex2: Sales manager registers a new customer + in this case: type is customer + action is create (register) + id is Customer id + customer and activity contentTypes are the same in this case + ex3: Cronjob runs and a customer is found to be suitable for a particular segment + action is create: a new segment user + type is segment + id is Segment id + ex4: An internalNote concerning a customer was updated + action is update + type is InternalNote + id is InternalNote id */ const Activity = mongoose.Schema( { @@ -89,8 +89,8 @@ const Activity = mongoose.Schema( { _id: false }, ); -// the customer that is related to a given ActivityLog -// can be both Company or Customer documents +/* the customer that is related to a given ActivityLog + can be both Company or Customer documents */ const Customer = mongoose.Schema( { id: { @@ -125,6 +125,12 @@ const ActivityLogSchema = mongoose.Schema({ }); class ActivityLog { + /** + * Create an ActivityLog document + * @param {Object|null} object1.performedBy - The performer of the action + * @param {Object} object1 - Data to insert according to schema + * @return {Promise} returns Promise resolving created ActivityLog document + */ static createDoc({ performedBy, ...doc }) { if (performedBy && performedBy._id) { performedBy = { @@ -146,10 +152,6 @@ class ActivityLog { * @return {Promise} returns Promise resolving created ActivityLog document */ static createInternalNoteLog(internalNote, user) { - if (user == null || (user && !user._id)) { - throw new Error(`'user' must be supplied when adding activity log for internal note`); - } - return this.createDoc({ activity: { type: ACTIVITY_TYPES.INTERNAL_NOTE, From 729342cca847c60b3cfb9e73647e5aea73471dd7 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Fri, 10 Nov 2017 13:01:09 +0800 Subject: [PATCH 229/318] Change user password sha-256 --- package.json | 1 + src/__tests__/userDb.test.js | 10 +++++----- src/db/factories.js | 2 +- src/db/models/Users.js | 25 ++++++++++++++++++++----- yarn.lock | 15 +++++++++++++++ 5 files changed, 42 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 6fda99871..a9315d738 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "mongoose-type-email": "^1.0.5", "node-schedule": "^1.2.5", "nodemailer": "^4.1.3", + "sha256": "^0.2.0", "sinon": "^4.0.1", "social-oauth-client": "^0.1.6", "strip": "^3.0.0", diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index 61fb8ba7a..4a7eda693 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -171,7 +171,7 @@ describe('User db utils', () => { const user = await userFactory({}); try { - await Users.changePassword({ _id: user._id, currentPassword: 'p' }); + await Users.changePassword({ _id: user._id, currentPassword: 'admin' }); } catch (e) { expect(e.message).toBe('Incorrect current password'); } @@ -182,11 +182,11 @@ describe('User db utils', () => { const updatedUser = await Users.changePassword({ _id: user._id, - currentPassword: 'Dombo@123', + currentPassword: 'pass', newPassword: 'Lombo@123', }); - expect(bcrypt.compare(updatedUser.password, 'Lombo@123')).toBeTruthy(); + expect(await Users.comparePassword('Lombo@123', updatedUser.password)).toBeTruthy(); }); test('Forgot password', async () => { @@ -219,7 +219,7 @@ describe('User db utils', () => { // invalid password ============== try { - await Users.login({ email: _user.email, password: 'pass' }); + await Users.login({ email: _user.email, password: 'admin' }); } catch (e) { expect(e.message).toBe('Invalid login'); } @@ -227,7 +227,7 @@ describe('User db utils', () => { // valid const { token, refreshToken } = await Users.login({ email: _user.email, - password: 'Dombo@123', + password: 'pass', }); expect(token).toBeDefined(); diff --git a/src/db/factories.js b/src/db/factories.js index 5ec3e2e7b..b41ccb906 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -37,7 +37,7 @@ export const userFactory = (params = {}) => { }, email: params.email || faker.internet.email(), role: params.role || 'contributor', - password: params.password || '$2a$12$eStwXbJ03luTm2826cbkWu57PnUA4Whk.KVOClc1P2kqcZTtsMK/i', + password: params.password || '$2a$10$qfBFBmWmUjeRcR.nBBfgDO/BEbxgoai5qQhyjsrDUMiZC6dG7sg1q', isOwner: params.isOwner || false, }); diff --git a/src/db/models/Users.js b/src/db/models/Users.js index c911939ff..2715fbd27 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -2,6 +2,7 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; import bcrypt from 'bcrypt'; import crypto from 'crypto'; +import sha256 from 'sha256'; import jwt from 'jsonwebtoken'; import { ROLES } from '../../data/constants'; @@ -163,7 +164,21 @@ class User { * @return hashed password */ static generatePassword(password) { - return bcrypt.hash(password, SALT_WORK_FACTOR); + const hashPassword = sha256(password); + + return bcrypt.hash(hashPassword, SALT_WORK_FACTOR); + } + + /* + Compare password + @param {String} password + @param {String} userPassword - Current password + return {Boolean} is valid + */ + static comparePassword(password, userPassword) { + const hashPassword = sha256(password); + + return bcrypt.compare(hashPassword, userPassword); } /* @@ -193,7 +208,7 @@ class User { await this.findByIdAndUpdate( { _id: user._id }, { - password: bcrypt.hashSync(newPassword, 10), + password: await this.generatePassword(newPassword), resetPasswordToken: undefined, resetPasswordExpires: undefined, }, @@ -212,7 +227,7 @@ class User { const user = await this.findOne({ _id }); // check current password ============ - const valid = await bcrypt.compare(currentPassword, user.password); + const valid = await this.comparePassword(currentPassword, user.password); if (!valid) { throw new Error('Incorrect current password'); @@ -222,7 +237,7 @@ class User { await this.findByIdAndUpdate( { _id: user._id }, { - password: bcrypt.hashSync(newPassword, 10), + password: await this.generatePassword(newPassword), }, ); @@ -320,7 +335,7 @@ class User { throw new Error('Invalid login'); } - const valid = await bcrypt.compare(password, user.password); + const valid = await this.comparePassword(password, user.password); if (!valid) { // bad password diff --git a/yarn.lock b/yarn.lock index e38987c20..8e622acb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1179,10 +1179,18 @@ content-type@~1.0.2, content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" +convert-hex@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/convert-hex/-/convert-hex-0.1.0.tgz#08c04568922c27776b8a2e81a95d393362ea0b65" + convert-source-map@^1.4.0, convert-source-map@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" +convert-string@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/convert-string/-/convert-string-0.1.0.tgz#79ce41a9bb0d03bcf72cdc6a8f3c56fbbc64410a" + cookie-parser@^1.4.0: version "1.4.3" resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.3.tgz#0fe31fa19d000b95f4aadf1f53fdc2b8a203baa5" @@ -4490,6 +4498,13 @@ setprototypeof@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" +sha256@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/sha256/-/sha256-0.2.0.tgz#73a0b418daab7035bff86e8491e363412fc2ab05" + dependencies: + convert-hex "~0.1.0" + convert-string "~0.1.0" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" From 8e9136cef41afe66cb0fbe64bcaade224ed3c0ba Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 10 Nov 2017 17:30:55 +0800 Subject: [PATCH 230/318] change CUSTOMER_CONTENT_TYPES -> COC_TYPES, change ActivityLogs.customer -> ActivityLogs.coc --- src/__tests__/activityLogCronJob.test.js | 10 ++--- src/__tests__/activityLogDb.test.js | 44 +++++++++++----------- src/__tests__/activityLogMutations.test.js | 20 +++++----- src/cronJobs/activityLogs.js | 4 -- src/data/constants.js | 2 +- src/data/resolvers/queries/companies.js | 4 +- src/data/resolvers/queries/customers.js | 14 +------ src/data/utils.js | 6 +-- src/db/factories.js | 6 +-- src/db/models/ActivityLogs.js | 40 ++++++++++---------- src/db/models/InternalNotes.js | 4 +- src/db/models/Segments.js | 4 +- 12 files changed, 72 insertions(+), 86 deletions(-) diff --git a/src/__tests__/activityLogCronJob.test.js b/src/__tests__/activityLogCronJob.test.js index 01ee875e9..0399ed04e 100644 --- a/src/__tests__/activityLogCronJob.test.js +++ b/src/__tests__/activityLogCronJob.test.js @@ -3,7 +3,7 @@ import { connect, disconnect } from '../db/connection'; import cronJobs from '../cronJobs'; -import { CUSTOMER_CONTENT_TYPES } from '../data/constants'; +import { COC_CONTENT_TYPES } from '../data/constants'; import { customerFactory, segmentFactory } from '../db/factories'; import ActivityLogs, { ACTIVITY_TYPES, @@ -29,7 +29,7 @@ describe('test activityLogsCronJob', () => { const customer = await customerFactory({ name: 'john smith' }); const segment = await segmentFactory({ - contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + contentType: COC_CONTENT_TYPES.CUSTOMER, conditions: nameEqualsConditions, }); @@ -47,8 +47,8 @@ describe('test activityLogsCronJob', () => { }, id: segment._id, }); - expect(aLog.customer.toObject()).toEqual({ - type: CUSTOMER_CONTENT_TYPES.CUSTOMER, + expect(aLog.coc.toObject()).toEqual({ + type: COC_CONTENT_TYPES.CUSTOMER, id: customer._id, }); expect(aLog.performedBy.toObject()).toEqual({ @@ -70,7 +70,7 @@ describe('test activityLogsCronJob', () => { await customerFactory({ name: 'jane smith' }); await segmentFactory({ - contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + contentType: COC_CONTENT_TYPES.CUSTOMER, conditions: nameEqualsConditions2, }); diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index 8e73b183e..befa68e2d 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -2,7 +2,7 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { CUSTOMER_CONTENT_TYPES } from '../data/constants'; +import { COC_CONTENT_TYPES } from '../data/constants'; import { ActivityLogs, Conversations } from '../db/models'; import { ACTION_PERFORMER_TYPES, @@ -36,20 +36,20 @@ describe('ActivityLogs model methods', () => { }; const customerDoc = { - type: CUSTOMER_CONTENT_TYPES.CUSTOMER, + type: COC_CONTENT_TYPES.CUSTOMER, id: 'testCustomerId', }; const doc = { activity: activityDoc, - customer: customerDoc, + coc: customerDoc, performedBy: null, }; const aLog = await ActivityLogs.createDoc(doc); expect(aLog.activity.toObject()).toEqual(activityDoc); - expect(aLog.customer.toObject()).toEqual(customerDoc); + expect(aLog.coc.toObject()).toEqual(customerDoc); expect(aLog.performedBy.type).toBe(ACTION_PERFORMER_TYPES.SYSTEM); }); @@ -58,7 +58,7 @@ describe('ActivityLogs model methods', () => { const customer = await customerFactory(); const internalNote = await internalNoteFactory({ - contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + contentType: COC_CONTENT_TYPES.CUSTOMER, contentTypeId: customer._id, }); @@ -75,7 +75,7 @@ describe('ActivityLogs model methods', () => { const customer = await customerFactory(); const internalNote = await internalNoteFactory({ - contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + contentType: COC_CONTENT_TYPES.CUSTOMER, contentTypeId: customer, }); @@ -83,8 +83,8 @@ describe('ActivityLogs model methods', () => { expect(aLog.performedBy.type).toBe(ACTION_PERFORMER_TYPES.USER); expect(aLog.performedBy.id).toBe(user._id); - expect(aLog.customer.type).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); - expect(aLog.customer.id).toBe(internalNote.contentTypeId); + expect(aLog.coc.type).toBe(COC_CONTENT_TYPES.CUSTOMER); + expect(aLog.coc.id).toBe(internalNote.contentTypeId); expect(aLog.activity.toObject()).toEqual({ type: ACTIVITY_TYPES.INTERNAL_NOTE, action: ACTIVITY_ACTIONS.CREATE, @@ -119,7 +119,7 @@ describe('ActivityLogs model methods', () => { const customer = await customerFactory({ name: 'john smith' }); const segment = await segmentFactory({ - contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + contentType: COC_CONTENT_TYPES.CUSTOMER, conditions: nameEqualsConditions, }); @@ -133,7 +133,7 @@ describe('ActivityLogs model methods', () => { }, id: segment._id, }); - expect(segmentLog.customer.toObject()).toEqual({ + expect(segmentLog.coc.toObject()).toEqual({ type: segment.contentType, id: customer._id, }); @@ -185,8 +185,8 @@ describe('ActivityLogs model methods', () => { type: ACTION_PERFORMER_TYPES.USER, id: user._id, }); - expect(aLog.customer.toObject()).toEqual({ - type: CUSTOMER_CONTENT_TYPES.CUSTOMER, + expect(aLog.coc.toObject()).toEqual({ + type: COC_CONTENT_TYPES.CUSTOMER, id: customer._id, }); expect(aLog.activity.toObject()).toEqual({ @@ -202,12 +202,12 @@ describe('ActivityLogs model methods', () => { 'activity.id': conversation._id, 'performedBy.type': ACTION_PERFORMER_TYPES.USER, 'performedBy.id': user._id, - 'customer.type': CUSTOMER_CONTENT_TYPES.COMPANY, - 'customer.id': companyA._id, + 'coc.type': COC_CONTENT_TYPES.COMPANY, + 'coc.id': companyA._id, }); expect(aLog).toBeDefined(); - expect(aLog.customer.id).toBe(companyA._id); + expect(aLog.coc.id).toBe(companyA._id); aLog = await ActivityLogs.findOne({ 'activity.type': ACTIVITY_TYPES.CONVERSATION, @@ -215,12 +215,12 @@ describe('ActivityLogs model methods', () => { 'activity.id': conversation._id, 'performedBy.type': ACTION_PERFORMER_TYPES.USER, 'performedBy.id': user._id, - 'customer.type': CUSTOMER_CONTENT_TYPES.COMPANY, - 'customer.id': companyB._id, + 'coc.type': COC_CONTENT_TYPES.COMPANY, + 'coc.id': companyB._id, }); expect(aLog).toBeDefined(); - expect(aLog.customer.id).toBe(companyB._id); + expect(aLog.coc.id).toBe(companyB._id); expect(await ActivityLogs.find({}).count()).toBe(3); @@ -248,8 +248,8 @@ describe('ActivityLogs model methods', () => { }, id: customer._id, }); - expect(aLog.customer.toObject()).toEqual({ - type: CUSTOMER_CONTENT_TYPES.CUSTOMER, + expect(aLog.coc.toObject()).toEqual({ + type: COC_CONTENT_TYPES.CUSTOMER, id: customer._id, }); }); @@ -272,8 +272,8 @@ describe('ActivityLogs model methods', () => { }, id: company._id, }); - expect(aLog.customer.toObject()).toEqual({ - type: CUSTOMER_CONTENT_TYPES.COMPANY, + expect(aLog.coc.toObject()).toEqual({ + type: COC_CONTENT_TYPES.COMPANY, id: company._id, }); }); diff --git a/src/__tests__/activityLogMutations.test.js b/src/__tests__/activityLogMutations.test.js index 55d388a0c..2394bee4d 100644 --- a/src/__tests__/activityLogMutations.test.js +++ b/src/__tests__/activityLogMutations.test.js @@ -6,7 +6,7 @@ import mutations from '../data/resolvers/mutations'; import { ROLES } from '../data/constants'; import ActivityLogs, { ACTIVITY_TYPES } from '../db/models/ActivityLogs'; import { userFactory, customerFactory } from '../db/factories'; -import { CUSTOMER_CONTENT_TYPES } from '../data/constants'; +import { COC_CONTENT_TYPES } from '../data/constants'; beforeAll(() => connect()); afterAll(() => disconnect()); @@ -29,10 +29,10 @@ describe('ActivityLog creation on Customer creation', () => { const aLog = await ActivityLogs.findOne({}); expect(aLog).toBeDefined(); - expect(aLog.activity.type).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); + expect(aLog.activity.type).toBe(COC_CONTENT_TYPES.CUSTOMER); expect(aLog.activity.id).toBe(customer._id); - expect(aLog.customer.type).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); - expect(aLog.customer.id).toBe(customer._id); + expect(aLog.coc.type).toBe(COC_CONTENT_TYPES.CUSTOMER); + expect(aLog.coc.id).toBe(customer._id); }); test(`createCompanyRegistrationLog`, async () => { @@ -52,10 +52,10 @@ describe('ActivityLog creation on Customer creation', () => { const aLog = await ActivityLogs.findOne({}); expect(aLog).toBeDefined(); - expect(aLog.activity.type).toBe(CUSTOMER_CONTENT_TYPES.COMPANY); + expect(aLog.activity.type).toBe(COC_CONTENT_TYPES.COMPANY); expect(aLog.activity.id).toBe(company._id); - expect(aLog.customer.type).toBe(CUSTOMER_CONTENT_TYPES.COMPANY); - expect(aLog.customer.id).toBe(company._id); + expect(aLog.coc.type).toBe(COC_CONTENT_TYPES.COMPANY); + expect(aLog.coc.id).toBe(company._id); }); test(`createInternalNote`, async () => { @@ -65,7 +65,7 @@ describe('ActivityLog creation on Customer creation', () => { const internalNote = await mutations.internalNotesAdd( null, { - contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + contentType: COC_CONTENT_TYPES.CUSTOMER, contentTypeId: customer._id, content: 'test string', }, @@ -78,7 +78,7 @@ describe('ActivityLog creation on Customer creation', () => { expect(aLog.activity.type).toBe(ACTIVITY_TYPES.INTERNAL_NOTE); expect(aLog.activity.id).toBe(internalNote._id); - expect(aLog.customer.type).toBe(CUSTOMER_CONTENT_TYPES.CUSTOMER); - expect(aLog.customer.id).toBe(customer._id); + expect(aLog.coc.type).toBe(COC_CONTENT_TYPES.CUSTOMER); + expect(aLog.coc.id).toBe(customer._id); }); }); diff --git a/src/cronJobs/activityLogs.js b/src/cronJobs/activityLogs.js index c82942fb2..de2cb3d9e 100644 --- a/src/cronJobs/activityLogs.js +++ b/src/cronJobs/activityLogs.js @@ -10,14 +10,10 @@ export const createActivityLogsFromSegments = async () => { for (let segment of segments) { const selector = await QueryBuilder.segments(segment); - // console.log('segment: ', segment); - // console.log('selector: ', selector['$and'] && selector['$and'][0]['$or']); const customers = await Customers.find(selector); if (segment.contentType) { for (let customer of customers) { - // console.log('customer: ', customer); - await ActivityLogs.createSegmentLog(segment, customer); } } diff --git a/src/data/constants.js b/src/data/constants.js index 3df029238..3ea39f018 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -170,7 +170,7 @@ export const FIELD_CONTENT_TYPES = { ALL: ['form', 'customer', 'company'], }; -export const CUSTOMER_CONTENT_TYPES = { +export const COC_CONTENT_TYPES = { CUSTOMER: 'customer', COMPANY: 'company', ALL: ['customer', 'company'], diff --git a/src/data/resolvers/queries/companies.js b/src/data/resolvers/queries/companies.js index 1e4c0013a..f97c5b957 100644 --- a/src/data/resolvers/queries/companies.js +++ b/src/data/resolvers/queries/companies.js @@ -1,6 +1,6 @@ import { Companies, Segments } from '../../../db/models'; import QueryBuilder from '../../segmentQueryBuilder'; -import { CUSTOMER_CONTENT_TYPES } from '../../constants'; +import { COC_CONTENT_TYPES } from '../../constants'; import { moduleRequireLogin } from '../../permissions'; import { paginate } from './utils'; @@ -54,7 +54,7 @@ const companyQueries = { // Count companies by segments const segments = await Segments.find({ - contentType: CUSTOMER_CONTENT_TYPES.COMPANY, + contentType: COC_CONTENT_TYPES.COMPANY, }); for (let s of segments) { diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index 1bef412eb..0666ab356 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -1,6 +1,6 @@ import _ from 'underscore'; import { Brands, Tags, Integrations, Customers, Segments } from '../../../db/models'; -import { TAG_TYPES, INTEGRATION_KIND_CHOICES, CUSTOMER_CONTENT_TYPES } from '../../constants'; +import { TAG_TYPES, INTEGRATION_KIND_CHOICES, COC_CONTENT_TYPES } from '../../constants'; import QueryBuilder from '../../segmentQueryBuilder'; import { moduleRequireLogin } from '../../permissions'; import { paginate } from './utils'; @@ -85,7 +85,7 @@ const customerQueries = { // Count customers by segments const segments = await Segments.find({ - contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER, + contentType: COC_CONTENT_TYPES.CUSTOMER, }); for (let s of segments) { @@ -162,16 +162,6 @@ const customerQueries = { const m = new CustomerMonthActivityLogBuilder(customer); return m.build(); - // const cursor = ActivityLogs.find({ - // 'customer.type': CUSTOMER_CONTENT_TYPES.customer, - // 'customer.id': _id, - // }); - // - // if (sortDoc) { - // cursor.sort(sortDoc); - // } - - // return customerActivityLog; }, }; diff --git a/src/data/utils.js b/src/data/utils.js index 35a46c6a5..80e4c8a4e 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -2,7 +2,7 @@ import fs from 'fs'; import nodemailer from 'nodemailer'; import Handlebars from 'handlebars'; import { Notifications, Users } from '../db/models'; -import { CUSTOMER_CONTENT_TYPES } from './constants'; +import { COC_CONTENT_TYPES } from './constants'; /** * Read contents of a file @@ -209,14 +209,14 @@ class BaseMonthActivityBuilder { export class CustomerMonthActivityLogBuilder extends BaseMonthActivityBuilder { constructor(customer) { super(customer); - this.customerType = CUSTOMER_CONTENT_TYPES.CUSTOMER; + this.customerType = COC_CONTENT_TYPES.CUSTOMER; } } export class CompanyMonthActivityLogBuilder extends BaseMonthActivityBuilder { constructor(customer) { super(customer); - this.customerType = CUSTOMER_CONTENT_TYPES.COMPANY; + this.customerType = COC_CONTENT_TYPES.COMPANY; } } diff --git a/src/db/factories.js b/src/db/factories.js index 5c4cf4df2..121b489bf 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -1,6 +1,6 @@ import faker from 'faker'; import Random from 'meteor-random'; -import { MODULES, CUSTOMER_CONTENT_TYPES } from '../data/constants'; +import { MODULES, COC_CONTENT_TYPES } from '../data/constants'; import { Users, @@ -128,7 +128,7 @@ export const segmentFactory = (params = {}) => { ]; const segment = new Segments({ - contentType: CUSTOMER_CONTENT_TYPES.CUSTOMER || params.contentType, + contentType: COC_CONTENT_TYPES.CUSTOMER || params.contentType, name: faker.random.word(), description: params.description || faker.random.word(), subOf: params.subOf || 'DFSAFDFDSFDSF', @@ -142,7 +142,7 @@ export const segmentFactory = (params = {}) => { export const internalNoteFactory = (params = {}) => { const internalNote = new InternalNotes({ - contentType: params.contentType || CUSTOMER_CONTENT_TYPES.CUSTOMER, + contentType: params.contentType || COC_CONTENT_TYPES.CUSTOMER, contentTypeId: params.contentTypeId || 'DFASFDFSDAFDF', content: params.content || faker.random.word(), }); diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index bcd4b33ca..bfa6792d1 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -1,6 +1,6 @@ import mongoose, { SchemaTypes } from 'mongoose'; import Random from 'meteor-random'; -import { CUSTOMER_CONTENT_TYPES } from '../../data/constants'; +import { COC_CONTENT_TYPES } from '../../data/constants'; export const ACTIVITY_TYPES = { CUSTOMER: 'customer', @@ -99,7 +99,7 @@ const Customer = mongoose.Schema( }, type: { type: String, - enum: CUSTOMER_CONTENT_TYPES.ALL, + enum: COC_CONTENT_TYPES.ALL, required: true, }, }, @@ -115,7 +115,7 @@ const ActivityLogSchema = mongoose.Schema({ activity: Activity, performedBy: ActionPerformer, - customer: Customer, + coc: Customer, createdAt: { type: Date, @@ -159,7 +159,7 @@ class ActivityLog { id: internalNote._id, }, performedBy: user, - customer: { + coc: { id: internalNote.contentTypeId, type: internalNote.contentType, }, @@ -174,7 +174,7 @@ class ActivityLog { * @param {Object} user - User object * @param {Object} user._id - User document id * @param {Object} customer - Customer object - * @param {string} customer.type - One of CUSTOMER_CONTENT_TYPES choices + * @param {string} customer.type - One of COC_CONTENT_TYPES choices * @param {string} customer.id - Customer document id */ static async createConversationLog(conversation, user, customer) { @@ -195,8 +195,8 @@ class ActivityLog { 'activity.id': conversation._id, 'performedBy.type': ACTION_PERFORMER_TYPES.USER, 'performedBy.id': user._id, - 'customer.type': CUSTOMER_CONTENT_TYPES.COMPANY, - 'customer.id': companyId, + 'coc.type': COC_CONTENT_TYPES.COMPANY, + 'coc.id': companyId, }); if (!foundLog) { @@ -207,8 +207,8 @@ class ActivityLog { id: conversation._id, }, performedBy: user, - customer: { - type: CUSTOMER_CONTENT_TYPES.COMPANY, + coc: { + type: COC_CONTENT_TYPES.COMPANY, id: companyId, }, }); @@ -223,8 +223,8 @@ class ActivityLog { 'activity.id': conversation._id, 'performedBy.type': ACTION_PERFORMER_TYPES.USER, 'performedBy.id': user._id, - 'customer.type': CUSTOMER_CONTENT_TYPES.CUSTOMER, - 'customer.id': customer._id, + 'coc.type': COC_CONTENT_TYPES.CUSTOMER, + 'coc.id': customer._id, }); if (!foundLog) { @@ -235,8 +235,8 @@ class ActivityLog { id: conversation._id, }, performedBy: user, - customer: { - type: CUSTOMER_CONTENT_TYPES.CUSTOMER, + coc: { + type: COC_CONTENT_TYPES.CUSTOMER, id: customer._id, }, }); @@ -258,8 +258,8 @@ class ActivityLog { 'activity.type': ACTIVITY_TYPES.SEGMENT, 'activity.action': ACTIVITY_ACTIONS.CREATE, 'activity.id': segment._id, - 'customer.type': segment.contentType, - 'customer.id': customer._id, + 'coc.type': segment.contentType, + 'coc.id': customer._id, }); if (foundSegment) { @@ -276,7 +276,7 @@ class ActivityLog { }, id: segment._id, }, - customer: { + coc: { type: segment.contentType, id: customer._id, }, @@ -299,8 +299,8 @@ class ActivityLog { }, id: customer._id, }, - customer: { - type: CUSTOMER_CONTENT_TYPES.CUSTOMER, + coc: { + type: COC_CONTENT_TYPES.CUSTOMER, id: customer._id, }, performedBy: user, @@ -323,8 +323,8 @@ class ActivityLog { }, id: company._id, }, - customer: { - type: CUSTOMER_CONTENT_TYPES.COMPANY, + coc: { + type: COC_CONTENT_TYPES.COMPANY, id: company._id, }, performedBy: user, diff --git a/src/db/models/InternalNotes.js b/src/db/models/InternalNotes.js index bc10cf652..b15705931 100644 --- a/src/db/models/InternalNotes.js +++ b/src/db/models/InternalNotes.js @@ -1,6 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -import { CUSTOMER_CONTENT_TYPES } from '../../data/constants'; +import { COC_CONTENT_TYPES } from '../../data/constants'; /* * internal note schema @@ -13,7 +13,7 @@ const InternalNoteSchema = mongoose.Schema({ }, contentType: { type: String, - enum: CUSTOMER_CONTENT_TYPES.ALL, + enum: COC_CONTENT_TYPES.ALL, }, contentTypeId: String, content: { diff --git a/src/db/models/Segments.js b/src/db/models/Segments.js index c3aa27d6f..6282c659e 100644 --- a/src/db/models/Segments.js +++ b/src/db/models/Segments.js @@ -1,6 +1,6 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; -import { CUSTOMER_CONTENT_TYPES } from '../../data/constants'; +import { COC_CONTENT_TYPES } from '../../data/constants'; const ConditionSchema = mongoose.Schema( { @@ -29,7 +29,7 @@ const SegmentSchema = mongoose.Schema({ }, contentType: { type: String, - enum: CUSTOMER_CONTENT_TYPES.ALL, + enum: COC_CONTENT_TYPES.ALL, }, name: String, description: String, From 5ddd37081f863fefce7bd1de9e59cfe3d66c19d3 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Fri, 10 Nov 2017 18:05:31 +0800 Subject: [PATCH 231/318] #22 Add companyLog query --- src/cronJobs/activityLogs.js | 6 ++---- src/data/resolvers/queries/companies.js | 15 +++++++++++++++ src/data/resolvers/queries/customers.js | 1 - src/data/schema/company.js | 1 + 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/cronJobs/activityLogs.js b/src/cronJobs/activityLogs.js index de2cb3d9e..7c0721482 100644 --- a/src/cronJobs/activityLogs.js +++ b/src/cronJobs/activityLogs.js @@ -12,10 +12,8 @@ export const createActivityLogsFromSegments = async () => { const selector = await QueryBuilder.segments(segment); const customers = await Customers.find(selector); - if (segment.contentType) { - for (let customer of customers) { - await ActivityLogs.createSegmentLog(segment, customer); - } + for (let customer of customers) { + await ActivityLogs.createSegmentLog(segment, customer); } } }; diff --git a/src/data/resolvers/queries/companies.js b/src/data/resolvers/queries/companies.js index f97c5b957..56d562bc4 100644 --- a/src/data/resolvers/queries/companies.js +++ b/src/data/resolvers/queries/companies.js @@ -3,6 +3,7 @@ import QueryBuilder from '../../segmentQueryBuilder'; import { COC_CONTENT_TYPES } from '../../constants'; import { moduleRequireLogin } from '../../permissions'; import { paginate } from './utils'; +import { CompanyMonthActivityLogBuilder } from '../../utils'; const listQuery = async params => { const selector = {}; @@ -73,6 +74,20 @@ const companyQueries = { companyDetail(root, { _id }) { return Companies.findOne({ _id }); }, + + /** + * Get activity log for company + * @param {Object} root + * @param {Object} object2 - Graphql input data + * @param {string} object._id - Company id + * @return {Promise} Promise resolving array of ActivityLogForMonth + */ + async companyActivityLog(root, { _id }) { + const company = await Companies.findOne({ _id }); + + const m = new CompanyMonthActivityLogBuilder(company); + return m.build(); + }, }; moduleRequireLogin(companyQueries); diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index 0666ab356..fba8787f8 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -154,7 +154,6 @@ const customerQueries = { * @param {Object} root * @param {Object} object2 - Graphql input data * @param {string} object._id - Customer id - * @param {string} object.sortDoc - Graphql ActivityLogSort doc object * @return {Promise} found customer */ async customerActivityLog(root, { _id }) { diff --git a/src/data/schema/company.js b/src/data/schema/company.js index 794ec2621..8a89b6265 100644 --- a/src/data/schema/company.js +++ b/src/data/schema/company.js @@ -27,6 +27,7 @@ export const queries = ` companies(params: CompanyListParams): [Company] companyCounts(params: CompanyListParams): JSON companyDetail(_id: String!): Company + companyActivityLog(_id: String!): [ActivityLogForMonth] `; const commonFields = ` From 4ac332d10f372eeb9a7481a071927c0447a2530f Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 10 Nov 2017 19:44:58 +0800 Subject: [PATCH 232/318] Update conversation message add mutation params --- src/data/resolvers/queries/conversations.js | 12 +++--------- src/data/schema/conversation.js | 11 +++++++++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/data/resolvers/queries/conversations.js b/src/data/resolvers/queries/conversations.js index 7fabd7ed1..8d93ccaa1 100644 --- a/src/data/resolvers/queries/conversations.js +++ b/src/data/resolvers/queries/conversations.js @@ -168,17 +168,11 @@ const conversationQueries = { }, /** - * Get conversation by given id or return last conversation + * Get last conversation * @return {Promise} - Conversation object */ - async conversationsGetCurrent(root, { _id }) { - let conversation = await Conversations.findOne({ _id }); - - if (!conversation) { - conversation = Conversations.findOne({}).sort({ createdAt: -1 }); - } - - return conversation; + async conversationsGetLast() { + return Conversations.findOne({}).sort({ createdAt: -1 }); }, }; diff --git a/src/data/schema/conversation.js b/src/data/schema/conversation.js index 6ae3427c1..da1bc27c3 100644 --- a/src/data/schema/conversation.js +++ b/src/data/schema/conversation.js @@ -86,11 +86,18 @@ export const queries = ` conversationCounts(params: ConversationListParams): JSON conversationDetail(_id: String!): Conversation conversationsTotalCount(params: ConversationListParams): Int - conversationsGetCurrent(_id: String): Conversation + conversationsGetLast: Conversation `; export const mutations = ` - conversationMessageAdd(params: ConversationMessageParams): ConversationMessage + conversationMessageAdd( + conversationId: String, + content: String, + mentionedUserIds: [String], + internal: Boolean, + attachments: [String], + ): ConversationMessage + conversationsAssign(conversationIds: [String]!, assignedUserId: String): [Conversation] conversationsUnassign(_ids: [String]!): [Conversation] conversationsChangeStatus(_ids: [String]!): [Conversation] From 93936a727d63e88c9aba6742f0cb9393a62516d4 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 10 Nov 2017 20:02:32 +0800 Subject: [PATCH 233/318] Add subscription in create internal note --- src/data/resolvers/mutations/conversations.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 9e7406c83..fb5e5d476 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -96,6 +96,9 @@ const conversationMutations = { // do not send internal message to third service integrations if (doc.internal) { + // notify subscription + await conversationMessageCreated(message, doc.conversationId); + return message; } From cbecb6236e12c5e07af2d382fae7d4dc02bf2b3c Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 10 Nov 2017 20:29:47 +0800 Subject: [PATCH 234/318] Change conversationChangeStatus mutation return value --- src/data/resolvers/mutations/conversations.js | 4 ++-- src/data/schema/conversation.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index fb5e5d476..e6b620c37 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -198,7 +198,7 @@ const conversationMutations = { async conversationsChangeStatus(root, { _ids, status }, { user }) { const { conversations } = await Conversations.checkExistanceConversations(_ids); - const changedConversations = await Conversations.changeStatusConversation(_ids, status); + await Conversations.changeStatusConversation(_ids, status); // notify graphl subscription await conversationsChanged(_ids, 'statusChanged'); @@ -241,7 +241,7 @@ const conversationMutations = { }); } - return changedConversations; + return Conversations.find({ _id: { $in: _ids } }); }, /** diff --git a/src/data/schema/conversation.js b/src/data/schema/conversation.js index da1bc27c3..59ac6c803 100644 --- a/src/data/schema/conversation.js +++ b/src/data/schema/conversation.js @@ -100,7 +100,7 @@ export const mutations = ` conversationsAssign(conversationIds: [String]!, assignedUserId: String): [Conversation] conversationsUnassign(_ids: [String]!): [Conversation] - conversationsChangeStatus(_ids: [String]!): [Conversation] + conversationsChangeStatus(_ids: [String]!, status: String!): [Conversation] conversationsStar(_ids: [String]!): User conversationsUnstar(_ids: [String]!): User conversationsToggleParticipate(_ids: [String]!): Conversation From 82ef185e03cf4546c33e4de4d16aba68ca5e0e54 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 10 Nov 2017 23:20:17 +0800 Subject: [PATCH 235/318] Call subscription when new twitter entry --- src/data/resolvers/mutations/conversations.js | 2 +- src/social/twitter.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index e6b620c37..5b473dd5e 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -59,7 +59,7 @@ const conversationsChanged = async (_ids, type) => { * @param {Object} message object * @param {String} conversationId */ -const conversationMessageCreated = async (message, conversationId) => { +export const conversationMessageCreated = async (message, conversationId) => { // subscribe pubsub.publish('conversationMessageInserted', { conversationMessageInserted: message, diff --git a/src/social/twitter.js b/src/social/twitter.js index b27f9ffb7..7d9693e82 100755 --- a/src/social/twitter.js +++ b/src/social/twitter.js @@ -1,5 +1,6 @@ import { Customers, ConversationMessages, Conversations, Integrations } from '../db/models'; import { CONVERSATION_STATUSES } from '../data/constants'; +import { conversationMessageCreated } from '../data/resolvers/mutations/conversations'; /* * Get or create customer using twitter data @@ -49,7 +50,10 @@ const createMessage = async (conversation, content, user) => { internal: false, }); - // TODO notify subscription server new message + // notify subscription ========= + const message = await ConversationMessages.findOne({ _id: messageId }); + + await conversationMessageCreated(message, message.conversationId); return messageId; }; From 922ce3af971049b1aafebdb2384576238bddbcac Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 10 Nov 2017 23:28:25 +0800 Subject: [PATCH 236/318] Add twitterReply --- src/data/resolvers/mutations/conversations.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 5b473dd5e..8d42897b9 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -1,10 +1,12 @@ +import { _ } from 'underscore'; +import strip from 'strip'; import { Conversations, ConversationMessages, Integrations, Customers } from '../../../db/models'; +import { tweetReply } from '../../../social/twitter'; import { NOTIFICATION_TYPES } from '../../constants'; -import { pubsub } from '../subscriptions'; import { CONVERSATION_STATUSES, KIND_CHOICES } from '../../constants'; -import utils from '../../utils'; -import { _ } from 'underscore'; import { moduleRequireLogin } from '../../permissions'; +import utils from '../../utils'; +import { pubsub } from '../subscriptions'; /** * conversation notrification receiver ids @@ -112,7 +114,8 @@ const conversationMutations = { // send reply to twitter if (kind === KIND_CHOICES.TWITTER) { - // TODO: return tweetReply(conversation, strip(content)); + tweetReply(conversation, strip(doc.content)); + return message; } const customer = await Customers.findOne({ _id: conversation.customerId }); From 30d3e6c8155f12792d9d6de32153498d252974b8 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 10 Nov 2017 23:48:33 +0800 Subject: [PATCH 237/318] Use createConversation when twitter new message --- src/social/twitter.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/social/twitter.js b/src/social/twitter.js index 7d9693e82..9b1837f90 100755 --- a/src/social/twitter.js +++ b/src/social/twitter.js @@ -1,5 +1,4 @@ import { Customers, ConversationMessages, Conversations, Integrations } from '../db/models'; -import { CONVERSATION_STATUSES } from '../data/constants'; import { conversationMessageCreated } from '../data/resolvers/mutations/conversations'; /* @@ -85,11 +84,10 @@ export const getOrCreateCommonConversation = async (data, integration) => { } else { const customerId = await getOrCreateCustomer(integration._id, data.user); - const conversationId = await Conversations.create({ + const conversationId = await Conversations.createConversation({ content: data.text, integrationId: integration._id, customerId, - status: CONVERSATION_STATUSES.NEW, // save tweet id twitterData: { @@ -148,11 +146,10 @@ export const getOrCreateDirectMessageConversation = async (data, integration) => } else { const customerId = await getOrCreateCustomer(integration._id, data.sender); - const conversationId = await Conversations.create({ + const conversationId = await Conversations.createConversation({ content: data.text, integrationId: integration._id, customerId, - status: CONVERSATION_STATUSES.NEW, // save tweet id twitterData: { From 3831b1a2e10c90145d75315df9a8d61e323d7dc3 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 11 Nov 2017 00:23:34 +0800 Subject: [PATCH 238/318] Call subscription when new fb message --- src/__tests__/social/facebook.saveResponse.test.js | 3 +++ src/__tests__/social/twitter.test.js | 4 ++++ src/data/resolvers/mutations/conversations.js | 3 ++- src/db/models/Conversations.js | 2 +- src/social/facebook.js | 10 +++++++--- src/social/twitter.js | 2 +- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/__tests__/social/facebook.saveResponse.test.js b/src/__tests__/social/facebook.saveResponse.test.js index 9e03a3caa..f126e0abb 100755 --- a/src/__tests__/social/facebook.saveResponse.test.js +++ b/src/__tests__/social/facebook.saveResponse.test.js @@ -222,6 +222,7 @@ describe('facebook integration: save webhook response', () => { const message = await ConversationMessages.findOne(); // check conversation field values + expect(conversation.createdAt).toBeDefined(); expect(conversation.integrationId).toBe(integration._id); expect(conversation.customerId).toBe(customer._id); expect(conversation.status).toBe(CONVERSATION_STATUSES.NEW); @@ -236,6 +237,7 @@ describe('facebook integration: save webhook response', () => { expect(customer.facebookData.id).toBe(senderId); // check message field values + expect(message.createdAt).toBeDefined(); expect(message.conversationId).toBe(conversation._id); expect(message.customerId).toBe(customer._id); expect(message.internal).toBe(false); @@ -286,6 +288,7 @@ describe('facebook integration: save webhook response', () => { const newMessage = await ConversationMessages.findOne({ _id: { $ne: message._id } }); // check message fields + expect(newMessage.createdAt).toBeDefined(); expect(newMessage.conversationId).toBe(conversation._id); expect(newMessage.customerId).toBe(customer._id); expect(newMessage.internal).toBe(false); diff --git a/src/__tests__/social/twitter.test.js b/src/__tests__/social/twitter.test.js index 90a42872e..47c68c892 100755 --- a/src/__tests__/social/twitter.test.js +++ b/src/__tests__/social/twitter.test.js @@ -261,6 +261,7 @@ describe('twitter integration', () => { const message = await ConversationMessages.findOne(); // check conversation field values + expect(conversation.createdAt).toBeDefined(); expect(conversation.integrationId).toBe(_integration._id); expect(conversation.customerId).toBe(customer._id); expect(conversation.status).toBe(CONVERSATION_STATUSES.NEW); @@ -279,6 +280,7 @@ describe('twitter integration', () => { expect(customer.twitterData.profileImageUrl).toBe(profileImageUrl); // check message field values + expect(message.createdAt).toBeDefined(); expect(message.conversationId).toBe(conversation._id); expect(message.customerId).toBe(customer._id); expect(message.internal).toBe(false); @@ -349,6 +351,7 @@ describe('twitter integration', () => { const message = await ConversationMessages.findOne(); // check conv field values + expect(conv.createdAt).toBeDefined(); expect(conv.integrationId).toBe(_integration._id); expect(conv.customerId).toBe(customer._id); expect(conv.status).toBe(CONVERSATION_STATUSES.NEW); @@ -371,6 +374,7 @@ describe('twitter integration', () => { expect(customer.twitterData.profileImageUrl).toBe(data.sender.profile_image_url); // check message field values + expect(message.createdAt).toBeDefined(); expect(message.conversationId).toBe(conv._id); expect(message.customerId).toBe(customer._id); expect(message.internal).toBe(false); diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 8d42897b9..4f24a212e 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -2,6 +2,7 @@ import { _ } from 'underscore'; import strip from 'strip'; import { Conversations, ConversationMessages, Integrations, Customers } from '../../../db/models'; import { tweetReply } from '../../../social/twitter'; +import { facebookReply } from '../../../social/facebook'; import { NOTIFICATION_TYPES } from '../../constants'; import { CONVERSATION_STATUSES, KIND_CHOICES } from '../../constants'; import { moduleRequireLogin } from '../../permissions'; @@ -137,7 +138,7 @@ const conversationMutations = { // send reply to facebook if (kind === KIND_CHOICES.FACEBOOK) { // when facebook kind is feed, assign commentId in extraData - // TODO: facebookReply(conversation, strip(content), messageId); + facebookReply(conversation, strip(doc.content), message._id); } // notify subscription diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index bb49f740b..42b5410e8 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -126,8 +126,8 @@ class Conversation { */ static async createConversation(doc) { return this.create({ - ...doc, status: CONVERSATION_STATUSES.NEW, + ...doc, createdAt: new Date(), number: (await this.find().count()) + 1, messageCount: 0, diff --git a/src/social/facebook.js b/src/social/facebook.js index 0f656584d..fd0e478d5 100755 --- a/src/social/facebook.js +++ b/src/social/facebook.js @@ -1,4 +1,5 @@ import { Integrations, Conversations, ConversationMessages, Customers } from '../db/models'; +import { conversationMessageCreated } from '../data/resolvers/mutations/conversations'; import { INTEGRATION_KIND_CHOICES, @@ -115,7 +116,7 @@ export class SaveWebhookResponse { // create new conversation if (!conversation) { - const conversationId = await Conversations.create({ + const conversationId = await Conversations.createConversation({ integrationId: this.integration._id, customerId: await this.getOrCreateCustomer(senderId), status, @@ -338,7 +339,7 @@ export class SaveWebhookResponse { async createMessage({ conversation, userId, content, attachments, facebookData }) { if (conversation) { // create new message - const messageId = await ConversationMessages.create({ + const messageId = await ConversationMessages.createMessage({ conversationId: conversation._id, customerId: await this.getOrCreateCustomer(userId), content, @@ -347,7 +348,10 @@ export class SaveWebhookResponse { internal: false, }); - // TODO notify subscription server new message + // notify subscription server new message + const message = await ConversationMessages.findOne({ _id: messageId }); + + conversationMessageCreated(message, message.conversationId); return messageId; } diff --git a/src/social/twitter.js b/src/social/twitter.js index 9b1837f90..ab438b8d9 100755 --- a/src/social/twitter.js +++ b/src/social/twitter.js @@ -42,7 +42,7 @@ const createMessage = async (conversation, content, user) => { const customerId = await getOrCreateCustomer(conversation.integrationId, user); // create new message - const messageId = await ConversationMessages.create({ + const messageId = await ConversationMessages.createMessage({ conversationId: conversation._id, customerId, content, From 8d8978c1277e1036e11f772295371d8f00db381c Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 11 Nov 2017 01:33:52 +0800 Subject: [PATCH 239/318] Add widget-api helper mutations --- src/__tests__/conversationDb.test.js | 1 - src/__tests__/conversationMutations.test.js | 12 +++++++++ .../facebook.conversationByFeed.test.js | 5 +++- src/data/resolvers/mutations/conversations.js | 27 +++++++++++++++++-- src/data/schema/conversation.js | 2 ++ src/db/factories.js | 11 ++++++-- 6 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/__tests__/conversationDb.test.js b/src/__tests__/conversationDb.test.js index f3da61113..dcb6338c4 100644 --- a/src/__tests__/conversationDb.test.js +++ b/src/__tests__/conversationDb.test.js @@ -241,7 +241,6 @@ describe('Conversation db', () => { test('Conversation message', async () => { expect(await ConversationMessages.getNonAsnweredMessage(_conversation._id).count()).toBe(1); - // expect(question) await ConversationMessages.update( { conversationId: _conversation._id }, diff --git a/src/__tests__/conversationMutations.test.js b/src/__tests__/conversationMutations.test.js index c4a4ced4c..76ca2ed5f 100644 --- a/src/__tests__/conversationMutations.test.js +++ b/src/__tests__/conversationMutations.test.js @@ -263,4 +263,16 @@ describe('Conversation message mutations', () => { expect(Conversations.markAsReadConversation.mock.calls.length).toBe(1); expect(Conversations.markAsReadConversation).toBeCalledWith(_conversation._id, _user._id); }); + + test('subscription call for widget api', async () => { + await conversationMutations.conversationSubscribeMessageCreated( + {}, + { _id: _conversationMessage._id }, + ); + + await conversationMutations.conversationSubscribeChanged( + {}, + { _ids: ['_id'], type: 'readState' }, + ); + }); }); diff --git a/src/__tests__/social/facebook.conversationByFeed.test.js b/src/__tests__/social/facebook.conversationByFeed.test.js index db2a036b1..568538583 100755 --- a/src/__tests__/social/facebook.conversationByFeed.test.js +++ b/src/__tests__/social/facebook.conversationByFeed.test.js @@ -53,7 +53,10 @@ describe('facebook integration: get or create conversation by feed info', () => expect(await saveWebhookResponse.getOrCreateConversationByFeed(value)).toBe(null); // already saved comments ========== - await conversationMessageFactory({ facebookData: { commentId: 1 } }); + await conversationMessageFactory({ + facebookData: { commentId: 1 }, + conversationId: 'DFASFDFAD', + }); value.item = null; value.comment_id = 1; diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 4f24a212e..2005eae0d 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -5,7 +5,7 @@ import { tweetReply } from '../../../social/twitter'; import { facebookReply } from '../../../social/facebook'; import { NOTIFICATION_TYPES } from '../../constants'; import { CONVERSATION_STATUSES, KIND_CHOICES } from '../../constants'; -import { moduleRequireLogin } from '../../permissions'; +import { requireLogin } from '../../permissions'; import utils from '../../utils'; import { pubsub } from '../subscriptions'; @@ -76,6 +76,22 @@ export const conversationMessageCreated = async (message, conversationId) => { }; const conversationMutations = { + /* + * Calling this mutation from widget api run new message subscription + */ + async conversationSubscribeMessageCreated(root, { _id }) { + const message = await ConversationMessages.findOne({ _id }); + + conversationMessageCreated(message, message.conversationId); + }, + + /* + * Calling this mutation from widget api run read state subscription + */ + async conversationSubscribeChanged(root, { _ids, type }) { + conversationsChanged(_ids, type); + }, + /** * Create new message in conversation * @param {Object} doc contains conversation message inputs @@ -290,6 +306,13 @@ const conversationMutations = { }, }; -moduleRequireLogin(conversationMutations); +requireLogin(conversationMutations, 'conversationMessageAdd'); +requireLogin(conversationMutations, 'conversationsAssign'); +requireLogin(conversationMutations, 'conversationsUnassign'); +requireLogin(conversationMutations, 'conversationsChangeStatus'); +requireLogin(conversationMutations, 'conversationsStar'); +requireLogin(conversationMutations, 'conversationsUnstar'); +requireLogin(conversationMutations, 'conversationsToggleParticipate'); +requireLogin(conversationMutations, 'conversationMarkAsRead'); export default conversationMutations; diff --git a/src/data/schema/conversation.js b/src/data/schema/conversation.js index 59ac6c803..f8ee434bf 100644 --- a/src/data/schema/conversation.js +++ b/src/data/schema/conversation.js @@ -105,4 +105,6 @@ export const mutations = ` conversationsUnstar(_ids: [String]!): User conversationsToggleParticipate(_ids: [String]!): Conversation conversationMarkAsRead(_id: String): Conversation + conversationSubscribeMessageCreated(_id: String!): String + conversationSubscribeChanged(_ids: [String], type: String!): String `; diff --git a/src/db/factories.js b/src/db/factories.js index 5ec3e2e7b..d652752cc 100644 --- a/src/db/factories.js +++ b/src/db/factories.js @@ -200,12 +200,19 @@ export const conversationFactory = (params = {}) => { }); }; -export const conversationMessageFactory = (params = {}) => { +export const conversationMessageFactory = async (params = {}) => { + let conversationId = params.conversationId; + + if (!conversationId) { + const conversation = await conversationFactory({}); + conversationId = conversation._id; + } + return ConversationMessages.createMessage({ content: params.content || faker.random.word(), attachments: {}, mentionedUserIds: params.mentionedUserIds || [Random.id()], - conversationId: params.conversationId || Random.id(), + conversationId, internal: params.internal || true, customerId: params.customerId || Random.id(), userId: params.userId || Random.id(), From 65bff7eab933ca696f0d3a412f3f6cd341f82ea8 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 11 Nov 2017 01:40:35 +0800 Subject: [PATCH 240/318] User test fix --- src/__tests__/userMutations.test.js | 2 +- src/data/resolvers/mutations/users.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/__tests__/userMutations.test.js b/src/__tests__/userMutations.test.js index 79939bda9..6089402da 100644 --- a/src/__tests__/userMutations.test.js +++ b/src/__tests__/userMutations.test.js @@ -235,7 +235,7 @@ describe('User mutations', () => { }, }; - await userMutations.usersEditProfile({}, { ...doc, password: 'Dombo@123' }, { user }); + await userMutations.usersEditProfile({}, { ...doc, password: 'pass' }, { user }); expect(Users.editProfile).toBeCalledWith(user._id, doc); }); diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index b9b7ae8b2..e3aaaa187 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -1,4 +1,3 @@ -import bcrypt from 'bcrypt'; import { Users, Channels } from '../../../db/models'; import utils from '../../../data/utils'; import { requireLogin, requireAdmin } from '../../permissions'; @@ -136,7 +135,7 @@ const userMutations = { */ async usersEditProfile(root, { username, email, password, details }, { user }) { const userOnDb = await Users.findOne({ _id: user._id }); - const valid = await bcrypt.compare(password, userOnDb.password); + const valid = await Users.comparePassword(password, userOnDb.password); if (!password || !valid) { // bad password From 6b20585fa262e6e14e180a0442fb64aa44c29490 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 11 Nov 2017 07:30:57 +0800 Subject: [PATCH 241/318] #22 Refactor, updated customerActivityLog query test --- src/__tests__/activityLogDb.test.js | 48 +++------ src/__tests__/activityLogQueries.test.js | 106 +++++++++++++++++++ src/data/resolvers/activityLogForMonth.js | 6 +- src/data/resolvers/mutations/activityLogs.js | 18 ++++ src/data/resolvers/mutations/index.js | 2 + src/data/schema/activityLog.js | 4 + src/data/schema/index.js | 3 +- src/db/models/ActivityLogs.js | 54 +++++----- 8 files changed, 178 insertions(+), 63 deletions(-) create mode 100644 src/__tests__/activityLogQueries.test.js create mode 100644 src/data/resolvers/mutations/activityLogs.js diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index befa68e2d..b0d50f870 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -14,7 +14,7 @@ import { internalNoteFactory, customerFactory, companyFactory, - conversationFactory, + conversationMessageFactory, segmentFactory, } from '../db/factories'; @@ -142,19 +142,12 @@ describe('ActivityLogs model methods', () => { }); }); - test(`check if exceptions are being thrown as intended when calling createConversationLog`, async () => { - expect.assertions(3); - const conversation = await conversationFactory({}); - const customer = await customerFactory({}); - - try { - await ActivityLogs.createConversationLog(conversation, null, customer); - } catch (e) { - expect(e.message).toBe(`'user' must be supplied when adding activity log for conversations`); - } + test(`check if exceptions are being thrown as intended when calling createConversationMessageLog`, async () => { + expect.assertions(2); + const message = await conversationMessageFactory({}); try { - await ActivityLogs.createConversationLog(conversation, conversation, null); + await ActivityLogs.createConversationMessageLog(message, null); } catch (e) { expect(e.message).toBe( `'customer' must be supplied when adding activity log for conversations`, @@ -162,7 +155,7 @@ describe('ActivityLogs model methods', () => { } try { - await ActivityLogs.createConversationLog(conversation, conversation, {}); + await ActivityLogs.createConversationMessageLog(message, {}); } catch (e) { expect(e.message).toBe( `'customer' must be supplied when adding activity log for conversations`, @@ -170,38 +163,33 @@ describe('ActivityLogs model methods', () => { } }); - test(`check if createConversationLog is working as intended`, async () => { - const conversation = await conversationFactory({}); + test(`check if createConversationMessageLog is working as intended`, async () => { + const message = await conversationMessageFactory({}); const companyA = await companyFactory({}); const companyB = await companyFactory({}); const customer = await customerFactory({ companyIds: [companyA._id, companyB._id] }); - const user = await userFactory({}); - - let aLog = await ActivityLogs.createConversationLog(conversation, user, customer); + let aLog = await ActivityLogs.createConversationMessageLog(message, customer); // check customer conversation log expect(aLog.performedBy.toObject()).toEqual({ - type: ACTION_PERFORMER_TYPES.USER, - id: user._id, + type: ACTION_PERFORMER_TYPES.CUSTOMER, }); expect(aLog.coc.toObject()).toEqual({ type: COC_CONTENT_TYPES.CUSTOMER, id: customer._id, }); expect(aLog.activity.toObject()).toEqual({ - type: ACTIVITY_TYPES.CONVERSATION, + type: ACTIVITY_TYPES.CONVERSATION_MESSAGE, action: ACTIVITY_ACTIONS.CREATE, - id: conversation._id, + id: message._id, }); // check company conversation logs ===================================== aLog = await ActivityLogs.findOne({ - 'activity.type': ACTIVITY_TYPES.CONVERSATION, + 'activity.type': ACTIVITY_TYPES.CONVERSATION_MESSAGE, 'activity.action': ACTIVITY_ACTIONS.CREATE, - 'activity.id': conversation._id, - 'performedBy.type': ACTION_PERFORMER_TYPES.USER, - 'performedBy.id': user._id, + 'activity.id': message._id, 'coc.type': COC_CONTENT_TYPES.COMPANY, 'coc.id': companyA._id, }); @@ -210,11 +198,9 @@ describe('ActivityLogs model methods', () => { expect(aLog.coc.id).toBe(companyA._id); aLog = await ActivityLogs.findOne({ - 'activity.type': ACTIVITY_TYPES.CONVERSATION, + 'activity.type': ACTIVITY_TYPES.CONVERSATION_MESSAGE, 'activity.action': ACTIVITY_ACTIONS.CREATE, - 'activity.id': conversation._id, - 'performedBy.type': ACTION_PERFORMER_TYPES.USER, - 'performedBy.id': user._id, + 'activity.id': message._id, 'coc.type': COC_CONTENT_TYPES.COMPANY, 'coc.id': companyB._id, }); @@ -225,7 +211,7 @@ describe('ActivityLogs model methods', () => { expect(await ActivityLogs.find({}).count()).toBe(3); // test whether activity logs for this conversation is being duplicated or not ======== - await ActivityLogs.createConversationLog(conversation, user, customer); + await ActivityLogs.createConversationMessageLog(message, customer); expect(await ActivityLogs.find({}).count()).toBe(3); }); diff --git a/src/__tests__/activityLogQueries.test.js b/src/__tests__/activityLogQueries.test.js new file mode 100644 index 000000000..6dccc7ce7 --- /dev/null +++ b/src/__tests__/activityLogQueries.test.js @@ -0,0 +1,106 @@ +/* eslint-env jest */ +/* eslint-disable no-underscore-dangle */ + +import { connect, disconnect } from '../db/connection'; +import { COC_CONTENT_TYPES } from '../data/constants'; +import mutations from '../data/resolvers/mutations'; +import { userFactory, segmentFactory, conversationMessageFactory } from '../db/factories'; +import schema from '../data'; +import cronJobs from '../cronJobs'; + +import { graphql } from 'graphql'; + +beforeAll(() => connect()); +afterAll(() => disconnect()); + +describe('customerActivityLog', () => { + let _user; + let _customer; + let _message; + + beforeAll(async () => { + _user = await userFactory({}); + _customer = await mutations.customersAdd( + null, + { + name: 'test user', + email: 'test@test.test', + phone: '123456789', + }, + { user: _user }, + ); + _message = await conversationMessageFactory({}); + }); + + test('customerActivityLog', async () => { + await mutations.activitivyLogsAddConversationMessageLog(null, { + customerId: _customer._id, + messageId: _message._id, + }); + + // create internal note + await mutations.internalNotesAdd( + null, + { + contentType: COC_CONTENT_TYPES.CUSTOMER, + contentTypeId: _customer._id, + content: 'test internal note', + }, + { user: _user }, + ); + + const nameEqualsConditions = [ + { + type: 'string', + dateUnit: 'days', + value: 'Test user', + operator: 'e', + field: 'name', + }, + ]; + + await segmentFactory({ + contentType: COC_CONTENT_TYPES.CUSTOMER, + conditions: nameEqualsConditions, + }); + + // create segment log + await cronJobs.createActivityLogsFromSegments(); + + const query = ` + query customerActivityLog($_id: String!) { + customerActivityLog(_id: $_id) { + date { + year + month + } + list { + id + action + content { + name + } + createdAt + } + } + } + `; + + const rootValue = {}; + const context = { user: _user }; + + const result = await graphql(schema, query, rootValue, context, { _id: _customer._id }); + + // TODO: test values + + // TODO: change activity log 'createdAt' values + + // TODO: test again + // console.log("result: ", result); + for (let item of result.data.customerActivityLog) { + console.log('item: ', item); + // console.log('item.date: ', item.date); + // console.log('item.list: ', item.list); + } + }); +}); diff --git a/src/data/resolvers/activityLogForMonth.js b/src/data/resolvers/activityLogForMonth.js index 8f7cd1883..e8e7e808c 100644 --- a/src/data/resolvers/activityLogForMonth.js +++ b/src/data/resolvers/activityLogForMonth.js @@ -7,11 +7,11 @@ export default { list(obj) { return ActivityLogs.find({ - 'customer.type': obj.customerType, - 'customer.id': obj.customer._id, + 'coc.type': obj.customerType, + 'coc.id': obj.customer._id, createdAt: { $gte: obj.date.interval.start, - $lte: obj.date.interval.end, + $lt: obj.date.interval.end, }, }); }, diff --git a/src/data/resolvers/mutations/activityLogs.js b/src/data/resolvers/mutations/activityLogs.js new file mode 100644 index 000000000..4777079b7 --- /dev/null +++ b/src/data/resolvers/mutations/activityLogs.js @@ -0,0 +1,18 @@ +import { ActivityLogs, Customers, ConversationMessages } from '../../../db/models'; + +export default { + /** + * Add conversation message log + * @param {Object} root + * @param {Object} object2 - arguments + * @param {string} customerId - id of customer + * @param {string} conversationId - id of conversation + * @param {string} messageId - id of message + */ + async activitivyLogsAddConversationMessageLog(root, { customerId, messageId }) { + const customer = await Customers.findOne({ _id: customerId }); + const message = await ConversationMessages.findOne({ _id: messageId }); + + return ActivityLogs.createConversationMessageLog(message, customer); + }, +}; diff --git a/src/data/resolvers/mutations/index.js b/src/data/resolvers/mutations/index.js index a161c7baa..a8901aea7 100644 --- a/src/data/resolvers/mutations/index.js +++ b/src/data/resolvers/mutations/index.js @@ -15,6 +15,7 @@ import forms from './forms'; import integrations from './integrations'; import notifications from './notifications'; import knowledgeBase from './knowledgeBase'; +import activityLogs from './activityLogs'; export default { ...users, @@ -34,4 +35,5 @@ export default { ...integrations, ...notifications, ...knowledgeBase, + ...activityLogs, }; diff --git a/src/data/schema/activityLog.js b/src/data/schema/activityLog.js index 6fe6d30f4..c5521c640 100644 --- a/src/data/schema/activityLog.js +++ b/src/data/schema/activityLog.js @@ -20,3 +20,7 @@ export const types = ` content: ActivityContent! } `; + +export const mutations = ` + activitivyLogsAddConversationMessageLog(customerId: String!, messageId: String!): ActivityLog +`; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index 57292dd1e..3ff9e0a8f 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -82,7 +82,7 @@ import { mutations as ConversationMutations, } from './conversation'; -import { types as ActivityLogTypes } from './activityLog'; +import { types as ActivityLogTypes, mutations as ActivityLogMutations } from './activityLog'; export const types = ` scalar JSON @@ -151,6 +151,7 @@ export const mutations = ` ${IntegrationMutations} ${KnowledgeBaseMutations} ${NotificationMutations} + ${ActivityLogMutations} } `; diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index bfa6792d1..867a36af2 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -6,10 +6,10 @@ export const ACTIVITY_TYPES = { CUSTOMER: 'customer', COMPANY: 'company', INTERNAL_NOTE: 'internal_note', - CONVERSATION: 'conversation', + CONVERSATION_MESSAGE: 'conversation_message', SEGMENT: 'segment', - ALL: ['customer', 'company', 'internal_note', 'conversation', 'segment'], + ALL: ['customer', 'company', 'internal_note', 'conversation_message', 'segment'], }; export const ACTIVITY_ACTIONS = { @@ -23,8 +23,9 @@ export const ACTIVITY_ACTIONS = { export const ACTION_PERFORMER_TYPES = { SYSTEM: 'SYSTEM', USER: 'USER', + CUSTOMER: 'CUSTOMER', - ALL: ['SYSTEM', 'USER'], + ALL: ['SYSTEM', 'USER', 'CUSTOMER'], }; /* Performer of the action: @@ -137,7 +138,7 @@ class ActivityLog { type: ACTION_PERFORMER_TYPES.USER, id: performedBy._id, }; - } else { + } else if (!performedBy) { performedBy = {}; } @@ -167,21 +168,16 @@ class ActivityLog { } /** - * Create conversation log for a given customer, if the customer is related to companies, + * Create a conversation message log for a given customer, + * if the customer is related to companies, * then create conversation log with all related companies - * @param {Object} conversation - Conversation object - * @param {string} conversation._id - Conversation document id - * @param {Object} user - User object - * @param {Object} user._id - User document id + * @param {Object} message - Conversation object + * @param {string} message._id - Conversation document id * @param {Object} customer - Customer object * @param {string} customer.type - One of COC_CONTENT_TYPES choices * @param {string} customer.id - Customer document id */ - static async createConversationLog(conversation, user, customer) { - if (user == null || (user && !user._id)) { - throw new Error(`'user' must be supplied when adding activity log for conversations`); - } - + static async createConversationMessageLog(message, customer) { if (customer == null || (customer && !customer._id)) { throw new Error(`'customer' must be supplied when adding activity log for conversations`); } @@ -190,23 +186,24 @@ class ActivityLog { for (let companyId of customer.companyIds) { // check against duplication const foundLog = await this.findOne({ - 'activity.type': ACTIVITY_TYPES.CONVERSATION, + 'activity.type': ACTIVITY_TYPES.CONVERSATION_MESSAGE, 'activity.action': ACTIVITY_ACTIONS.CREATE, - 'activity.id': conversation._id, - 'performedBy.type': ACTION_PERFORMER_TYPES.USER, - 'performedBy.id': user._id, + 'activity.id': message._id, 'coc.type': COC_CONTENT_TYPES.COMPANY, + 'performedBy.type': ACTION_PERFORMER_TYPES.CUSTOMER, 'coc.id': companyId, }); if (!foundLog) { await this.createDoc({ activity: { - type: ACTIVITY_TYPES.CONVERSATION, + type: ACTIVITY_TYPES.CONVERSATION_MESSAGE, action: ACTIVITY_ACTIONS.CREATE, - id: conversation._id, + id: message._id, + }, + performedBy: { + type: ACTION_PERFORMER_TYPES.CUSTOMER, }, - performedBy: user, coc: { type: COC_CONTENT_TYPES.COMPANY, id: companyId, @@ -218,11 +215,10 @@ class ActivityLog { // check against duplication ====== const foundLog = await this.findOne({ - 'activity.type': ACTIVITY_TYPES.CONVERSATION, + 'activity.type': ACTIVITY_TYPES.CONVERSATION_MESSAGE, 'activity.action': ACTIVITY_ACTIONS.CREATE, - 'activity.id': conversation._id, - 'performedBy.type': ACTION_PERFORMER_TYPES.USER, - 'performedBy.id': user._id, + 'activity.id': message._id, + 'performedBy.type': ACTION_PERFORMER_TYPES.CUSTOMER, 'coc.type': COC_CONTENT_TYPES.CUSTOMER, 'coc.id': customer._id, }); @@ -230,11 +226,13 @@ class ActivityLog { if (!foundLog) { return this.createDoc({ activity: { - type: ACTIVITY_TYPES.CONVERSATION, + type: ACTIVITY_TYPES.CONVERSATION_MESSAGE, action: ACTIVITY_ACTIONS.CREATE, - id: conversation._id, + id: message._id, + }, + performedBy: { + type: ACTION_PERFORMER_TYPES.CUSTOMER, }, - performedBy: user, coc: { type: COC_CONTENT_TYPES.CUSTOMER, id: customer._id, From d43f444c45c6ce5362d438cbcad0e0390d057557 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 11 Nov 2017 08:24:19 +0800 Subject: [PATCH 242/318] #22 Refactor ActivityLogs.content, complete custome activity log test --- src/__tests__/activityLogCronJob.test.js | 4 +- src/__tests__/activityLogDb.test.js | 14 +-- src/__tests__/activityLogQueries.test.js | 129 ++++++++++++++++++---- src/data/resolvers/activityLog.js | 4 + src/data/resolvers/activityLogForMonth.js | 2 +- src/data/schema/activityLog.js | 6 +- src/db/models/ActivityLogs.js | 15 +-- 7 files changed, 125 insertions(+), 49 deletions(-) diff --git a/src/__tests__/activityLogCronJob.test.js b/src/__tests__/activityLogCronJob.test.js index 0399ed04e..a3102ddef 100644 --- a/src/__tests__/activityLogCronJob.test.js +++ b/src/__tests__/activityLogCronJob.test.js @@ -42,9 +42,7 @@ describe('test activityLogsCronJob', () => { expect(aLog.activity.toObject()).toEqual({ type: ACTIVITY_TYPES.SEGMENT, action: ACTIVITY_ACTIONS.CREATE, - content: { - name: segment.name, - }, + content: segment.name, id: segment._id, }); expect(aLog.coc.toObject()).toEqual({ diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index b0d50f870..600be6deb 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -89,6 +89,7 @@ describe('ActivityLogs model methods', () => { type: ACTIVITY_TYPES.INTERNAL_NOTE, action: ACTIVITY_ACTIONS.CREATE, id: internalNote._id, + content: internalNote.content, }); }); @@ -128,9 +129,7 @@ describe('ActivityLogs model methods', () => { expect(segmentLog.activity.toObject()).toEqual({ type: ACTIVITY_TYPES.SEGMENT, action: ACTIVITY_ACTIONS.CREATE, - content: { - name: segment.name, - }, + content: segment.name, id: segment._id, }); expect(segmentLog.coc.toObject()).toEqual({ @@ -182,6 +181,7 @@ describe('ActivityLogs model methods', () => { expect(aLog.activity.toObject()).toEqual({ type: ACTIVITY_TYPES.CONVERSATION_MESSAGE, action: ACTIVITY_ACTIONS.CREATE, + content: message.content, id: message._id, }); @@ -229,9 +229,7 @@ describe('ActivityLogs model methods', () => { expect(aLog.activity.toObject()).toEqual({ type: ACTIVITY_TYPES.CUSTOMER, action: ACTIVITY_ACTIONS.CREATE, - content: { - name: customer.name, - }, + content: customer.name, id: customer._id, }); expect(aLog.coc.toObject()).toEqual({ @@ -253,9 +251,7 @@ describe('ActivityLogs model methods', () => { expect(aLog.activity.toObject()).toEqual({ type: ACTIVITY_TYPES.COMPANY, action: ACTIVITY_ACTIONS.CREATE, - content: { - name: company.name, - }, + content: company.name, id: company._id, }); expect(aLog.coc.toObject()).toEqual({ diff --git a/src/__tests__/activityLogQueries.test.js b/src/__tests__/activityLogQueries.test.js index 6dccc7ce7..113d1b354 100644 --- a/src/__tests__/activityLogQueries.test.js +++ b/src/__tests__/activityLogQueries.test.js @@ -5,6 +5,7 @@ import { connect, disconnect } from '../db/connection'; import { COC_CONTENT_TYPES } from '../data/constants'; import mutations from '../data/resolvers/mutations'; import { userFactory, segmentFactory, conversationMessageFactory } from '../db/factories'; +import { ActivityLogs } from '../db/models'; import schema from '../data'; import cronJobs from '../cronJobs'; @@ -15,12 +16,15 @@ afterAll(() => disconnect()); describe('customerActivityLog', () => { let _user; - let _customer; let _message; beforeAll(async () => { _user = await userFactory({}); - _customer = await mutations.customersAdd( + _message = await conversationMessageFactory({}); + }); + + test('customerActivityLog', async () => { + const customer = await mutations.customersAdd( null, { name: 'test user', @@ -29,21 +33,18 @@ describe('customerActivityLog', () => { }, { user: _user }, ); - _message = await conversationMessageFactory({}); - }); - test('customerActivityLog', async () => { await mutations.activitivyLogsAddConversationMessageLog(null, { - customerId: _customer._id, + customerId: customer._id, messageId: _message._id, }); // create internal note - await mutations.internalNotesAdd( + const internalNote = await mutations.internalNotesAdd( null, { contentType: COC_CONTENT_TYPES.CUSTOMER, - contentTypeId: _customer._id, + contentTypeId: customer._id, content: 'test internal note', }, { user: _user }, @@ -59,7 +60,7 @@ describe('customerActivityLog', () => { }, ]; - await segmentFactory({ + const segment = await segmentFactory({ contentType: COC_CONTENT_TYPES.CUSTOMER, conditions: nameEqualsConditions, }); @@ -77,9 +78,7 @@ describe('customerActivityLog', () => { list { id action - content { - name - } + content createdAt } } @@ -89,18 +88,104 @@ describe('customerActivityLog', () => { const rootValue = {}; const context = { user: _user }; - const result = await graphql(schema, query, rootValue, context, { _id: _customer._id }); + // call graphql query + let result = await graphql(schema, query, rootValue, context, { _id: customer._id }); + + let logs = result.data.customerActivityLog; + + // test values + expect(logs[0].list[0].id).toBe(segment._id); + expect(logs[0].list[0].action).toBe('segment-create'); + expect(logs[0].list[0].content).toBe(segment.name); + + expect(logs[0].list[1].id).toBe(internalNote._id); + expect(logs[0].list[1].action).toBe('internal_note-create'); + expect(logs[0].list[1].content).toBe(internalNote.content); + + expect(logs[0].list[2].id).toBe(_message._id); + expect(logs[0].list[2].action).toBe('conversation_message-create'); + expect(logs[0].list[2].content).toBe(_message.content); + + expect(logs[0].list[3].id).toBe(customer._id); + expect(logs[0].list[3].action).toBe('customer-create'); + expect(logs[0].list[3].content).toBe(customer.name); + + // change activity log 'createdAt' values + await ActivityLogs.update( + { + 'activity.type': 'segment', + 'activity.action': 'create', + }, + { + $set: { + createdAt: new Date(2017, 0, 1), + }, + }, + ); + + await ActivityLogs.update( + { + 'activity.type': 'internal_note', + 'activity.action': 'create', + }, + { + $set: { + createdAt: new Date(2017, 1, 1), + }, + }, + ); + + await ActivityLogs.update( + { + 'activity.type': 'conversation_message', + 'activity.action': 'create', + }, + { + $set: { + createdAt: new Date(2017, 1, 3), + }, + }, + ); - // TODO: test values + await ActivityLogs.update( + { + 'activity.type': 'customer', + 'activity.action': 'create', + }, + { + $set: { + createdAt: new Date(2017, 1, 4), + }, + }, + ); + + // call graphql query + result = await graphql(schema, query, rootValue, context, { _id: customer._id }); + + // get new log + logs = result.data.customerActivityLog; - // TODO: change activity log 'createdAt' values + // test freshly list + const yearMonthLength = logs.length - 1; + + expect(logs[yearMonthLength].list[0].id).toBe(segment._id); + expect(logs[yearMonthLength].list[0].action).toBe('segment-create'); + expect(logs[yearMonthLength].list[0].content).toBe(segment.name); + + const februaryLogLength = logs[yearMonthLength - 1].list.length - 1; + + expect(logs[yearMonthLength - 1].list[februaryLogLength].id).toBe(internalNote._id); + expect(logs[yearMonthLength - 1].list[februaryLogLength].action).toBe('internal_note-create'); + expect(logs[yearMonthLength - 1].list[februaryLogLength].content).toBe(internalNote.content); + + expect(logs[yearMonthLength - 1].list[februaryLogLength - 1].id).toBe(_message._id); + expect(logs[yearMonthLength - 1].list[februaryLogLength - 1].action).toBe( + 'conversation_message-create', + ); + expect(logs[yearMonthLength - 1].list[februaryLogLength - 1].content).toBe(_message.content); - // TODO: test again - // console.log("result: ", result); - for (let item of result.data.customerActivityLog) { - console.log('item: ', item); - // console.log('item.date: ', item.date); - // console.log('item.list: ', item.list); - } + expect(logs[yearMonthLength - 1].list[februaryLogLength - 2].id).toBe(customer._id); + expect(logs[yearMonthLength - 1].list[februaryLogLength - 2].action).toBe('customer-create'); + expect(logs[yearMonthLength - 1].list[februaryLogLength - 2].content).toBe(customer.name); }); }); diff --git a/src/data/resolvers/activityLog.js b/src/data/resolvers/activityLog.js index 12b19dcb8..746f6b33b 100644 --- a/src/data/resolvers/activityLog.js +++ b/src/data/resolvers/activityLog.js @@ -1,4 +1,8 @@ export default { + id(obj) { + return obj.activity.id; + }, + action(obj) { return `${obj.activity.type}-${obj.activity.action}`; }, diff --git a/src/data/resolvers/activityLogForMonth.js b/src/data/resolvers/activityLogForMonth.js index e8e7e808c..c1cb7bb36 100644 --- a/src/data/resolvers/activityLogForMonth.js +++ b/src/data/resolvers/activityLogForMonth.js @@ -13,6 +13,6 @@ export default { $gte: obj.date.interval.start, $lt: obj.date.interval.end, }, - }); + }).sort({ createdAt: -1 }); }, }; diff --git a/src/data/schema/activityLog.js b/src/data/schema/activityLog.js index c5521c640..d3b1d8465 100644 --- a/src/data/schema/activityLog.js +++ b/src/data/schema/activityLog.js @@ -4,10 +4,6 @@ export const types = ` month: Int } - type ActivityContent { - name: String - } - type ActivityLogForMonth { date: YearMonthDoc! list: [ActivityLog]! @@ -17,7 +13,7 @@ export const types = ` action: String! id: String! createdAt: Date! - content: ActivityContent! + content: String } `; diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 867a36af2..68f792bb1 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -158,6 +158,7 @@ class ActivityLog { type: ACTIVITY_TYPES.INTERNAL_NOTE, action: ACTIVITY_ACTIONS.CREATE, id: internalNote._id, + content: internalNote.content, }, performedBy: user, coc: { @@ -199,6 +200,7 @@ class ActivityLog { activity: { type: ACTIVITY_TYPES.CONVERSATION_MESSAGE, action: ACTIVITY_ACTIONS.CREATE, + content: message.content, id: message._id, }, performedBy: { @@ -228,6 +230,7 @@ class ActivityLog { activity: { type: ACTIVITY_TYPES.CONVERSATION_MESSAGE, action: ACTIVITY_ACTIONS.CREATE, + content: message.content, id: message._id, }, performedBy: { @@ -269,9 +272,7 @@ class ActivityLog { activity: { type: ACTIVITY_TYPES.SEGMENT, action: ACTIVITY_ACTIONS.CREATE, - content: { - name: segment.name, - }, + content: segment.name, id: segment._id, }, coc: { @@ -292,9 +293,7 @@ class ActivityLog { activity: { type: ACTIVITY_TYPES.CUSTOMER, action: ACTIVITY_ACTIONS.CREATE, - content: { - name: customer.name, - }, + content: customer.name, id: customer._id, }, coc: { @@ -316,9 +315,7 @@ class ActivityLog { activity: { type: ACTIVITY_TYPES.COMPANY, action: ACTIVITY_ACTIONS.CREATE, - content: { - name: company.name, - }, + content: company.name, id: company._id, }, coc: { From 5361cfd1fded761581646fdac42d623b5280e1eb Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 11 Nov 2017 09:17:33 +0800 Subject: [PATCH 243/318] #22 Complete company activity log test --- src/__tests__/activityLogQueries.test.js | 80 ++++++++++++++++--- src/cronJobs/activityLogs.js | 2 +- src/data/resolvers/mutations/companies.js | 8 +- src/data/resolvers/mutations/engageUtils.js | 2 +- src/data/resolvers/queries/activityLogs.js | 37 +++++++++ src/data/resolvers/queries/companies.js | 17 +--- src/data/resolvers/queries/customers.js | 17 +--- src/data/resolvers/queries/index.js | 2 + .../queries}/segmentQueryBuilder.js | 62 +++++++------- src/data/schema/activityLog.js | 9 ++- src/data/schema/company.js | 1 - src/data/schema/customer.js | 1 - src/data/schema/index.js | 7 +- src/db/models/Companies.js | 4 +- 14 files changed, 163 insertions(+), 86 deletions(-) create mode 100644 src/data/resolvers/queries/activityLogs.js rename src/data/{ => resolvers/queries}/segmentQueryBuilder.js (58%) diff --git a/src/__tests__/activityLogQueries.test.js b/src/__tests__/activityLogQueries.test.js index 113d1b354..c1b306878 100644 --- a/src/__tests__/activityLogQueries.test.js +++ b/src/__tests__/activityLogQueries.test.js @@ -4,7 +4,12 @@ import { connect, disconnect } from '../db/connection'; import { COC_CONTENT_TYPES } from '../data/constants'; import mutations from '../data/resolvers/mutations'; -import { userFactory, segmentFactory, conversationMessageFactory } from '../db/factories'; +import { + userFactory, + segmentFactory, + conversationMessageFactory, + customerFactory, +} from '../db/factories'; import { ActivityLogs } from '../db/models'; import schema from '../data'; import cronJobs from '../cronJobs'; @@ -14,7 +19,7 @@ import { graphql } from 'graphql'; beforeAll(() => connect()); afterAll(() => disconnect()); -describe('customerActivityLog', () => { +describe('activityLogs', () => { let _user; let _message; @@ -23,7 +28,12 @@ describe('customerActivityLog', () => { _message = await conversationMessageFactory({}); }); + afterEach(() => { + return ActivityLogs.remove({}); + }); + test('customerActivityLog', async () => { + // create customer const customer = await mutations.customersAdd( null, { @@ -34,6 +44,7 @@ describe('customerActivityLog', () => { { user: _user }, ); + // create conversation message await mutations.activitivyLogsAddConversationMessageLog(null, { customerId: customer._id, messageId: _message._id, @@ -69,8 +80,8 @@ describe('customerActivityLog', () => { await cronJobs.createActivityLogsFromSegments(); const query = ` - query customerActivityLog($_id: String!) { - customerActivityLog(_id: $_id) { + query activityLogsCustomer($_id: String!) { + activityLogsCustomer(_id: $_id) { date { year month @@ -91,9 +102,9 @@ describe('customerActivityLog', () => { // call graphql query let result = await graphql(schema, query, rootValue, context, { _id: customer._id }); - let logs = result.data.customerActivityLog; + let logs = result.data.activityLogsCustomer; - // test values + // test values ============== expect(logs[0].list[0].id).toBe(segment._id); expect(logs[0].list[0].action).toBe('segment-create'); expect(logs[0].list[0].content).toBe(segment.name); @@ -110,7 +121,7 @@ describe('customerActivityLog', () => { expect(logs[0].list[3].action).toBe('customer-create'); expect(logs[0].list[3].content).toBe(customer.name); - // change activity log 'createdAt' values + // change activity log 'createdAt' values =================== await ActivityLogs.update( { 'activity.type': 'segment', @@ -163,9 +174,9 @@ describe('customerActivityLog', () => { result = await graphql(schema, query, rootValue, context, { _id: customer._id }); // get new log - logs = result.data.customerActivityLog; + logs = result.data.activityLogsCustomer; - // test freshly list + // test the fetched data ============================= const yearMonthLength = logs.length - 1; expect(logs[yearMonthLength].list[0].id).toBe(segment._id); @@ -188,4 +199,55 @@ describe('customerActivityLog', () => { expect(logs[yearMonthLength - 1].list[februaryLogLength - 2].action).toBe('customer-create'); expect(logs[yearMonthLength - 1].list[februaryLogLength - 2].content).toBe(customer.name); }); + + test('companyActivityLog', async () => { + const company = await mutations.companiesAdd( + null, + { + name: 'test company', + website: 'test.test.test', + }, + { user: _user }, + ); + const customer = await customerFactory({ companyIds: [company._id] }); + + await mutations.activitivyLogsAddConversationMessageLog(null, { + customerId: customer._id, + messageId: _message._id, + }); + + const query = ` + query activityLogsCompany($_id: String!) { + activityLogsCompany(_id: $_id) { + date { + year + month + } + list { + id + action + content + createdAt + } + } + } + `; + + const rootValue = {}; + const context = { user: _user }; + + // call graphql query + let result = await graphql(schema, query, rootValue, context, { _id: company._id }); + + const logs = result.data.activityLogsCompany; + + // test values =========================== + expect(logs[0].list[0].id).toBe(_message._id); + expect(logs[0].list[0].action).toBe('conversation_message-create'); + expect(logs[0].list[0].content).toBe(_message.content); + + expect(logs[0].list[1].id).toBe(company._id); + expect(logs[0].list[1].action).toBe('company-create'); + expect(logs[0].list[1].content).toBe(company.name); + }); }); diff --git a/src/cronJobs/activityLogs.js b/src/cronJobs/activityLogs.js index 7c0721482..1a0e048e2 100644 --- a/src/cronJobs/activityLogs.js +++ b/src/cronJobs/activityLogs.js @@ -1,6 +1,6 @@ import schedule from 'node-schedule'; import { Segments, Customers, ActivityLogs } from '../db/models'; -import QueryBuilder from '../data/segmentQueryBuilder'; +import QueryBuilder from '../data/resolvers/queries/segmentQueryBuilder'; /** * Send conversation messages to customer diff --git a/src/data/resolvers/mutations/companies.js b/src/data/resolvers/mutations/companies.js index 906aca792..b2311622c 100644 --- a/src/data/resolvers/mutations/companies.js +++ b/src/data/resolvers/mutations/companies.js @@ -1,4 +1,4 @@ -import { Companies } from '../../../db/models'; +import { Companies, ActivityLogs } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; const companyMutations = { @@ -6,8 +6,10 @@ const companyMutations = { * Create new company * @return {Promise} company object */ - companiesAdd(root, doc) { - return Companies.createCompany(doc); + async companiesAdd(root, doc, { user }) { + const company = await Companies.createCompany(doc); + await ActivityLogs.createCompanyRegistrationLog(company, user); + return company; }, /** diff --git a/src/data/resolvers/mutations/engageUtils.js b/src/data/resolvers/mutations/engageUtils.js index 190e851ab..0c3cb77f9 100644 --- a/src/data/resolvers/mutations/engageUtils.js +++ b/src/data/resolvers/mutations/engageUtils.js @@ -15,7 +15,7 @@ import { INTEGRATION_KIND_CHOICES, } from '../../constants'; import Random from 'meteor-random'; -import QueryBuilder from '../../segmentQueryBuilder'; +import QueryBuilder from '../queries/segmentQueryBuilder'; import { createTransporter } from '../../utils'; /** diff --git a/src/data/resolvers/queries/activityLogs.js b/src/data/resolvers/queries/activityLogs.js new file mode 100644 index 000000000..43434c94b --- /dev/null +++ b/src/data/resolvers/queries/activityLogs.js @@ -0,0 +1,37 @@ +import { Customers, Companies } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions'; +import { CustomerMonthActivityLogBuilder, CompanyMonthActivityLogBuilder } from '../../utils'; + +const activityLogQueries = { + /** + * Get activity log for customer + * @param {Object} root + * @param {Object} object2 - Graphql input data + * @param {string} object._id - Customer id + * @return {Promise} found customer + */ + async activityLogsCustomer(root, { _id }) { + const customer = await Customers.findOne({ _id }); + + const m = new CustomerMonthActivityLogBuilder(customer); + return m.build(); + }, + + /** + * Get activity log for company + * @param {Object} root + * @param {Object} object2 - Graphql input data + * @param {string} object._id - Company id + * @return {Promise} Promise resolving array of ActivityLogForMonth + */ + async activityLogsCompany(root, { _id }) { + const company = await Companies.findOne({ _id }); + + const m = new CompanyMonthActivityLogBuilder(company); + return m.build(); + }, +}; + +moduleRequireLogin(activityLogQueries); + +export default activityLogQueries; diff --git a/src/data/resolvers/queries/companies.js b/src/data/resolvers/queries/companies.js index 56d562bc4..e88c643bf 100644 --- a/src/data/resolvers/queries/companies.js +++ b/src/data/resolvers/queries/companies.js @@ -1,9 +1,8 @@ import { Companies, Segments } from '../../../db/models'; -import QueryBuilder from '../../segmentQueryBuilder'; +import QueryBuilder from './segmentQueryBuilder'; import { COC_CONTENT_TYPES } from '../../constants'; import { moduleRequireLogin } from '../../permissions'; import { paginate } from './utils'; -import { CompanyMonthActivityLogBuilder } from '../../utils'; const listQuery = async params => { const selector = {}; @@ -74,20 +73,6 @@ const companyQueries = { companyDetail(root, { _id }) { return Companies.findOne({ _id }); }, - - /** - * Get activity log for company - * @param {Object} root - * @param {Object} object2 - Graphql input data - * @param {string} object._id - Company id - * @return {Promise} Promise resolving array of ActivityLogForMonth - */ - async companyActivityLog(root, { _id }) { - const company = await Companies.findOne({ _id }); - - const m = new CompanyMonthActivityLogBuilder(company); - return m.build(); - }, }; moduleRequireLogin(companyQueries); diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index fba8787f8..bac8897c9 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -1,10 +1,9 @@ import _ from 'underscore'; import { Brands, Tags, Integrations, Customers, Segments } from '../../../db/models'; import { TAG_TYPES, INTEGRATION_KIND_CHOICES, COC_CONTENT_TYPES } from '../../constants'; -import QueryBuilder from '../../segmentQueryBuilder'; +import QueryBuilder from './segmentQueryBuilder'; import { moduleRequireLogin } from '../../permissions'; import { paginate } from './utils'; -import { CustomerMonthActivityLogBuilder } from '../../utils'; const listQuery = async params => { const selector = {}; @@ -148,20 +147,6 @@ const customerQueries = { customerDetail(root, { _id }) { return Customers.findOne({ _id }); }, - - /** - * Get activity log for customer - * @param {Object} root - * @param {Object} object2 - Graphql input data - * @param {string} object._id - Customer id - * @return {Promise} found customer - */ - async customerActivityLog(root, { _id }) { - const customer = await Customers.findOne({ _id }); - - const m = new CustomerMonthActivityLogBuilder(customer); - return m.build(); - }, }; moduleRequireLogin(customerQueries); diff --git a/src/data/resolvers/queries/index.js b/src/data/resolvers/queries/index.js index 86590d061..77835e09e 100644 --- a/src/data/resolvers/queries/index.js +++ b/src/data/resolvers/queries/index.js @@ -16,6 +16,7 @@ import conversations from './conversations'; import insights from './insights'; import knowledgeBase from './knowledgeBase'; import notifications from './notifications'; +import activityLogs from './activityLogs'; export default { ...users, @@ -36,4 +37,5 @@ export default { ...insights, ...knowledgeBase, ...notifications, + ...activityLogs, }; diff --git a/src/data/segmentQueryBuilder.js b/src/data/resolvers/queries/segmentQueryBuilder.js similarity index 58% rename from src/data/segmentQueryBuilder.js rename to src/data/resolvers/queries/segmentQueryBuilder.js index 6cea24e72..50ad79833 100644 --- a/src/data/segmentQueryBuilder.js +++ b/src/data/resolvers/queries/segmentQueryBuilder.js @@ -1,41 +1,38 @@ import moment from 'moment'; -export const segments = (segment, headSegment) => { - const query = { $and: [] }; - - const childQuery = { - [segment.connector === 'any' ? '$or' : '$and']: segment.conditions.map(condition => ({ - [condition.field]: convertConditionToQuery(condition), - })), - }; - - if (segment.conditions.length) { - query.$and.push(childQuery); - } - - // Fetching parent segment - const embeddedParentSegment = - typeof segment.getParentSegment === 'function' ? segment.getParentSegment() : null; - const parentSegment = headSegment || embeddedParentSegment; +export default { + segments(segment, headSegment) { + const query = { $and: [] }; - if (parentSegment) { - const parentQuery = { - [parentSegment.connector === 'any' - ? '$or' - : '$and']: parentSegment.conditions.map(condition => ({ + const childQuery = { + [segment.connector === 'any' ? '$or' : '$and']: segment.conditions.map(condition => ({ [condition.field]: convertConditionToQuery(condition), })), }; - if (parentSegment.conditions.length) { - query.$and.push(parentQuery); + if (segment.conditions.length) { + query.$and.push(childQuery); } - } - return query.$and.length ? query : {}; -}; + // Fetching parent segment + const embeddedParentSegment = + typeof segment.getParentSegment === 'function' ? segment.getParentSegment() : null; + const parentSegment = headSegment || embeddedParentSegment; -export default { - segments, + if (parentSegment) { + const parentQuery = { + [parentSegment.connector === 'any' + ? '$or' + : '$and']: parentSegment.conditions.map(condition => ({ + [condition.field]: convertConditionToQuery(condition), + })), + }; + if (parentSegment.conditions.length) { + query.$and.push(parentQuery); + } + } + + return query.$and.length ? query : {}; + }, }; function convertConditionToQuery(condition) { @@ -63,9 +60,9 @@ function convertConditionToQuery(condition) { case 'dne': return { $ne: transformedValue }; case 'c': - return { $regex: `.*${escapeRegExp(transformedValue)}.*`, $options: 'i' }; + return { $regex: new RegExp(`.*${escapeRegExp(transformedValue)}.*`, 'i') }; case 'dnc': - return { $regex: `^((?!${escapeRegExp(transformedValue)}).)*$`, $options: 'i' }; + return { $regex: new RegExp(`^((?!${escapeRegExp(transformedValue)}).)*$`, 'i') }; case 'igt': return { $gt: transformedValue }; case 'ilt': @@ -108,6 +105,5 @@ function convertConditionToQuery(condition) { } function escapeRegExp(string) { - // $& means the whole matched string - return new String(string).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } diff --git a/src/data/schema/activityLog.js b/src/data/schema/activityLog.js index d3b1d8465..b8cc8c4b2 100644 --- a/src/data/schema/activityLog.js +++ b/src/data/schema/activityLog.js @@ -1,11 +1,11 @@ export const types = ` - type YearMonthDoc { + type ActivityLogYearMonthDoc { year: Int month: Int } type ActivityLogForMonth { - date: YearMonthDoc! + date: ActivityLogYearMonthDoc! list: [ActivityLog]! } @@ -17,6 +17,11 @@ export const types = ` } `; +export const queries = ` + activityLogsCustomer(_id: String!): [ActivityLogForMonth] + activityLogsCompany(_id: String!): [ActivityLogForMonth] +`; + export const mutations = ` activitivyLogsAddConversationMessageLog(customerId: String!, messageId: String!): ActivityLog `; diff --git a/src/data/schema/company.js b/src/data/schema/company.js index 8a89b6265..794ec2621 100644 --- a/src/data/schema/company.js +++ b/src/data/schema/company.js @@ -27,7 +27,6 @@ export const queries = ` companies(params: CompanyListParams): [Company] companyCounts(params: CompanyListParams): JSON companyDetail(_id: String!): Company - companyActivityLog(_id: String!): [ActivityLogForMonth] `; const commonFields = ` diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index 93fbb741b..a43210dda 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -37,7 +37,6 @@ export const queries = ` customers(params: CustomerListParams): [Customer] customerCounts(params: CustomerListParams): JSON customerDetail(_id: String!): Customer - customerActivityLog(_id: String!): [ActivityLogForMonth] customerListForSegmentPreview(segment: JSON, limit: Int): [Customer] `; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index 3ff9e0a8f..fef16028c 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -82,7 +82,11 @@ import { mutations as ConversationMutations, } from './conversation'; -import { types as ActivityLogTypes, mutations as ActivityLogMutations } from './activityLog'; +import { + types as ActivityLogTypes, + queries as ActivityLogQueries, + mutations as ActivityLogMutations, +} from './activityLog'; export const types = ` scalar JSON @@ -129,6 +133,7 @@ export const queries = ` ${InsightQueries} ${KnowledgeBaseQueries} ${NotificationQueries} + ${ActivityLogQueries} } `; diff --git a/src/db/models/Companies.js b/src/db/models/Companies.js index cc32031c6..182e22cc4 100644 --- a/src/db/models/Companies.js +++ b/src/db/models/Companies.js @@ -77,7 +77,7 @@ class Company { return this.create(doc); } - /* + /** * Update company * @param {String} _id company id to update * @param {Object} doc field values to update @@ -102,7 +102,7 @@ class Company { return this.findOne({ _id }); } - /* + /** * Create new customer and add to customer's customer list * @return {Promise} newly created customer */ From 44ffa47c3f309517bc3af6df04799136ddfd0108 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 11 Nov 2017 09:42:48 +0800 Subject: [PATCH 244/318] Refactor, fix tests --- src/__tests__/activityLogMutations.test.js | 3 ++- src/__tests__/companyMutations.test.js | 5 +++- src/data/resolvers/activityLog.js | 18 +++++++++++++ src/data/resolvers/activityLogForMonth.js | 31 ++++++++++++++++++++-- src/data/utils.js | 20 +++++++------- src/db/models/ActivityLogs.js | 1 - 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/src/__tests__/activityLogMutations.test.js b/src/__tests__/activityLogMutations.test.js index 2394bee4d..be56c8388 100644 --- a/src/__tests__/activityLogMutations.test.js +++ b/src/__tests__/activityLogMutations.test.js @@ -73,9 +73,10 @@ describe('ActivityLog creation on Customer creation', () => { ); expect(await ActivityLogs.find().count()).toBe(1); + const aLog = await ActivityLogs.findOne({}); - expect(aLog).toBeDefined(); + expect(aLog).toBeDefined(); expect(aLog.activity.type).toBe(ACTIVITY_TYPES.INTERNAL_NOTE); expect(aLog.activity.id).toBe(internalNote._id); expect(aLog.coc.type).toBe(COC_CONTENT_TYPES.CUSTOMER); diff --git a/src/__tests__/companyMutations.test.js b/src/__tests__/companyMutations.test.js index be7b6762a..5d7ea30ca 100644 --- a/src/__tests__/companyMutations.test.js +++ b/src/__tests__/companyMutations.test.js @@ -48,7 +48,10 @@ describe('Companies mutations', () => { }); test('Create company', async () => { - Companies.createCompany = jest.fn(); + Companies.createCompany = jest.fn(() => ({ + name: 'test company name', + _id: 'test company id', + })); const doc = { name: 'name', email: 'dombo@yahoo.com' }; diff --git a/src/data/resolvers/activityLog.js b/src/data/resolvers/activityLog.js index 746f6b33b..4d97aa566 100644 --- a/src/data/resolvers/activityLog.js +++ b/src/data/resolvers/activityLog.js @@ -1,12 +1,30 @@ +/* + * Placeholder object for ActivityLog resolver (used with graphql) + */ export default { + /** + * Finds id of the activity + * @param {ActivityLog} obj - ActivityLog model document + * @return {String} returns id of the activity + */ id(obj) { return obj.activity.id; }, + /** + * Finds action of the activity + * @param {ActivityLog} obj - ActivityLog model document + * @return {String} returns action of the activity + */ action(obj) { return `${obj.activity.type}-${obj.activity.action}`; }, + /** + * Finds content of the activity + * @param {ActivityLog} obj - ActivityLog model document + * @return {String} returns content of the activity + */ content(obj) { return obj.activity.content; }, diff --git a/src/data/resolvers/activityLogForMonth.js b/src/data/resolvers/activityLogForMonth.js index c1cb7bb36..0bfc8557e 100644 --- a/src/data/resolvers/activityLogForMonth.js +++ b/src/data/resolvers/activityLogForMonth.js @@ -1,14 +1,41 @@ import { ActivityLogs } from '../../db/models'; +/* + * Placeholder object for ActivityLogForMonth resolver (used with graphql) + */ export default { + /** + * Returns current date interval + * @param {Object} obj + * @param {Object} obj.date + * @param {Object} obj.date.interval - On object representing month of a year + * @param {Object} obj.date.interval.start - Date object representing start of a month + * @param {Object} obj.date.interval.end - Date object representing end of a month + * @param {Object} obj.date.yearMonth - A dictionary containing year and month int values + * @param {string} obj.contenType - COC_CONTENT_TYPES string + * @param {string} obj.coc - customer or company document/object with _id set + * @return {Object} returns Object {month: int, year: int} + */ date(obj) { return obj.date.yearMonth; }, + /** + * Returns a list of activity logs present in the given date interval + * @param {Object} obj + * @param {Object} obj.date + * @param {Object} obj.date.interval - On object representing month of a year + * @param {Object} obj.date.interval.start - Date object representing start of a month + * @param {Object} obj.date.interval.end - Date object representing end of a month + * @param {Object} obj.date.yearMonth - A dictionary containing year and month int values + * @param {string} obj.contenType - COC_CONTENT_TYPES string + * @param {string} obj.coc - customer or company document/object with _id set + * @return {Promise} returns a Promise resolving a list of ActivityLog resolvers + */ list(obj) { return ActivityLogs.find({ - 'coc.type': obj.customerType, - 'coc.id': obj.customer._id, + 'coc.type': obj.cocContentType, + 'coc.id': obj.coc._id, createdAt: { $gte: obj.date.interval.start, $lt: obj.date.interval.end, diff --git a/src/data/utils.js b/src/data/utils.js index 80e4c8a4e..310ab7359 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -149,8 +149,8 @@ const START_DATE = { }; class BaseMonthActivityBuilder { - constructor(customer) { - this.customer = customer; + constructor(coc) { + this.coc = coc; } getDaysInMonth(year, month) { @@ -196,8 +196,8 @@ class BaseMonthActivityBuilder { for (let date of dates) { list.unshift({ - customer: this.customer, - customerType: this.customerType, + coc: this.coc, + cocContentType: this.cocContentType, date, }); } @@ -207,16 +207,16 @@ class BaseMonthActivityBuilder { } export class CustomerMonthActivityLogBuilder extends BaseMonthActivityBuilder { - constructor(customer) { - super(customer); - this.customerType = COC_CONTENT_TYPES.CUSTOMER; + constructor(coc) { + super(coc); + this.cocContentType = COC_CONTENT_TYPES.CUSTOMER; } } export class CompanyMonthActivityLogBuilder extends BaseMonthActivityBuilder { - constructor(customer) { - super(customer); - this.customerType = COC_CONTENT_TYPES.COMPANY; + constructor(coc) { + super(coc); + this.cocContentType = COC_CONTENT_TYPES.COMPANY; } } diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 68f792bb1..555ede807 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -149,7 +149,6 @@ class ActivityLog { * Create activity log for internal note * @param {InternalNote} internalNote - Internal note document * @param {User} performedBy - User collection document - * @param {Customer|Company} customer - Customer or Company document * @return {Promise} returns Promise resolving created ActivityLog document */ static createInternalNoteLog(internalNote, user) { From 9c1204e21517835ec4cb2e3645a5713167ca91d3 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 11 Nov 2017 09:55:37 +0800 Subject: [PATCH 245/318] Add documentation, fixes based on reviews --- src/__tests__/companyMutations.test.js | 5 ++++- src/data/resolvers/mutations/activityLogs.js | 1 - src/data/resolvers/mutations/companies.js | 12 +++++++----- src/data/resolvers/mutations/customers.js | 3 ++- src/index.js | 1 - 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/__tests__/companyMutations.test.js b/src/__tests__/companyMutations.test.js index 5d7ea30ca..d8515c35d 100644 --- a/src/__tests__/companyMutations.test.js +++ b/src/__tests__/companyMutations.test.js @@ -75,7 +75,10 @@ describe('Companies mutations', () => { }); test('Add customer', async () => { - Companies.addCustomer = jest.fn(); + Companies.addCustomer = jest.fn(() => ({ + name: 'test customer name', + _id: 'test customer id', + })); const doc = { name: 'name', email: 'name@gmail.com' }; diff --git a/src/data/resolvers/mutations/activityLogs.js b/src/data/resolvers/mutations/activityLogs.js index 4777079b7..7cef939ae 100644 --- a/src/data/resolvers/mutations/activityLogs.js +++ b/src/data/resolvers/mutations/activityLogs.js @@ -6,7 +6,6 @@ export default { * @param {Object} root * @param {Object} object2 - arguments * @param {string} customerId - id of customer - * @param {string} conversationId - id of conversation * @param {string} messageId - id of message */ async activitivyLogsAddConversationMessageLog(root, { customerId, messageId }) { diff --git a/src/data/resolvers/mutations/companies.js b/src/data/resolvers/mutations/companies.js index b2311622c..456abdfcb 100644 --- a/src/data/resolvers/mutations/companies.js +++ b/src/data/resolvers/mutations/companies.js @@ -3,7 +3,7 @@ import { moduleRequireLogin } from '../../permissions'; const companyMutations = { /** - * Create new company + * Create new company also adds Company registration log * @return {Promise} company object */ async companiesAdd(root, doc, { user }) { @@ -21,15 +21,17 @@ const companyMutations = { }, /** - * Add new companyId to company's companyIds list + * Add new companyId to company's companyIds list also adds Customer registration log * @param {Object} args - Graphql input data - * @param {String} args._id - Customer id + * @param {String} args._id - Company id * @param {String} args.name - Customer name * @param {String} args.email - Customer email * @return {Promise} newly created customer */ - async companiesAddCustomer(root, args) { - return Companies.addCustomer(args); + async companiesAddCustomer(root, args, { user }) { + const customer = Companies.addCustomer(args); + await ActivityLogs.createCustomerRegistrationLog(customer, user); + return customer; }, }; diff --git a/src/data/resolvers/mutations/customers.js b/src/data/resolvers/mutations/customers.js index 15f9b8198..0164065e9 100644 --- a/src/data/resolvers/mutations/customers.js +++ b/src/data/resolvers/mutations/customers.js @@ -4,7 +4,7 @@ import { moduleRequireLogin } from '../../permissions'; const customerMutations = { /** - * Create new customer + * Create new customer also adds Customer registration log * @return {Promise} customer object */ async customersAdd(root, doc, { user }) { @@ -23,6 +23,7 @@ const customerMutations = { /** * Add new companyId to customer's companyIds list + * also adds a Company registration activity log * @param {Object} args - Graphql input data * @param {String} args._id - Customer id * @param {String} args.name - Company name diff --git a/src/index.js b/src/index.js index d2930bc9b..f363f7270 100755 --- a/src/index.js +++ b/src/index.js @@ -13,7 +13,6 @@ import { connect } from './db/connection'; import { userMiddleware } from './auth'; import schema from './data'; import { init } from './startup'; -import './cronJobs'; // load environment variables dotenv.config(); From 018097831e8a35ec5b1aae57096a12db52fa29d7 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 11 Nov 2017 11:08:59 +0800 Subject: [PATCH 246/318] #22 Add 'by' field on resolver --- src/__tests__/activityLogCronJob.test.js | 9 +++--- src/__tests__/activityLogDb.test.js | 6 ++-- src/__tests__/activityLogMutations.test.js | 5 ++-- src/__tests__/activityLogQueries.test.js | 16 +++++++++++ src/data/constants.js | 26 +++++++++++++++++ src/data/resolvers/activityLog.js | 24 ++++++++++++++++ src/data/schema/activityLog.js | 14 +++++++++ src/db/models/ActivityLogs.js | 33 ++++------------------ 8 files changed, 96 insertions(+), 37 deletions(-) diff --git a/src/__tests__/activityLogCronJob.test.js b/src/__tests__/activityLogCronJob.test.js index a3102ddef..a82f3949e 100644 --- a/src/__tests__/activityLogCronJob.test.js +++ b/src/__tests__/activityLogCronJob.test.js @@ -3,13 +3,14 @@ import { connect, disconnect } from '../db/connection'; import cronJobs from '../cronJobs'; -import { COC_CONTENT_TYPES } from '../data/constants'; -import { customerFactory, segmentFactory } from '../db/factories'; -import ActivityLogs, { +import { + COC_CONTENT_TYPES, ACTIVITY_TYPES, ACTIVITY_ACTIONS, ACTION_PERFORMER_TYPES, -} from '../db/models/ActivityLogs'; +} from '../data/constants'; +import { ActivityLogs } from '../db/models'; +import { customerFactory, segmentFactory } from '../db/factories'; beforeAll(() => connect()); afterAll(() => disconnect()); diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index 600be6deb..4aa61ca94 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -2,13 +2,13 @@ /* eslint-disable no-underscore-dangle */ import { connect, disconnect } from '../db/connection'; -import { COC_CONTENT_TYPES } from '../data/constants'; -import { ActivityLogs, Conversations } from '../db/models'; import { + COC_CONTENT_TYPES, ACTION_PERFORMER_TYPES, ACTIVITY_TYPES, ACTIVITY_ACTIONS, -} from '../db/models/ActivityLogs'; +} from '../data/constants'; +import { ActivityLogs, Conversations } from '../db/models'; import { userFactory, internalNoteFactory, diff --git a/src/__tests__/activityLogMutations.test.js b/src/__tests__/activityLogMutations.test.js index be56c8388..8bdc48f6d 100644 --- a/src/__tests__/activityLogMutations.test.js +++ b/src/__tests__/activityLogMutations.test.js @@ -3,10 +3,9 @@ import { connect, disconnect } from '../db/connection'; import mutations from '../data/resolvers/mutations'; -import { ROLES } from '../data/constants'; -import ActivityLogs, { ACTIVITY_TYPES } from '../db/models/ActivityLogs'; +import { ROLES, ACTIVITY_TYPES, COC_CONTENT_TYPES } from '../data/constants'; +import { ActivityLogs } from '../db/models'; import { userFactory, customerFactory } from '../db/factories'; -import { COC_CONTENT_TYPES } from '../data/constants'; beforeAll(() => connect()); afterAll(() => disconnect()); diff --git a/src/__tests__/activityLogQueries.test.js b/src/__tests__/activityLogQueries.test.js index c1b306878..b83c093e5 100644 --- a/src/__tests__/activityLogQueries.test.js +++ b/src/__tests__/activityLogQueries.test.js @@ -91,6 +91,14 @@ describe('activityLogs', () => { action content createdAt + by { + _id + type + details { + avatar + fullName + } + } } } } @@ -228,6 +236,14 @@ describe('activityLogs', () => { action content createdAt + by { + _id + type + details { + avatar + fullName + } + } } } } diff --git a/src/data/constants.js b/src/data/constants.js index 3ea39f018..e603ff311 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -186,3 +186,29 @@ export const PUBLISH_STATUSES = { PUBLISH: 'publish', ALL: ['draft', 'publish'], }; + +export const ACTIVITY_TYPES = { + CUSTOMER: 'customer', + COMPANY: 'company', + INTERNAL_NOTE: 'internal_note', + CONVERSATION_MESSAGE: 'conversation_message', + SEGMENT: 'segment', + + ALL: ['customer', 'company', 'internal_note', 'conversation_message', 'segment'], +}; + +export const ACTIVITY_ACTIONS = { + CREATE: 'create', + UPDATE: 'update', + DELETE: 'delete', + + ALL: ['create', 'update', 'delete'], +}; + +export const ACTION_PERFORMER_TYPES = { + SYSTEM: 'SYSTEM', + USER: 'USER', + CUSTOMER: 'CUSTOMER', + + ALL: ['SYSTEM', 'USER', 'CUSTOMER'], +}; diff --git a/src/data/resolvers/activityLog.js b/src/data/resolvers/activityLog.js index 4d97aa566..adf5b17ec 100644 --- a/src/data/resolvers/activityLog.js +++ b/src/data/resolvers/activityLog.js @@ -1,3 +1,6 @@ +import { ACTION_PERFORMER_TYPES } from '../../data/constants'; +import { Users } from '../../db/models'; + /* * Placeholder object for ActivityLog resolver (used with graphql) */ @@ -28,4 +31,25 @@ export default { content(obj) { return obj.activity.content; }, + + /** + * Finds content of the activity + * @param {ActivityLog} obj - ActivityLog model document + * @return {Object} returns details with his/her id + */ + async by(obj) { + const performedBy = obj.performedBy; + if (performedBy.type === ACTION_PERFORMER_TYPES.USER) { + const user = await Users.findOne({ _id: performedBy.id }); + return { + _id: user._id, + type: performedBy.type, + details: user.details, + }; + } + return { + type: performedBy.type, + details: {}, + }; + }, }; diff --git a/src/data/schema/activityLog.js b/src/data/schema/activityLog.js index b8cc8c4b2..08eab69ca 100644 --- a/src/data/schema/activityLog.js +++ b/src/data/schema/activityLog.js @@ -9,11 +9,25 @@ export const types = ` list: [ActivityLog]! } + type ActivityLogPerformerDetails { + avatar: String + fullName: String + position: String + twitterUsername: String + } + + type ActivityLogActionPerformer { + _id: String + type: String! + details: ActivityLogPerformerDetails + } + type ActivityLog { action: String! id: String! createdAt: Date! content: String + by: ActivityLogActionPerformer } `; diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 555ede807..5e4290fc8 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -1,32 +1,11 @@ import mongoose, { SchemaTypes } from 'mongoose'; import Random from 'meteor-random'; -import { COC_CONTENT_TYPES } from '../../data/constants'; - -export const ACTIVITY_TYPES = { - CUSTOMER: 'customer', - COMPANY: 'company', - INTERNAL_NOTE: 'internal_note', - CONVERSATION_MESSAGE: 'conversation_message', - SEGMENT: 'segment', - - ALL: ['customer', 'company', 'internal_note', 'conversation_message', 'segment'], -}; - -export const ACTIVITY_ACTIONS = { - CREATE: 'create', - UPDATE: 'update', - DELETE: 'delete', - - ALL: ['create', 'update', 'delete'], -}; - -export const ACTION_PERFORMER_TYPES = { - SYSTEM: 'SYSTEM', - USER: 'USER', - CUSTOMER: 'CUSTOMER', - - ALL: ['SYSTEM', 'USER', 'CUSTOMER'], -}; +import { + COC_CONTENT_TYPES, + ACTION_PERFORMER_TYPES, + ACTIVITY_TYPES, + ACTIVITY_ACTIONS, +} from '../../data/constants'; /* Performer of the action: *system* cron job, user From ab796cca90a0f48f31e60c86e13d0977991c29f8 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sat, 11 Nov 2017 11:17:01 +0800 Subject: [PATCH 247/318] Add a half line of documenation --- src/data/resolvers/mutations/internalNotes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/resolvers/mutations/internalNotes.js b/src/data/resolvers/mutations/internalNotes.js index c32adb0a8..160d12727 100644 --- a/src/data/resolvers/mutations/internalNotes.js +++ b/src/data/resolvers/mutations/internalNotes.js @@ -3,7 +3,7 @@ import { moduleRequireLogin } from '../../permissions'; const internalNoteMutations = { /** - * Adds internalNote object + * Adds internalNote object and also adds an activity log * @return {Promise} */ async internalNotesAdd(root, args, { user }) { From 17801b1255385c19a36dcd5d4ea59b21255b5d97 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 12 Nov 2017 12:42:45 +0800 Subject: [PATCH 248/318] Add field helper & use it in models --- src/db/models/Brands.js | 25 +++---- src/db/models/Channels.js | 26 +++---- src/db/models/Companies.js | 45 ++++++------ src/db/models/ConversationMessages.js | 60 +++++++-------- src/db/models/Conversations.js | 101 ++++++++++++-------------- src/db/models/Customers.js | 83 ++++++++++----------- src/db/models/EmailTemplates.js | 12 +-- src/db/models/Engages.js | 68 ++++++++--------- src/db/models/Fields.js | 32 ++++---- src/db/models/Forms.js | 22 +++--- src/db/models/Integrations.js | 99 ++++++++++++------------- src/db/models/InternalNotes.js | 26 +++---- src/db/models/KnowledgeBase.js | 84 ++++++--------------- src/db/models/Notifications.js | 44 +++++------ src/db/models/ResponseTemplates.js | 18 ++--- src/db/models/Segments.js | 38 +++++----- src/db/models/Tags.js | 20 ++--- src/db/models/Users.js | 47 ++++++------ src/db/models/utils.js | 20 +++++ 19 files changed, 398 insertions(+), 472 deletions(-) create mode 100644 src/db/models/utils.js diff --git a/src/db/models/Brands.js b/src/db/models/Brands.js index cdc84bd3c..1e8b99cdf 100644 --- a/src/db/models/Brands.js +++ b/src/db/models/Brands.js @@ -1,26 +1,23 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; +import { field } from './utils'; const BrandEmailConfigSchema = mongoose.Schema({ - type: { + type: field({ type: String, enum: ['simple', 'custom'], - }, - template: String, + }), + template: field({ type: String }), }); const BrandSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - code: String, - name: String, - description: String, - userId: String, - createdAt: Date, - emailConfig: BrandEmailConfigSchema, + _id: field({ pkey: true }), + code: field({ type: String }), + name: field({ type: String }), + description: field({ type: String }), + userId: field({ type: String }), + createdAt: field({ type: Date }), + emailConfig: field({ type: BrandEmailConfigSchema }), }); class Brand { diff --git a/src/db/models/Channels.js b/src/db/models/Channels.js index 6d6a3f113..7327acd00 100644 --- a/src/db/models/Channels.js +++ b/src/db/models/Channels.js @@ -1,26 +1,26 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; import { createdAtModifier } from '../plugins'; +import { field } from './utils'; // schema for Channels const ChannelSchema = mongoose.Schema({ - _id: { type: String, unique: true, default: () => Random.id() }, - name: String, - description: { + _id: field({ pkey: true }), + name: field({ type: String }), + description: field({ type: String, - required: false, - }, - integrationIds: [String], - memberIds: [String], - userId: String, - conversationCount: { + optional: true, + }), + integrationIds: field({ type: [String] }), + memberIds: field({ type: [String] }), + userId: field({ type: String }), + conversationCount: field({ type: Number, default: 0, - }, - openConversationCount: { + }), + openConversationCount: field({ type: Number, default: 0, - }, + }), }); class Channel { diff --git a/src/db/models/Companies.js b/src/db/models/Companies.js index cc32031c6..85987fd00 100644 --- a/src/db/models/Companies.js +++ b/src/db/models/Companies.js @@ -1,60 +1,57 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; import { Fields, Customers } from './'; +import { field } from './utils'; const CompanySchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - - name: { + _id: field({ pkey: true }), + name: field({ type: String, label: 'Name', unique: true, - }, + }), - size: { + size: field({ type: Number, label: 'Size', optional: true, - }, + }), - industry: { + industry: field({ type: String, label: 'Industry', optional: true, - }, + }), - website: { + website: field({ type: String, label: 'Website', optional: true, - }, + }), - plan: { + plan: field({ type: String, label: 'Plan', optional: true, - }, + }), - lastSeenAt: { + lastSeenAt: field({ type: Date, label: 'Last seen at', - }, + }), - sessionCount: { + sessionCount: field({ type: Number, label: 'Session count', - }, + }), - tagIds: { + tagIds: field({ type: [String], optional: true, - }, + }), - customFieldsData: Object, + customFieldsData: field({ + type: Object, + }), }); class Company { diff --git a/src/db/models/ConversationMessages.js b/src/db/models/ConversationMessages.js index 1da141694..843fb90a7 100644 --- a/src/db/models/ConversationMessages.js +++ b/src/db/models/ConversationMessages.js @@ -1,70 +1,70 @@ import strip from 'strip'; import mongoose from 'mongoose'; -import Random from 'meteor-random'; import { Conversations } from './'; +import { field } from './utils'; const FacebookSchema = mongoose.Schema( { - commentId: { + commentId: field({ type: String, optional: true, - }, + }), // comment, reaction, etc ... - item: { + item: field({ type: String, optional: true, - }, + }), // when share photo - photoId: { + photoId: field({ type: String, optional: true, - }, + }), // when share video - videoId: { + videoId: field({ type: String, optional: true, - }, + }), - link: { + link: field({ type: String, optional: true, - }, + }), - reactionType: { + reactionType: field({ type: String, optional: true, - }, + }), - senderId: { + senderId: field({ type: String, optional: true, - }, + }), - senderName: { + senderName: field({ type: String, optional: true, - }, + }), }, { _id: false }, ); const MessageSchema = mongoose.Schema({ - _id: { type: String, unique: true, default: () => Random.id() }, - content: String, - attachments: Object, - mentionedUserIds: [String], - conversationId: String, - internal: Boolean, - customerId: String, - userId: String, - createdAt: Date, - isCustomerRead: Boolean, - engageData: Object, - formWidgetData: Object, - facebookData: FacebookSchema, + _id: field({ pkey: true }), + content: field({ type: String }), + attachments: field({ type: Object }), + mentionedUserIds: field({ type: [String] }), + conversationId: field({ type: String }), + internal: field({ type: Boolean }), + customerId: field({ type: String }), + userId: field({ type: String }), + createdAt: field({ type: Date }), + isCustomerRead: field({ type: Boolean }), + engageData: field({ type: Object }), + formWidgetData: field({ type: Object }), + facebookData: field({ type: FacebookSchema }), }); class Message { diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index 42b5410e8..cd4de55b4 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -1,23 +1,22 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; import { CONVERSATION_STATUSES, FACEBOOK_DATA_KINDS } from '../../data/constants'; - import { Users } from '../../db/models'; +import { field } from './utils'; const TwitterDirectMessageSchema = mongoose.Schema( { - senderId: { + senderId: field({ type: Number, - }, - senderIdStr: { + }), + senderIdStr: field({ type: String, - }, - recipientId: { + }), + recipientId: field({ type: Number, - }, - recipientIdStr: { + }), + recipientIdStr: field({ type: String, - }, + }), }, { _id: false }, ); @@ -25,22 +24,22 @@ const TwitterDirectMessageSchema = mongoose.Schema( // Twitter schema const TwitterSchema = mongoose.Schema( { - id: { + id: field({ type: Number, - required: false, - }, - idStr: { + optional: true, + }), + idStr: field({ type: String, - }, - screenName: { + }), + screenName: field({ type: String, - }, - isDirectMessage: { + }), + isDirectMessage: field({ type: Boolean, - }, - directMessage: { + }), + directMessage: field({ type: TwitterDirectMessageSchema, - }, + }), }, { _id: false }, ); @@ -48,58 +47,54 @@ const TwitterSchema = mongoose.Schema( // facebook schema const FacebookSchema = mongoose.Schema( { - kind: { + kind: field({ type: String, enum: FACEBOOK_DATA_KINDS.ALL, - }, - senderName: { + }), + senderName: field({ type: String, - }, - senderId: { + }), + senderId: field({ type: String, - }, - recipientId: { + }), + recipientId: field({ type: String, - }, + }), // when wall post - postId: { + postId: field({ type: String, - }, + }), - pageId: { + pageId: field({ type: String, - }, + }), }, { _id: false }, ); // Conversation schema const ConversationSchema = mongoose.Schema({ - _id: { type: String, unique: true, default: () => Random.id() }, - content: String, - integrationId: { - type: String, - required: true, - }, - customerId: String, - userId: String, - assignedUserId: String, - participatedUserIds: [String], - readUserIds: [String], - createdAt: Date, - status: { + _id: field({ pkey: true }), + content: field({ type: String }), + integrationId: field({ type: String }), + customerId: field({ type: String }), + userId: field({ type: String }), + assignedUserId: field({ type: String }), + participatedUserIds: field({ type: [String] }), + readUserIds: field({ type: [String] }), + createdAt: field({ type: Date }), + status: field({ type: String, - required: true, enum: CONVERSATION_STATUSES.ALL, - }, - messageCount: Number, - tagIds: [String], + }), + messageCount: field({ type: Number }), + tagIds: field({ type: [String] }), // number of total conversations - number: Number, - twitterData: TwitterSchema, - facebookData: FacebookSchema, + number: field({ type: Number }), + twitterData: field({ type: TwitterSchema }), + facebookData: field({ type: FacebookSchema }), }); class Conversation { diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index 30ebc6c03..6412418cf 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -1,29 +1,28 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; import { Fields, Companies } from './'; +import { field } from './utils'; /* * messenger schema */ const messengerSchema = mongoose.Schema( { - lastSeenAt: { + lastSeenAt: field({ type: Date, label: 'Messenger: Last online', - }, - sessionCount: { + }), + sessionCount: field({ type: Number, label: 'Messenger: Session count', - }, - isActive: { + }), + isActive: field({ type: Boolean, label: 'Messenger: Is online', - }, - customData: { + }), + customData: field({ type: Object, - blackbox: true, optional: true, - }, + }), }, { _id: false }, ); @@ -33,26 +32,26 @@ const messengerSchema = mongoose.Schema( */ const twitterSchema = mongoose.Schema( { - id: { + id: field({ type: Number, label: 'Twitter: ID (Number)', - }, - idStr: { + }), + idStr: field({ type: String, label: 'Twitter: ID (String)', - }, - name: { + }), + name: field({ type: String, label: 'Twitter: Name', - }, - screenName: { + }), + screenName: field({ type: String, label: 'Twitter: Screen name', - }, - profileImageUrl: { + }), + profileImageUrl: field({ type: String, label: 'Twitter: Profile photo', - }, + }), }, { _id: false }, ); @@ -62,40 +61,36 @@ const twitterSchema = mongoose.Schema( */ const facebookSchema = mongoose.Schema( { - id: { + id: field({ type: String, label: 'Facebook: ID', - }, - profilePic: { + }), + profilePic: field({ type: String, optional: true, label: 'Facebook: Profile photo', - }, + }), }, { _id: false }, ); const CustomerSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - - name: { type: String, label: 'Name' }, - email: { type: String, label: 'Email' }, - phone: { type: String, label: 'Phone' }, - isUser: { type: Boolean, label: 'Is user' }, - createdAt: { type: Date, label: 'Created at' }, - - integrationId: String, - tagIds: [String], - companyIds: [String], - - customFieldsData: Object, - messengerData: messengerSchema, - twitterData: twitterSchema, - facebookData: facebookSchema, + _id: field({ pkey: true }), + + name: field({ type: String, label: 'Name' }), + email: field({ type: String, label: 'Email' }), + phone: field({ type: String, label: 'Phone' }), + isUser: field({ type: Boolean, label: 'Is user' }), + createdAt: field({ type: Date, label: 'Created at' }), + + integrationId: field({ type: String }), + tagIds: field({ type: [String] }), + companyIds: field({ type: [String] }), + + customFieldsData: field({ type: Object }), + messengerData: field({ type: messengerSchema }), + twitterData: field({ type: twitterSchema }), + facebookData: field({ type: facebookSchema }), }); class Customer { diff --git a/src/db/models/EmailTemplates.js b/src/db/models/EmailTemplates.js index b4f18f208..6daa54ade 100644 --- a/src/db/models/EmailTemplates.js +++ b/src/db/models/EmailTemplates.js @@ -1,14 +1,10 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; +import { field } from './utils'; const EmailTemplateSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - name: String, - content: String, + _id: field({ pkey: true }), + name: field({ type: String }), + content: field({ type: String }), }); class EmailTemplate { diff --git a/src/db/models/Engages.js b/src/db/models/Engages.js index e30bb1bbb..843c857ce 100644 --- a/src/db/models/Engages.js +++ b/src/db/models/Engages.js @@ -1,63 +1,63 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; import { MESSENGER_KINDS, SENT_AS_CHOICES, METHODS } from '../../data/constants'; +import { field } from './utils'; const EmailSchema = mongoose.Schema({ - templateId: String, - subject: String, - content: String, + templateId: field({ type: String }), + subject: field({ type: String }), + content: field({ type: String }), }); const RuleSchema = mongoose.Schema({ - _id: String, + _id: field({ type: String }), // browserLanguage, currentUrl, etc ... - kind: String, + kind: field({ type: String }), // Browser language, Current url etc ... - text: String, + text: field({ type: String }), // is, isNot, startsWith - condition: String, + condition: field({ type: String }), - value: String, + value: field({ type: String }), }); const MessengerSchema = mongoose.Schema({ - brandId: String, - kind: { + brandId: field({ type: String }), + kind: field({ type: String, enum: MESSENGER_KINDS.ALL, - }, - sentAs: { + }), + sentAs: field({ type: String, enum: SENT_AS_CHOICES.ALL, - }, - content: String, - rules: [RuleSchema], + }), + content: field({ type: String }), + rules: field({ type: [RuleSchema] }), }); const EngageMessageSchema = mongoose.Schema({ - _id: { type: String, unique: true, default: () => Random.id() }, - kind: String, - segmentId: String, - customerIds: [String], - title: String, - fromUserId: String, - method: { + _id: field({ pkey: true }), + kind: field({ type: String }), + segmentId: field({ type: String }), + customerIds: field({ type: [String] }), + title: field({ type: String }), + fromUserId: field({ type: String }), + method: field({ type: String, enum: METHODS.ALL, - }, - isDraft: Boolean, - isLive: Boolean, - stopDate: Date, - createdDate: Date, - tagIds: [String], - messengerReceivedCustomerIds: [String], - - email: EmailSchema, - messenger: MessengerSchema, - deliveryReports: Object, + }), + isDraft: field({ type: Boolean }), + isLive: field({ type: Boolean }), + stopDate: field({ type: Date }), + createdDate: field({ type: Date }), + tagIds: field({ type: [String] }), + messengerReceivedCustomerIds: field({ type: [String] }), + + email: field({ type: EmailSchema }), + messenger: field({ type: MessengerSchema }), + deliveryReports: field({ type: Object }), }); class Message { diff --git a/src/db/models/Fields.js b/src/db/models/Fields.js index 8242533d8..4f4882125 100644 --- a/src/db/models/Fields.js +++ b/src/db/models/Fields.js @@ -3,40 +3,36 @@ */ import mongoose from 'mongoose'; -import Random from 'meteor-random'; import validator from 'validator'; import { FIELD_CONTENT_TYPES } from '../../data/constants'; import { Forms } from './'; +import { field } from './utils'; const FieldSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, + _id: field({ pkey: true }), // form, customer, company - contentType: String, + contentType: field({ type: String }), // formId when contentType is form - contentTypeId: String, + contentTypeId: field({ type: String }), - type: String, - validation: { + type: field({ type: String }), + validation: field({ type: String, optional: true, - }, - text: String, - description: { + }), + text: field({ type: String }), + description: field({ type: String, optional: true, - }, - options: { + }), + options: field({ type: [String], optional: true, - }, - isRequired: Boolean, - order: Number, + }), + isRequired: field({ type: Boolean }), + order: field({ type: Number }), }); class Field { diff --git a/src/db/models/Forms.js b/src/db/models/Forms.js index a5e8037a6..6ffdc85db 100644 --- a/src/db/models/Forms.js +++ b/src/db/models/Forms.js @@ -2,24 +2,22 @@ import mongoose from 'mongoose'; import Random from 'meteor-random'; import { Integrations, Fields } from './'; import { FIELD_CONTENT_TYPES } from '../../data/constants'; +import { field } from './utils'; // schema for form document const FormSchema = mongoose.Schema({ - _id: { + _id: field({ pkey: true }), + title: field({ type: String }), + description: field({ type: String, - default: () => Random.id(), - }, - title: String, - description: { - type: String, - required: false, - }, - code: String, - createdUserId: String, - createdDate: { + optional: true, + }), + code: field({ type: String }), + createdUserId: field({ type: String }), + createdDate: field({ type: Date, default: Date.now, - }, + }), }); class Form { diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index 661e670c7..7c8bcbf17 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -1,6 +1,5 @@ import mongoose from 'mongoose'; import 'mongoose-type-email'; -import Random from 'meteor-random'; import { ConversationMessages, Conversations } from './'; import { Customers } from './'; import { @@ -11,13 +10,14 @@ import { } from '../../data/constants'; import { TwitterSchema, FacebookSchema } from '../../social/schemas'; +import { field } from './utils'; // subdocument schema for MessengerOnlineHours const MessengerOnlineHoursSchema = mongoose.Schema( { - day: String, - from: String, - to: String, + day: field({ type: String }), + from: field({ type: String }), + to: field({ type: String }), }, { _id: false }, ); @@ -25,19 +25,19 @@ const MessengerOnlineHoursSchema = mongoose.Schema( // subdocument schema for MessengerData const MessengerDataSchema = mongoose.Schema( { - notifyCustomer: Boolean, - availabilityMethod: { + notifyCustomer: field({ type: Boolean }), + availabilityMethod: field({ type: String, enum: MESSENGER_DATA_AVAILABILITY.ALL, - }, - isOnline: { + }), + isOnline: field({ type: Boolean, - }, - onlineHours: [MessengerOnlineHoursSchema], - timezone: String, - welcomeMessage: String, - awayMessage: String, - thankYouMessage: String, + }), + onlineHours: field({ type: [MessengerOnlineHoursSchema] }), + timezone: field({ type: String }), + welcomeMessage: field({ type: String }), + awayMessage: field({ type: String }), + thankYouMessage: field({ type: String }), }, { _id: false }, ); @@ -45,46 +45,46 @@ const MessengerDataSchema = mongoose.Schema( // subdocument schema for FormData const FormDataSchema = mongoose.Schema( { - loadType: { + loadType: field({ type: String, enum: FORM_LOAD_TYPES.ALL, - }, - successAction: { + }), + successAction: field({ type: String, enum: FORM_SUCCESS_ACTIONS.ALL, - }, - fromEmail: { + }), + fromEmail: field({ type: String, optional: true, - }, - userEmailTitle: { + }), + userEmailTitle: field({ type: String, optional: true, - }, - userEmailContent: { + }), + userEmailContent: field({ type: String, optional: true, - }, - adminEmails: { + }), + adminEmails: field({ type: [String], optional: true, - }, - adminEmailTitle: { + }), + adminEmailTitle: field({ type: String, optional: true, - }, - adminEmailContent: { + }), + adminEmailContent: field({ type: String, optional: true, - }, - thankContent: { + }), + thankContent: field({ type: String, optional: true, - }, - redirectUrl: { + }), + redirectUrl: field({ type: String, optional: true, - }, + }), }, { _id: false }, ); @@ -92,31 +92,28 @@ const FormDataSchema = mongoose.Schema( // subdocument schema for messenger UiOptions const UiOptionsSchema = mongoose.Schema( { - color: String, - wallpaper: String, - logo: String, + color: field({ type: String }), + wallpaper: field({ type: String }), + logo: field({ type: String }), }, { _id: false }, ); // schema for integration document const IntegrationSchema = mongoose.Schema({ - _id: { - type: String, - default: () => Random.id(), - }, - kind: { + _id: field({ pkey: true }), + kind: field({ type: String, enum: KIND_CHOICES.ALL, - }, - name: String, - brandId: String, - formId: String, - formData: FormDataSchema, - messengerData: MessengerDataSchema, - twitterData: TwitterSchema, - facebookData: FacebookSchema, - uiOptions: UiOptionsSchema, + }), + name: field({ type: String }), + brandId: field({ type: String }), + formId: field({ type: String }), + formData: field({ type: FormDataSchema }), + messengerData: field({ type: MessengerDataSchema }), + twitterData: field({ type: TwitterSchema }), + facebookData: field({ type: FacebookSchema }), + uiOptions: field({ type: UiOptionsSchema }), }); class Integration { diff --git a/src/db/models/InternalNotes.js b/src/db/models/InternalNotes.js index 76d399607..0a5500a10 100644 --- a/src/db/models/InternalNotes.js +++ b/src/db/models/InternalNotes.js @@ -1,30 +1,26 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; import { INTERNAL_NOTE_CONTENT_TYPES } from '../../data/constants'; +import { field } from './utils'; /* * internal note schema */ const InternalNoteSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - contentType: { + _id: field({ pkey: true }), + contentType: field({ type: String, enum: INTERNAL_NOTE_CONTENT_TYPES.ALL, - }, - contentTypeId: String, - content: { + }), + contentTypeId: field({ type: String }), + content: field({ type: String, - }, - createdUserId: { + }), + createdUserId: field({ type: String, - }, - createdDate: { + }), + createdDate: field({ type: Date, - }, + }), }); class InternalNote { diff --git a/src/db/models/KnowledgeBase.js b/src/db/models/KnowledgeBase.js index 901020da4..b58bdad82 100644 --- a/src/db/models/KnowledgeBase.js +++ b/src/db/models/KnowledgeBase.js @@ -1,16 +1,16 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; import { PUBLISH_STATUSES } from '../../data/constants'; +import { field } from './utils'; // Schema for common fields const commonFields = { - createdBy: String, - createdDate: { + createdBy: field({ type: String }), + createdDate: field({ type: Date, default: new Date(), - }, - modifiedBy: String, - modifiedDate: Date, + }), + modifiedBy: field({ type: String }), + modifiedDate: field({ type: Date }), }; /** @@ -73,26 +73,15 @@ class KnowledgeBaseCommonDocument { } const ArticleSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - title: { - type: String, - required: true, - }, - summary: String, - content: { - type: String, - required: true, - }, - status: { + _id: field({ pkey: true }), + title: field({ type: String }), + summary: field({ type: String }), + content: field({ type: String }), + status: field({ type: String, enum: PUBLISH_STATUSES.ALL, default: PUBLISH_STATUSES.DRAFT, - required: true, - }, + }), ...commonFields, }); @@ -143,27 +132,11 @@ class Article extends KnowledgeBaseCommonDocument { } const CategorySchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - title: { - type: String, - required: true, - }, - description: { - type: String, - required: false, - }, - articleIds: { - type: [String], - required: false, - }, - icon: { - type: String, - required: true, - }, + _id: field({ pkey: true }), + title: field({ type: String }), + description: field({ type: String }), + articleIds: field({ type: [String] }), + icon: field({ type: String }), ...commonFields, }); @@ -218,25 +191,14 @@ class Category extends KnowledgeBaseCommonDocument { } const TopicSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - title: { - type: String, - required: true, - }, - description: String, - brandId: { - type: String, - required: true, - validate: /\S+/, - }, - categoryIds: { + _id: field({ pkey: true }), + title: field({ type: String }), + description: field({ type: String }), + brandId: field({ type: String }), + categoryIds: field({ type: [String], required: false, - }, + }), ...commonFields, }); diff --git a/src/db/models/Notifications.js b/src/db/models/Notifications.js index 4b4a7da56..2edf59d27 100644 --- a/src/db/models/Notifications.js +++ b/src/db/models/Notifications.js @@ -1,31 +1,27 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; import { NOTIFICATION_TYPES } from '../../data/constants'; +import { field } from './utils'; // Notification schema const NotificationSchema = new mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - notifType: { + _id: field({ pkey: true }), + notifType: field({ type: String, enum: NOTIFICATION_TYPES.ALL, - }, - title: String, - link: String, - content: String, - createdUser: String, - receiver: String, - date: { + }), + title: field({ type: String }), + link: field({ type: String }), + content: field({ type: String }), + createdUser: field({ type: String }), + receiver: field({ type: String }), + date: field({ type: Date, default: Date.now, - }, - isRead: { + }), + isRead: field({ type: Boolean, default: false, - }, + }), }); class Notification { @@ -104,18 +100,14 @@ export const Notifications = mongoose.model('notifications', NotificationSchema) // schema for NotificationConfigurations const ConfigSchema = new mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, + _id: field({ pkey: true }), // to whom this config is related - user: String, - notifType: { + user: field({ type: String }), + notifType: field({ type: String, enum: NOTIFICATION_TYPES.ALL, - }, - isAllowed: Boolean, + }), + isAllowed: field({ type: Boolean }), }); class Configuration { diff --git a/src/db/models/ResponseTemplates.js b/src/db/models/ResponseTemplates.js index 22157ff17..e7b3775fe 100644 --- a/src/db/models/ResponseTemplates.js +++ b/src/db/models/ResponseTemplates.js @@ -1,18 +1,12 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; +import { field } from './utils'; const ResponseTemplateSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - name: String, - content: String, - brandId: String, - files: { - type: Array, - }, + _id: field({ pkey: true }), + name: field({ type: String }), + content: field({ type: String }), + brandId: field({ type: String }), + files: field({ type: Array }), }); class ResponseTemplate { diff --git a/src/db/models/Segments.js b/src/db/models/Segments.js index 5fb4354f8..3f588b832 100644 --- a/src/db/models/Segments.js +++ b/src/db/models/Segments.js @@ -1,42 +1,38 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; import { SEGMENT_CONTENT_TYPES } from '../../data/constants'; +import { field } from './utils'; const ConditionSchema = mongoose.Schema( { - field: String, - operator: String, - type: String, + field: field({ type: String }), + operator: field({ type: String }), + type: field({ type: String }), - value: { + value: field({ type: String, optional: true, - }, + }), - dateUnit: { + dateUnit: field({ type: String, optional: true, - }, + }), }, { _id: false }, ); const SegmentSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - contentType: { + _id: field({ pkey: true }), + contentType: field({ type: String, enum: SEGMENT_CONTENT_TYPES.ALL, - }, - name: String, - description: String, - subOf: String, - color: String, - connector: String, - conditions: [ConditionSchema], + }), + name: field({ type: String }), + description: field({ type: String }), + subOf: field({ type: String }), + color: field({ type: String }), + connector: field({ type: String }), + conditions: field({ type: [ConditionSchema] }), }); class Segment { diff --git a/src/db/models/Tags.js b/src/db/models/Tags.js index 0d334223a..b7d81e697 100644 --- a/src/db/models/Tags.js +++ b/src/db/models/Tags.js @@ -1,23 +1,19 @@ import _ from 'underscore'; import mongoose from 'mongoose'; -import Random from 'meteor-random'; import { TAG_TYPES } from '../../data/constants'; import { Customers, Conversations, EngageMessages } from '.'; +import { field } from './utils'; const TagSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - name: String, - type: { + _id: field({ pkey: true }), + name: field({ type: String }), + type: field({ type: String, enum: TAG_TYPES.ALL, - }, - colorCode: String, - createdAt: Date, - objectCount: Number, + }), + colorCode: field({ type: String }), + createdAt: field({ type: Date }), + objectCount: field({ type: Number }), }); /* diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 2715fbd27..835acc334 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -1,55 +1,54 @@ import mongoose from 'mongoose'; -import Random from 'meteor-random'; import bcrypt from 'bcrypt'; import crypto from 'crypto'; import sha256 from 'sha256'; import jwt from 'jsonwebtoken'; import { ROLES } from '../../data/constants'; +import { field } from './utils'; const SALT_WORK_FACTOR = 10; const EmailSignatureSchema = mongoose.Schema( - { brandId: String, signature: String }, + { + brandId: field({ type: String }), + signature: field({ type: String }), + }, { _id: false }, ); // Detail schema const DetailSchema = mongoose.Schema( { - avatar: String, - fullName: String, - position: String, - twitterUsername: String, + avatar: field({ type: String }), + fullName: field({ type: String }), + position: field({ type: String }), + twitterUsername: field({ type: String }), }, { _id: false }, ); // User schema const UserSchema = mongoose.Schema({ - _id: { - type: String, - unique: true, - default: () => Random.id(), - }, - username: String, - password: String, - resetPasswordToken: String, - resetPasswordExpires: Date, - role: { + _id: field({ pkey: true }), + username: field({ type: String }), + password: field({ type: String }), + resetPasswordToken: field({ type: String }), + resetPasswordExpires: field({ type: Date }), + role: field({ type: String, enum: [ROLES.ADMIN, ROLES.CONTRIBUTOR], - }, - isOwner: Boolean, - email: { + }), + isOwner: field({ type: Boolean }), + email: field({ type: String, lowercase: true, unique: true, match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 'Please fill a valid email address'], - }, - getNotificationByEmail: Boolean, - emailSignatures: [EmailSignatureSchema], - starredConversationIds: [String], - details: DetailSchema, + }), + getNotificationByEmail: field({ type: Boolean }), + emailSignatures: field({ type: [EmailSignatureSchema] }), + starredConversationIds: field({ type: [String] }), + details: field({ type: DetailSchema }), }); class User { diff --git a/src/db/models/utils.js b/src/db/models/utils.js new file mode 100644 index 000000000..051246cef --- /dev/null +++ b/src/db/models/utils.js @@ -0,0 +1,20 @@ +import Random from 'meteor-random'; + +/* + * Mongoose field options wrapper + */ +export const field = options => { + const { pkey, type, optional } = options; + + if (type === String && !pkey && !optional) { + options.validate = /\S+/; + } + + if (pkey) { + options.type = String; + options.unique = true; + options.default = () => Random.id(); + } + + return options; +}; From e2cae25683c7d21c78eee3fe3ae83d10810fee5c Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 12 Nov 2017 15:29:15 +0800 Subject: [PATCH 249/318] Add addMessage twitterReply test --- src/__tests__/conversationMutations.test.js | 60 +++++++++++++++++++++ src/cronJobs/conversations.js | 3 ++ src/db/models/Integrations.js | 1 + 3 files changed, 64 insertions(+) diff --git a/src/__tests__/conversationMutations.test.js b/src/__tests__/conversationMutations.test.js index 76ca2ed5f..61551ecb4 100644 --- a/src/__tests__/conversationMutations.test.js +++ b/src/__tests__/conversationMutations.test.js @@ -1,6 +1,8 @@ /* eslint-env jest */ /* eslint-disable no-underscore-dangle */ +import Twit from 'twit'; +import sinon from 'sinon'; import { connect, disconnect } from '../db/connection'; import { Conversations, ConversationMessages, Users, Customers, Integrations } from '../db/models'; import { @@ -11,6 +13,7 @@ import { customerFactory, } from '../db/factories'; import conversationMutations from '../data/resolvers/mutations/conversations'; +import { TwitMap } from '../social/twitter'; import utils from '../data/utils'; beforeAll(() => connect()); @@ -49,6 +52,11 @@ describe('Conversation message mutations', () => { await Users.remove({}); await Integrations.remove({}); await Customers.remove({}); + + // restore mocks + if (utils.sendNotification.mock) { + utils.sendNotification.mockRestore(); + } }); test('Conversation login required functions', async () => { @@ -147,6 +155,58 @@ describe('Conversation message mutations', () => { } }); + test('Add conversation message: twitter reply', async () => { + // mock utils functions =============== + ConversationMessages.addMessage = jest.fn(() => ({ _id: 'messageObject' })); + const spyNotification = jest.spyOn(utils, 'sendNotification'); + const spyEmail = jest.spyOn(utils, 'sendEmail'); + + // mock Twit instance + const twit = new Twit({ + consumer_key: 'consumer_key', + consumer_secret: 'consumer_secret', + access_token: 'access_token', + access_token_secret: 'token_secret', + }); + + // mock twitter request + const sandbox = sinon.sandbox.create(); + const stub = sandbox.stub(twit, 'post').callsFake(() => {}); + + // creating doc =================== + const integration = await integrationFactory({ kind: 'twitter' }); + const conversation = await conversationFactory({ + integrationId: integration._id, + twitterData: { + isDirectMessage: true, + directMessage: { + senderIdStr: 'sender_id', + }, + }, + }); + + TwitMap[integration._id] = twit; + + _doc.conversationId = conversation._id; + _doc.internal = false; + + // call mutation + await conversationMutations.conversationMessageAdd({}, _doc, { user: _user }); + + // check utils function calls + expect(ConversationMessages.addMessage.mock.calls.length).toBe(1); + expect(spyNotification.mock.calls.length).toBe(1); + expect(spyEmail.mock.calls.length).toBe(1); + + // check twit post params + expect( + stub.calledWith('direct_messages/new', { + user_id: 'sender_id', + text: _doc.content, + }), + ).toBe(true); + }); + // if user assigned to conversation test('Assign conversation to employee', async () => { Conversations.assignUserConversation = jest.fn(() => [_conversation]); diff --git a/src/cronJobs/conversations.js b/src/cronJobs/conversations.js index d2115d423..e90a55e68 100644 --- a/src/cronJobs/conversations.js +++ b/src/cronJobs/conversations.js @@ -21,6 +21,7 @@ export const sendMessageEmail = async () => { for (let conversation of conversations) { const customer = await Customers.findOne({ _id: conversation.customerId }); const integration = await Integrations.findOne({ _id: conversation.integrationId }); + const brand = await Brands.findOne({ _id: integration.brandId }); if (!customer || !customer.email) { @@ -99,3 +100,5 @@ export const sendMessageEmail = async () => { schedule.scheduleJob('*/10 * * * *', function() { sendMessageEmail(); }); + +sendMessageEmail(); diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index 7c8bcbf17..957e00dd7 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -52,6 +52,7 @@ const FormDataSchema = mongoose.Schema( successAction: field({ type: String, enum: FORM_SUCCESS_ACTIONS.ALL, + optional: true, }), fromEmail: field({ type: String, From 9d55f4a717f58d67d2397183506f00531c757a9d Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 12 Nov 2017 17:03:02 +0800 Subject: [PATCH 250/318] Add facebookReply test in addMessage --- src/__tests__/conversationMutations.test.js | 64 ++++++++++++++++++--- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/src/__tests__/conversationMutations.test.js b/src/__tests__/conversationMutations.test.js index 61551ecb4..7c8f80cbc 100644 --- a/src/__tests__/conversationMutations.test.js +++ b/src/__tests__/conversationMutations.test.js @@ -14,7 +14,9 @@ import { } from '../db/factories'; import conversationMutations from '../data/resolvers/mutations/conversations'; import { TwitMap } from '../social/twitter'; +import { graphRequest } from '../social/facebookTracker'; import utils from '../data/utils'; +import { FACEBOOK_DATA_KINDS } from '../data/constants'; beforeAll(() => connect()); @@ -158,8 +160,8 @@ describe('Conversation message mutations', () => { test('Add conversation message: twitter reply', async () => { // mock utils functions =============== ConversationMessages.addMessage = jest.fn(() => ({ _id: 'messageObject' })); - const spyNotification = jest.spyOn(utils, 'sendNotification'); - const spyEmail = jest.spyOn(utils, 'sendEmail'); + jest.spyOn(utils, 'sendNotification'); + jest.spyOn(utils, 'sendEmail'); // mock Twit instance const twit = new Twit({ @@ -193,11 +195,6 @@ describe('Conversation message mutations', () => { // call mutation await conversationMutations.conversationMessageAdd({}, _doc, { user: _user }); - // check utils function calls - expect(ConversationMessages.addMessage.mock.calls.length).toBe(1); - expect(spyNotification.mock.calls.length).toBe(1); - expect(spyEmail.mock.calls.length).toBe(1); - // check twit post params expect( stub.calledWith('direct_messages/new', { @@ -207,6 +204,59 @@ describe('Conversation message mutations', () => { ).toBe(true); }); + test('Add conversation message: facebook reply', async () => { + // mock settings + process.env.FACEBOOK = JSON.stringify([ + { + id: '1', + name: 'name', + accessToken: 'access_token', + }, + ]); + + // mock utils functions =============== + ConversationMessages.addMessage = jest.fn(() => ({ _id: 'messageObject' })); + jest.spyOn(utils, 'sendNotification'); + jest.spyOn(utils, 'sendEmail'); + + // mock graph api requests + sinon.stub(graphRequest, 'get').callsFake(() => ({ access_token: 'access_token' })); + const stub = sinon.stub(graphRequest, 'post').callsFake(() => ({ id: 'id' })); + + // factories ============ + const integration = await integrationFactory({ + kind: 'facebook', + facebookData: { + appId: '1', + }, + }); + + const conversation = await conversationFactory({ + integrationId: integration._id, + facebookData: { + kind: FACEBOOK_DATA_KINDS.FEED, + senderId: 'senderId', + pageId: 'id', + postId: 'postId', + }, + }); + + _doc.conversationId = conversation._id; + _doc.internal = false; + + // call mutation + await conversationMutations.conversationMessageAdd({}, _doc, { user: _user }); + + // check stub ============== + expect(stub.calledOnce).toBe(true); + + const [arg1, arg2, arg3] = stub.firstCall.args; + + expect(arg1).toBe('postId/comments'); + expect(arg2).toBe('access_token'); + expect(arg3).toEqual({ message: _doc.content }); + }); + // if user assigned to conversation test('Assign conversation to employee', async () => { Conversations.assignUserConversation = jest.fn(() => [_conversation]); From 78461f0e04c4b26a28ea326a56a96186b4eb118c Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 12 Nov 2017 19:43:22 +0800 Subject: [PATCH 251/318] Add changes based on reviews --- src/__tests__/activityLogUtils.test.js | 15 --- .../activityLogForMonthQueryBuilder.js | 100 ++++++++++++++++++ src/data/resolvers/queries/activityLogs.js | 13 ++- src/data/utils.js | 80 -------------- src/db/models/ActivityLogs.js | 85 +++++++-------- 5 files changed, 144 insertions(+), 149 deletions(-) delete mode 100644 src/__tests__/activityLogUtils.test.js create mode 100644 src/data/resolvers/queries/activityLogForMonthQueryBuilder.js diff --git a/src/__tests__/activityLogUtils.test.js b/src/__tests__/activityLogUtils.test.js deleted file mode 100644 index 1f0eac293..000000000 --- a/src/__tests__/activityLogUtils.test.js +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-env jest */ -/* eslint-disable no-underscore-dangle */ - -import { CustomerMonthActivityLogBuilder } from '../data/utils'; - -describe('activityLogUtils', () => { - test('MonthActivityLogBuilder', () => { - const customer = { - _id: 'customerId', - name: 'test customer name', - }; - - new CustomerMonthActivityLogBuilder(customer); - }); -}); diff --git a/src/data/resolvers/queries/activityLogForMonthQueryBuilder.js b/src/data/resolvers/queries/activityLogForMonthQueryBuilder.js new file mode 100644 index 000000000..edf656462 --- /dev/null +++ b/src/data/resolvers/queries/activityLogForMonthQueryBuilder.js @@ -0,0 +1,100 @@ +import { COC_CONTENT_TYPES } from '../../constants'; + +const START_DATE = { + year: 2017, + month: 0, +}; + +class BaseMonthActivityBuilder { + constructor(coc) { + this.coc = coc; + } + + /** + * Get the number of days in the given month + * @param {int} year - Year + * @param {int} month - Month [0..11] + * @return {int} returns number of days in the given month + */ + getDaysInMonth(year, month) { + return new Date(year, month, 0).getDate(); + } + + /** + * Generate dates with interval dates used to query ActivityLogs + * @return {Object} return a list of month objects with yearMonth: { year: int, month: int }, + * interval: { start: Date, year: Date } objects + */ + generateDates() { + const now = new Date(); + + const endYear = now.getFullYear(); + const endMonth = now.getMonth(); + + const monthIntervals = []; + + for ( + let year = START_DATE.year, month = START_DATE.month; + year < endYear || (year === endYear && month <= endMonth); + month++ + ) { + monthIntervals.push({ + yearMonth: { + year, + month, + }, + interval: { + start: new Date(year, month, 1), + end: new Date(year, month, this.getDaysInMonth(year, month)), + }, + }); + + if ((month + 1) % 12 == 0) { + month = 0; + year++; + } + } + + return monthIntervals; + } + + /** + * Build month intervals and collect ActivityLogForMonth resolver placeholders into them + * @return Month interval objects containing activitylogs for that month + */ + build() { + const dates = this.generateDates(); + const list = []; + + for (let date of dates) { + list.unshift({ + coc: this.coc, + cocContentType: this.cocContentType, + date, + }); + } + + return list; + } +} + +// Monthly log builder for customers +export class CustomerMonthActivityLogBuilder extends BaseMonthActivityBuilder { + constructor(coc) { + super(coc); + this.cocContentType = COC_CONTENT_TYPES.CUSTOMER; + } +} + +// Monthly log builder for companies +export class CompanyMonthActivityLogBuilder extends BaseMonthActivityBuilder { + constructor(coc) { + super(coc); + this.cocContentType = COC_CONTENT_TYPES.COMPANY; + } +} + +export default { + CustomerMonthActivityLogBuilder, + CompanyMonthActivityLogBuilder, +}; diff --git a/src/data/resolvers/queries/activityLogs.js b/src/data/resolvers/queries/activityLogs.js index 43434c94b..cf6c179b9 100644 --- a/src/data/resolvers/queries/activityLogs.js +++ b/src/data/resolvers/queries/activityLogs.js @@ -1,6 +1,9 @@ import { Customers, Companies } from '../../../db/models'; import { moduleRequireLogin } from '../../permissions'; -import { CustomerMonthActivityLogBuilder, CompanyMonthActivityLogBuilder } from '../../utils'; +import { + CustomerMonthActivityLogBuilder, + CompanyMonthActivityLogBuilder, +} from './activityLogForMonthQueryBuilder'; const activityLogQueries = { /** @@ -13,8 +16,8 @@ const activityLogQueries = { async activityLogsCustomer(root, { _id }) { const customer = await Customers.findOne({ _id }); - const m = new CustomerMonthActivityLogBuilder(customer); - return m.build(); + const companyMonthActivityLogBuilder = new CustomerMonthActivityLogBuilder(customer); + return companyMonthActivityLogBuilder.build(); }, /** @@ -27,8 +30,8 @@ const activityLogQueries = { async activityLogsCompany(root, { _id }) { const company = await Companies.findOne({ _id }); - const m = new CompanyMonthActivityLogBuilder(company); - return m.build(); + const companyMonthActivityLogBuilder = new CompanyMonthActivityLogBuilder(company); + return companyMonthActivityLogBuilder.build(); }, }; diff --git a/src/data/utils.js b/src/data/utils.js index 310ab7359..afe76b56f 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -2,7 +2,6 @@ import fs from 'fs'; import nodemailer from 'nodemailer'; import Handlebars from 'handlebars'; import { Notifications, Users } from '../db/models'; -import { COC_CONTENT_TYPES } from './constants'; /** * Read contents of a file @@ -143,88 +142,9 @@ export const sendNotification = async ({ createdUser, receivers, ...doc }) => { }); }; -const START_DATE = { - year: 2017, - month: 0, -}; - -class BaseMonthActivityBuilder { - constructor(coc) { - this.coc = coc; - } - - getDaysInMonth(year, month) { - return new Date(year, month, 0).getDate(); - } - - generateDates() { - const now = new Date(); - - const endYear = now.getFullYear(); - const endMonth = now.getMonth(); - - const monthIntervals = []; - - for ( - let year = START_DATE.year, month = START_DATE.month; - year < endYear || (year === endYear && month <= endMonth); - month++ - ) { - monthIntervals.push({ - yearMonth: { - year, - month, - }, - interval: { - start: new Date(year, month, 1), - end: new Date(year, month, this.getDaysInMonth(year, month)), - }, - }); - - if ((month + 1) % 12 == 0) { - month = 0; - year++; - } - } - - return monthIntervals; - } - - build() { - const dates = this.generateDates(); - const list = []; - - for (let date of dates) { - list.unshift({ - coc: this.coc, - cocContentType: this.cocContentType, - date, - }); - } - - return list; - } -} - -export class CustomerMonthActivityLogBuilder extends BaseMonthActivityBuilder { - constructor(coc) { - super(coc); - this.cocContentType = COC_CONTENT_TYPES.CUSTOMER; - } -} - -export class CompanyMonthActivityLogBuilder extends BaseMonthActivityBuilder { - constructor(coc) { - super(coc); - this.cocContentType = COC_CONTENT_TYPES.COMPANY; - } -} - export default { sendEmail, sendNotification, readFile, createTransporter, - CustomerMonthActivityLogBuilder, - CompanyMonthActivityLogBuilder, }; diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 7dd63731f..22f105bd6 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -71,7 +71,7 @@ const Activity = mongoose.Schema( /* the customer that is related to a given ActivityLog can be both Company or Customer documents */ -const Customer = mongoose.Schema( +const COC = mongoose.Schema( { id: field({ type: String, @@ -90,7 +90,7 @@ const ActivityLogSchema = mongoose.Schema({ _id: field({ pkey: true }), activity: Activity, performedBy: ActionPerformer, - coc: Customer, + coc: COC, createdAt: field({ type: Date, @@ -141,6 +141,35 @@ class ActivityLog { }); } + static cocFindOne(messageId, cocId, cocType) { + return this.findOne({ + 'activity.type': ACTIVITY_TYPES.CONVERSATION_MESSAGE, + 'activity.action': ACTIVITY_ACTIONS.CREATE, + 'activity.id': messageId, + 'coc.type': cocType, + 'performedBy.type': ACTION_PERFORMER_TYPES.CUSTOMER, + 'coc.id': cocId, + }); + } + + static cocCreate(messageId, content, cocId, cocType) { + return this.createDoc({ + activity: { + type: ACTIVITY_TYPES.CONVERSATION_MESSAGE, + action: ACTIVITY_ACTIONS.CREATE, + content: content, + id: messageId, + }, + performedBy: { + type: ACTION_PERFORMER_TYPES.CUSTOMER, + }, + coc: { + type: cocType, + id: cocId, + }, + }); + } + /** * Create a conversation message log for a given customer, * if the customer is related to companies, @@ -159,68 +188,26 @@ class ActivityLog { if (customer.companyIds && customer.companyIds.length > 0) { for (let companyId of customer.companyIds) { // check against duplication - const foundLog = await this.findOne({ - 'activity.type': ACTIVITY_TYPES.CONVERSATION_MESSAGE, - 'activity.action': ACTIVITY_ACTIONS.CREATE, - 'activity.id': message._id, - 'coc.type': COC_CONTENT_TYPES.COMPANY, - 'performedBy.type': ACTION_PERFORMER_TYPES.CUSTOMER, - 'coc.id': companyId, - }); + const foundLog = await this.cocFindOne(message._id, companyId, COC_CONTENT_TYPES.COMPANY); if (!foundLog) { - await this.createDoc({ - activity: { - type: ACTIVITY_TYPES.CONVERSATION_MESSAGE, - action: ACTIVITY_ACTIONS.CREATE, - content: message.content, - id: message._id, - }, - performedBy: { - type: ACTION_PERFORMER_TYPES.CUSTOMER, - }, - coc: { - type: COC_CONTENT_TYPES.COMPANY, - id: companyId, - }, - }); + await this.cocCreate(message._id, message.content, companyId, COC_CONTENT_TYPES.COMPANY); } } } // check against duplication ====== - const foundLog = await this.findOne({ - 'activity.type': ACTIVITY_TYPES.CONVERSATION_MESSAGE, - 'activity.action': ACTIVITY_ACTIONS.CREATE, - 'activity.id': message._id, - 'performedBy.type': ACTION_PERFORMER_TYPES.CUSTOMER, - 'coc.type': COC_CONTENT_TYPES.CUSTOMER, - 'coc.id': customer._id, - }); + const foundLog = await this.cocFindOne(message._id, customer._id, COC_CONTENT_TYPES.CUSTOMER); if (!foundLog) { - return this.createDoc({ - activity: { - type: ACTIVITY_TYPES.CONVERSATION_MESSAGE, - action: ACTIVITY_ACTIONS.CREATE, - content: message.content, - id: message._id, - }, - performedBy: { - type: ACTION_PERFORMER_TYPES.CUSTOMER, - }, - coc: { - type: COC_CONTENT_TYPES.CUSTOMER, - id: customer._id, - }, - }); + return this.cocCreate(message._id, message.content, customer._id, COC_CONTENT_TYPES.CUSTOMER); } } /** * Create a customer or company segment log * @param {Segment} segment - Segment document - * @param {Customer} customer - Related customer or company + * @param {COC} customer - Related customer or company * @return {Promise} return Promise resolving created Segment */ static async createSegmentLog(segment, customer) { From 58e58e44a5be38ccd6bdbdd4889f2d6160fe3168 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 12 Nov 2017 19:54:04 +0800 Subject: [PATCH 252/318] Fix typo --- src/data/resolvers/queries/activityLogs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/resolvers/queries/activityLogs.js b/src/data/resolvers/queries/activityLogs.js index cf6c179b9..8e9818ae2 100644 --- a/src/data/resolvers/queries/activityLogs.js +++ b/src/data/resolvers/queries/activityLogs.js @@ -16,8 +16,8 @@ const activityLogQueries = { async activityLogsCustomer(root, { _id }) { const customer = await Customers.findOne({ _id }); - const companyMonthActivityLogBuilder = new CustomerMonthActivityLogBuilder(customer); - return companyMonthActivityLogBuilder.build(); + const customerMonthActivityLogBuilder = new CustomerMonthActivityLogBuilder(customer); + return customerMonthActivityLogBuilder.build(); }, /** From d98dfc6b3e4917def272f2addf8c367044200e2e Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 12 Nov 2017 21:34:53 +0800 Subject: [PATCH 253/318] Add company and customer mutations on activity logs --- src/__tests__/activityLogCronJob.test.js | 4 +- src/__tests__/activityLogDb.test.js | 32 ++------ src/__tests__/activityLogMutations.test.js | 82 +++++++++++++++++++- src/__tests__/activityLogQueries.test.js | 4 +- src/data/constants.js | 2 +- src/data/resolvers/activityLog.js | 4 +- src/data/resolvers/mutations/activityLogs.js | 39 +++++++++- src/data/schema/activityLog.js | 14 +++- src/db/models/ActivityLogs.js | 62 ++++++++++----- 9 files changed, 188 insertions(+), 55 deletions(-) diff --git a/src/__tests__/activityLogCronJob.test.js b/src/__tests__/activityLogCronJob.test.js index a82f3949e..5c4e9c047 100644 --- a/src/__tests__/activityLogCronJob.test.js +++ b/src/__tests__/activityLogCronJob.test.js @@ -7,7 +7,7 @@ import { COC_CONTENT_TYPES, ACTIVITY_TYPES, ACTIVITY_ACTIONS, - ACTION_PERFORMER_TYPES, + ACTIVITY_PERFORMER_TYPES, } from '../data/constants'; import { ActivityLogs } from '../db/models'; import { customerFactory, segmentFactory } from '../db/factories'; @@ -51,7 +51,7 @@ describe('test activityLogsCronJob', () => { id: customer._id, }); expect(aLog.performedBy.toObject()).toEqual({ - type: ACTION_PERFORMER_TYPES.SYSTEM, + type: ACTIVITY_PERFORMER_TYPES.SYSTEM, }); // check if the second activity log is being created diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index 4aa61ca94..d1b9feaa0 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -4,7 +4,7 @@ import { connect, disconnect } from '../db/connection'; import { COC_CONTENT_TYPES, - ACTION_PERFORMER_TYPES, + ACTIVITY_PERFORMER_TYPES, ACTIVITY_TYPES, ACTIVITY_ACTIONS, } from '../data/constants'; @@ -43,30 +43,14 @@ describe('ActivityLogs model methods', () => { const doc = { activity: activityDoc, coc: customerDoc, - performedBy: null, + performer: null, }; const aLog = await ActivityLogs.createDoc(doc); expect(aLog.activity.toObject()).toEqual(activityDoc); expect(aLog.coc.toObject()).toEqual(customerDoc); - expect(aLog.performedBy.type).toBe(ACTION_PERFORMER_TYPES.SYSTEM); - }); - - test(`check if exception is being thrown when calling - createInternalNoteLog without setting 'user'`, async () => { - const customer = await customerFactory(); - - const internalNote = await internalNoteFactory({ - contentType: COC_CONTENT_TYPES.CUSTOMER, - contentTypeId: customer._id, - }); - - try { - await ActivityLogs.createInternalNoteLog(internalNote); - } catch (e) { - expect(e.message).toBe(`'user' must be supplied when adding activity log for internal note`); - } + expect(aLog.performedBy.toObject().type).toBe(ACTIVITY_PERFORMER_TYPES.SYSTEM); }); test(`createInternalNoteLog with setting 'user'`, async () => { @@ -81,7 +65,7 @@ describe('ActivityLogs model methods', () => { const aLog = await ActivityLogs.createInternalNoteLog(internalNote, user); - expect(aLog.performedBy.type).toBe(ACTION_PERFORMER_TYPES.USER); + expect(aLog.performedBy.type).toBe(ACTIVITY_PERFORMER_TYPES.USER); expect(aLog.performedBy.id).toBe(user._id); expect(aLog.coc.type).toBe(COC_CONTENT_TYPES.CUSTOMER); expect(aLog.coc.id).toBe(internalNote.contentTypeId); @@ -137,7 +121,7 @@ describe('ActivityLogs model methods', () => { id: customer._id, }); expect(segmentLog.performedBy.toObject()).toEqual({ - type: ACTION_PERFORMER_TYPES.SYSTEM, + type: ACTIVITY_PERFORMER_TYPES.SYSTEM, }); }); @@ -172,7 +156,7 @@ describe('ActivityLogs model methods', () => { // check customer conversation log expect(aLog.performedBy.toObject()).toEqual({ - type: ACTION_PERFORMER_TYPES.CUSTOMER, + type: ACTIVITY_PERFORMER_TYPES.CUSTOMER, }); expect(aLog.coc.toObject()).toEqual({ type: COC_CONTENT_TYPES.CUSTOMER, @@ -223,7 +207,7 @@ describe('ActivityLogs model methods', () => { const aLog = await ActivityLogs.createCustomerRegistrationLog(customer, user); expect(aLog.performedBy.toObject()).toEqual({ - type: ACTION_PERFORMER_TYPES.USER, + type: ACTIVITY_PERFORMER_TYPES.USER, id: user._id, }); expect(aLog.activity.toObject()).toEqual({ @@ -245,7 +229,7 @@ describe('ActivityLogs model methods', () => { const aLog = await ActivityLogs.createCompanyRegistrationLog(company, user); expect(aLog.performedBy.toObject()).toEqual({ - type: ACTION_PERFORMER_TYPES.USER, + type: ACTIVITY_PERFORMER_TYPES.USER, id: user._id, }); expect(aLog.activity.toObject()).toEqual({ diff --git a/src/__tests__/activityLogMutations.test.js b/src/__tests__/activityLogMutations.test.js index 8bdc48f6d..4eb17e6af 100644 --- a/src/__tests__/activityLogMutations.test.js +++ b/src/__tests__/activityLogMutations.test.js @@ -3,8 +3,14 @@ import { connect, disconnect } from '../db/connection'; import mutations from '../data/resolvers/mutations'; -import { ROLES, ACTIVITY_TYPES, COC_CONTENT_TYPES } from '../data/constants'; -import { ActivityLogs } from '../db/models'; +import { + ROLES, + ACTIVITY_TYPES, + ACTIVITY_ACTIONS, + COC_CONTENT_TYPES, + ACTIVITY_PERFORMER_TYPES, +} from '../data/constants'; +import { ActivityLogs, Customers, Companies } from '../db/models'; import { userFactory, customerFactory } from '../db/factories'; beforeAll(() => connect()); @@ -13,6 +19,8 @@ afterAll(() => disconnect()); describe('ActivityLog creation on Customer creation', () => { afterEach(async () => { await ActivityLogs.remove({}); + await Customers.remove({}); + await Companies.remove({}); }); test(`createCompanyRegistrationLog`, async () => { @@ -81,4 +89,74 @@ describe('ActivityLog creation on Customer creation', () => { expect(aLog.coc.type).toBe(COC_CONTENT_TYPES.CUSTOMER); expect(aLog.coc.id).toBe(customer._id); }); + + test(`activityLogsAddCustomerLog`, async () => { + const customerDoc = { + name: 'test user', + email: 'test email', + phone: 'test phone', + }; + + const aLog = await mutations.activityLogsAddCustomerLog(null, customerDoc); + + const customer = await Customers.findOne({}); + + expect(customer.name).toBe(customerDoc.name); + expect(customer.email).toBe(customerDoc.email); + expect(customer.phone).toBe(customerDoc.phone); + + expect(aLog.activity.toObject()).toEqual({ + type: ACTIVITY_TYPES.CUSTOMER, + action: ACTIVITY_ACTIONS.CREATE, + id: customer._id, + content: customer.name, + }); + expect(aLog.coc.toObject()).toEqual({ + type: COC_CONTENT_TYPES.CUSTOMER, + id: customer._id, + }); + expect(aLog.performedBy.toObject()).toEqual({ + type: ACTIVITY_PERFORMER_TYPES.SYSTEM, + }); + }); + + test(`activityLogsAddCompanyLog`, async () => { + const companyDoc = { + name: 'test company name', + size: 10, + website: 'test company website', + industry: 'test company industry', + plan: 'test company plan', + lastSeenAt: new Date(), + sessionCount: 25, + tagIds: ['111', '222'], + }; + + const aLog = await mutations.activityLogsAddCompanyLog(null, companyDoc); + + const company = await Companies.findOne({}); + + expect(company.name).toBe(companyDoc.name); + expect(company.size).toBe(companyDoc.size); + expect(company.website).toBe(companyDoc.website); + expect(company.industry).toBe(companyDoc.industry); + expect(company.plan).toBe(companyDoc.plan); + expect(company.lastSeenAt).toEqual(companyDoc.lastSeenAt); + expect(company.sessionCount).toBe(companyDoc.sessionCount); + expect(company.tagIds.toObject()).toEqual(companyDoc.tagIds); + + expect(aLog.activity.toObject()).toEqual({ + type: ACTIVITY_TYPES.COMPANY, + action: ACTIVITY_ACTIONS.CREATE, + content: company.name, + id: company._id, + }); + expect(aLog.coc.toObject()).toEqual({ + type: COC_CONTENT_TYPES.COMPANY, + id: company._id, + }); + expect(aLog.performedBy.toObject()).toEqual({ + type: ACTIVITY_PERFORMER_TYPES.SYSTEM, + }); + }); }); diff --git a/src/__tests__/activityLogQueries.test.js b/src/__tests__/activityLogQueries.test.js index b83c093e5..15c241a6e 100644 --- a/src/__tests__/activityLogQueries.test.js +++ b/src/__tests__/activityLogQueries.test.js @@ -45,7 +45,7 @@ describe('activityLogs', () => { ); // create conversation message - await mutations.activitivyLogsAddConversationMessageLog(null, { + await mutations.activityLogsAddConversationMessageLog(null, { customerId: customer._id, messageId: _message._id, }); @@ -219,7 +219,7 @@ describe('activityLogs', () => { ); const customer = await customerFactory({ companyIds: [company._id] }); - await mutations.activitivyLogsAddConversationMessageLog(null, { + await mutations.activityLogsAddConversationMessageLog(null, { customerId: customer._id, messageId: _message._id, }); diff --git a/src/data/constants.js b/src/data/constants.js index e603ff311..07e64fec6 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -205,7 +205,7 @@ export const ACTIVITY_ACTIONS = { ALL: ['create', 'update', 'delete'], }; -export const ACTION_PERFORMER_TYPES = { +export const ACTIVITY_PERFORMER_TYPES = { SYSTEM: 'SYSTEM', USER: 'USER', CUSTOMER: 'CUSTOMER', diff --git a/src/data/resolvers/activityLog.js b/src/data/resolvers/activityLog.js index adf5b17ec..c3c5d90d9 100644 --- a/src/data/resolvers/activityLog.js +++ b/src/data/resolvers/activityLog.js @@ -1,4 +1,4 @@ -import { ACTION_PERFORMER_TYPES } from '../../data/constants'; +import { ACTIVITY_PERFORMER_TYPES } from '../../data/constants'; import { Users } from '../../db/models'; /* @@ -39,7 +39,7 @@ export default { */ async by(obj) { const performedBy = obj.performedBy; - if (performedBy.type === ACTION_PERFORMER_TYPES.USER) { + if (performedBy.type === ACTIVITY_PERFORMER_TYPES.USER) { const user = await Users.findOne({ _id: performedBy.id }); return { _id: user._id, diff --git a/src/data/resolvers/mutations/activityLogs.js b/src/data/resolvers/mutations/activityLogs.js index 7cef939ae..cf309a197 100644 --- a/src/data/resolvers/mutations/activityLogs.js +++ b/src/data/resolvers/mutations/activityLogs.js @@ -1,4 +1,4 @@ -import { ActivityLogs, Customers, ConversationMessages } from '../../../db/models'; +import { ActivityLogs, Customers, Companies, ConversationMessages } from '../../../db/models'; export default { /** @@ -8,10 +8,45 @@ export default { * @param {string} customerId - id of customer * @param {string} messageId - id of message */ - async activitivyLogsAddConversationMessageLog(root, { customerId, messageId }) { + async activityLogsAddConversationMessageLog(root, { customerId, messageId }) { const customer = await Customers.findOne({ _id: customerId }); const message = await ConversationMessages.findOne({ _id: messageId }); return ActivityLogs.createConversationMessageLog(message, customer); }, + + /** + * Create new customer also adds Customer registration log + * @param {Object} root + * @param {Object} doc - Customer object + * @param {string} doc.name - Name of customer + * @param {string} doc.email - Email of customer + * @param {string} doc.phone - Phone of customer + * @param {JSON} doc.customFieldsData - customFieldsData of customer JSON + * @return {Promise} return Promise resolving created ActivityLog document + */ + async activityLogsAddCustomerLog(root, doc) { + const customer = await Customers.createCustomer(doc); + return ActivityLogs.createCustomerRegistrationLog(customer); + }, + + /** + * Create new company also adds Company registration log + * @param {Object} root + * @param {Object} doc - Customer object + * @param {string} doc.name - Name of company + * @param {int} doc.size - Size of company + * @param {string} doc.website - Website of company + * @param {string} doc.industry - Industry of company + * @param {string} doc.plan - Plan of company + * @param {Date} lastSeenAt + * @param {Int} sessionCount + * @param {string[]} tagIds - Related tag ids of company + * @param {JSON} doc.customFieldsData - customFieldsData of customer JSON + * @return {Promise} return Promise resolving created ActivityLog document + */ + async activityLogsAddCompanyLog(root, doc) { + const company = await Companies.createCompany(doc); + return ActivityLogs.createCompanyRegistrationLog(company); + }, }; diff --git a/src/data/schema/activityLog.js b/src/data/schema/activityLog.js index 08eab69ca..49d47502c 100644 --- a/src/data/schema/activityLog.js +++ b/src/data/schema/activityLog.js @@ -37,5 +37,17 @@ export const queries = ` `; export const mutations = ` - activitivyLogsAddConversationMessageLog(customerId: String!, messageId: String!): ActivityLog + activityLogsAddConversationMessageLog(customerId: String!, messageId: String!): ActivityLog + activityLogsAddCustomerLog( + name: String!, email: String, phone: String, customFieldsData: JSON): ActivityLog + activityLogsAddCompanyLog( + name: String!, + size: Int, + website: String, + industry: String, + plan: String, + lastSeenAt: Date, + sessionCount: Int, + tagIds: [String] + customFieldsData: JSON): ActivityLog `; diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 22f105bd6..05f8aca95 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -2,7 +2,7 @@ import mongoose, { SchemaTypes } from 'mongoose'; import { field } from './utils'; import { COC_CONTENT_TYPES, - ACTION_PERFORMER_TYPES, + ACTIVITY_PERFORMER_TYPES, ACTIVITY_TYPES, ACTIVITY_ACTIONS, } from '../../data/constants'; @@ -15,8 +15,8 @@ const ActionPerformer = mongoose.Schema( { type: field({ type: String, - enum: ACTION_PERFORMER_TYPES.ALL, - default: ACTION_PERFORMER_TYPES.SYSTEM, + enum: ACTIVITY_PERFORMER_TYPES.ALL, + default: ACTIVITY_PERFORMER_TYPES.SYSTEM, required: true, }), id: field({ @@ -102,18 +102,23 @@ const ActivityLogSchema = mongoose.Schema({ class ActivityLog { /** * Create an ActivityLog document - * @param {Object|null} object1.performedBy - The performer of the action + * @param {Object|null} object1.performer - The performer of the action * @param {Object} object1 - Data to insert according to schema * @return {Promise} returns Promise resolving created ActivityLog document */ - static createDoc({ performedBy, ...doc }) { - if (performedBy && performedBy._id) { - performedBy = { - type: ACTION_PERFORMER_TYPES.USER, - id: performedBy._id, - }; - } else if (!performedBy) { - performedBy = {}; + static createDoc({ performer, ...doc }) { + let performedBy = { + type: ACTIVITY_PERFORMER_TYPES.SYSTEM, + }; + + if (performer) { + if (performer.type) { + performedBy.type = performer.type; + } + + if (performer.id) { + performedBy.id = performer.id; + } } return this.create({ performedBy, ...doc }); @@ -122,7 +127,7 @@ class ActivityLog { /** * Create activity log for internal note * @param {InternalNote} internalNote - Internal note document - * @param {User} performedBy - User collection document + * @param {User} user - User collection document * @return {Promise} returns Promise resolving created ActivityLog document */ static createInternalNoteLog(internalNote, user) { @@ -133,7 +138,10 @@ class ActivityLog { id: internalNote._id, content: internalNote.content, }, - performedBy: user, + performer: { + type: ACTIVITY_PERFORMER_TYPES.USER, + id: user._id, + }, coc: { id: internalNote.contentTypeId, type: internalNote.contentType, @@ -147,7 +155,7 @@ class ActivityLog { 'activity.action': ACTIVITY_ACTIONS.CREATE, 'activity.id': messageId, 'coc.type': cocType, - 'performedBy.type': ACTION_PERFORMER_TYPES.CUSTOMER, + 'performedBy.type': ACTIVITY_PERFORMER_TYPES.CUSTOMER, 'coc.id': cocId, }); } @@ -160,8 +168,8 @@ class ActivityLog { content: content, id: messageId, }, - performedBy: { - type: ACTION_PERFORMER_TYPES.CUSTOMER, + performer: { + type: ACTIVITY_PERFORMER_TYPES.CUSTOMER, }, coc: { type: cocType, @@ -249,6 +257,14 @@ class ActivityLog { * @return {Promise} return Promise resolving created ActivityLog */ static createCustomerRegistrationLog(customer, user) { + const performer = + (user && + user._id && { + type: ACTIVITY_PERFORMER_TYPES.USER, + id: user._id, + }) || + null; + return this.createDoc({ activity: { type: ACTIVITY_TYPES.CUSTOMER, @@ -260,7 +276,7 @@ class ActivityLog { type: COC_CONTENT_TYPES.CUSTOMER, id: customer._id, }, - performedBy: user, + performer, }); } @@ -271,6 +287,14 @@ class ActivityLog { * @return {Promise} return Promise resolving created ActivityLog */ static createCompanyRegistrationLog(company, user) { + const performer = + (user && + user._id && { + type: ACTIVITY_PERFORMER_TYPES.USER, + id: user._id, + }) || + null; + return this.createDoc({ activity: { type: ACTIVITY_TYPES.COMPANY, @@ -282,7 +306,7 @@ class ActivityLog { type: COC_CONTENT_TYPES.COMPANY, id: company._id, }, - performedBy: user, + performer, }); } } From a9cb09609023315f3d0e5ca242f747ca89ec0661 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 12 Nov 2017 21:56:56 +0800 Subject: [PATCH 254/318] Fix activityLog mutations --- src/__tests__/activityLogMutations.test.js | 49 ++++++-------------- src/data/resolvers/mutations/activityLogs.js | 27 ++++------- src/data/schema/activityLog.js | 14 +----- 3 files changed, 23 insertions(+), 67 deletions(-) diff --git a/src/__tests__/activityLogMutations.test.js b/src/__tests__/activityLogMutations.test.js index 4eb17e6af..4a796091a 100644 --- a/src/__tests__/activityLogMutations.test.js +++ b/src/__tests__/activityLogMutations.test.js @@ -92,28 +92,23 @@ describe('ActivityLog creation on Customer creation', () => { test(`activityLogsAddCustomerLog`, async () => { const customerDoc = { - name: 'test user', - email: 'test email', - phone: 'test phone', + name: 'test customer', + _id: 'testCustomerId', }; - const aLog = await mutations.activityLogsAddCustomerLog(null, customerDoc); + Customers.findOne = jest.fn(() => customerDoc); - const customer = await Customers.findOne({}); - - expect(customer.name).toBe(customerDoc.name); - expect(customer.email).toBe(customerDoc.email); - expect(customer.phone).toBe(customerDoc.phone); + const aLog = await mutations.activityLogsAddCustomerLog(null, { _id: customerDoc._id }); expect(aLog.activity.toObject()).toEqual({ type: ACTIVITY_TYPES.CUSTOMER, action: ACTIVITY_ACTIONS.CREATE, - id: customer._id, - content: customer.name, + id: customerDoc._id, + content: customerDoc.name, }); expect(aLog.coc.toObject()).toEqual({ type: COC_CONTENT_TYPES.CUSTOMER, - id: customer._id, + id: customerDoc._id, }); expect(aLog.performedBy.toObject()).toEqual({ type: ACTIVITY_PERFORMER_TYPES.SYSTEM, @@ -121,39 +116,21 @@ describe('ActivityLog creation on Customer creation', () => { }); test(`activityLogsAddCompanyLog`, async () => { - const companyDoc = { - name: 'test company name', - size: 10, - website: 'test company website', - industry: 'test company industry', - plan: 'test company plan', - lastSeenAt: new Date(), - sessionCount: 25, - tagIds: ['111', '222'], - }; - - const aLog = await mutations.activityLogsAddCompanyLog(null, companyDoc); + const companyDoc = { _id: 'testCompanyId', name: 'test company' }; - const company = await Companies.findOne({}); + Companies.findOne = jest.fn(() => companyDoc); - expect(company.name).toBe(companyDoc.name); - expect(company.size).toBe(companyDoc.size); - expect(company.website).toBe(companyDoc.website); - expect(company.industry).toBe(companyDoc.industry); - expect(company.plan).toBe(companyDoc.plan); - expect(company.lastSeenAt).toEqual(companyDoc.lastSeenAt); - expect(company.sessionCount).toBe(companyDoc.sessionCount); - expect(company.tagIds.toObject()).toEqual(companyDoc.tagIds); + const aLog = await mutations.activityLogsAddCompanyLog(null, { _id: companyDoc._id }); expect(aLog.activity.toObject()).toEqual({ type: ACTIVITY_TYPES.COMPANY, action: ACTIVITY_ACTIONS.CREATE, - content: company.name, - id: company._id, + content: companyDoc.name, + id: companyDoc._id, }); expect(aLog.coc.toObject()).toEqual({ type: COC_CONTENT_TYPES.COMPANY, - id: company._id, + id: companyDoc._id, }); expect(aLog.performedBy.toObject()).toEqual({ type: ACTIVITY_PERFORMER_TYPES.SYSTEM, diff --git a/src/data/resolvers/mutations/activityLogs.js b/src/data/resolvers/mutations/activityLogs.js index cf309a197..03634f8c8 100644 --- a/src/data/resolvers/mutations/activityLogs.js +++ b/src/data/resolvers/mutations/activityLogs.js @@ -16,37 +16,26 @@ export default { }, /** - * Create new customer also adds Customer registration log + * Create customer registration log for the given customer * @param {Object} root - * @param {Object} doc - Customer object - * @param {string} doc.name - Name of customer - * @param {string} doc.email - Email of customer - * @param {string} doc.phone - Phone of customer - * @param {JSON} doc.customFieldsData - customFieldsData of customer JSON + * @param {Object} doc - Input data + * @param {string} doc._id - Customer id * @return {Promise} return Promise resolving created ActivityLog document */ async activityLogsAddCustomerLog(root, doc) { - const customer = await Customers.createCustomer(doc); + const customer = await Customers.findOne(doc); return ActivityLogs.createCustomerRegistrationLog(customer); }, /** - * Create new company also adds Company registration log + * Creates company registration log for the given company * @param {Object} root - * @param {Object} doc - Customer object - * @param {string} doc.name - Name of company - * @param {int} doc.size - Size of company - * @param {string} doc.website - Website of company - * @param {string} doc.industry - Industry of company - * @param {string} doc.plan - Plan of company - * @param {Date} lastSeenAt - * @param {Int} sessionCount - * @param {string[]} tagIds - Related tag ids of company - * @param {JSON} doc.customFieldsData - customFieldsData of customer JSON + * @param {Object} doc - input data + * @param {string} doc._id - Company id * @return {Promise} return Promise resolving created ActivityLog document */ async activityLogsAddCompanyLog(root, doc) { - const company = await Companies.createCompany(doc); + const company = await Companies.findOne(doc); return ActivityLogs.createCompanyRegistrationLog(company); }, }; diff --git a/src/data/schema/activityLog.js b/src/data/schema/activityLog.js index 49d47502c..3f89ea61e 100644 --- a/src/data/schema/activityLog.js +++ b/src/data/schema/activityLog.js @@ -38,16 +38,6 @@ export const queries = ` export const mutations = ` activityLogsAddConversationMessageLog(customerId: String!, messageId: String!): ActivityLog - activityLogsAddCustomerLog( - name: String!, email: String, phone: String, customFieldsData: JSON): ActivityLog - activityLogsAddCompanyLog( - name: String!, - size: Int, - website: String, - industry: String, - plan: String, - lastSeenAt: Date, - sessionCount: Int, - tagIds: [String] - customFieldsData: JSON): ActivityLog + activityLogsAddCustomerLog(_id: String!): ActivityLog + activityLogsAddCompanyLog(_id: String!): ActivityLog `; From 0729fa1f0b7cc897df879829a05e993760a86839 Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 12 Nov 2017 22:09:07 +0800 Subject: [PATCH 255/318] Refactor --- src/db/models/ActivityLogs.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 05f8aca95..2af3b0ef1 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -112,13 +112,7 @@ class ActivityLog { }; if (performer) { - if (performer.type) { - performedBy.type = performer.type; - } - - if (performer.id) { - performedBy.id = performer.id; - } + performedBy = performer; } return this.create({ performedBy, ...doc }); From ead62c9bb41aaa184701112411cf898a96236dff Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 12 Nov 2017 22:12:32 +0800 Subject: [PATCH 256/318] Refactor --- src/data/resolvers/mutations/activityLogs.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data/resolvers/mutations/activityLogs.js b/src/data/resolvers/mutations/activityLogs.js index 03634f8c8..0073c3691 100644 --- a/src/data/resolvers/mutations/activityLogs.js +++ b/src/data/resolvers/mutations/activityLogs.js @@ -22,8 +22,8 @@ export default { * @param {string} doc._id - Customer id * @return {Promise} return Promise resolving created ActivityLog document */ - async activityLogsAddCustomerLog(root, doc) { - const customer = await Customers.findOne(doc); + async activityLogsAddCustomerLog(root, { _id }) { + const customer = await Customers.findOne({ _id }); return ActivityLogs.createCustomerRegistrationLog(customer); }, @@ -34,8 +34,8 @@ export default { * @param {string} doc._id - Company id * @return {Promise} return Promise resolving created ActivityLog document */ - async activityLogsAddCompanyLog(root, doc) { - const company = await Companies.findOne(doc); + async activityLogsAddCompanyLog(root, { _id }) { + const company = await Companies.findOne({ _id }); return ActivityLogs.createCompanyRegistrationLog(company); }, }; From c5435a5a516aeb06134f1069cd9a42b6da54835d Mon Sep 17 00:00:00 2001 From: Javkhlan Shirendev Date: Sun, 12 Nov 2017 22:28:49 +0800 Subject: [PATCH 257/318] Set performer.id when createing conversation message activity logs --- src/__tests__/activityLogDb.test.js | 1 + .../{activityLogForMonthQueryBuilder.js => activityLogUtils.js} | 0 src/data/resolvers/queries/activityLogs.js | 2 +- src/db/models/ActivityLogs.js | 1 + 4 files changed, 3 insertions(+), 1 deletion(-) rename src/data/resolvers/queries/{activityLogForMonthQueryBuilder.js => activityLogUtils.js} (100%) diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index d1b9feaa0..bedc221b3 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -157,6 +157,7 @@ describe('ActivityLogs model methods', () => { // check customer conversation log expect(aLog.performedBy.toObject()).toEqual({ type: ACTIVITY_PERFORMER_TYPES.CUSTOMER, + id: customer._id, }); expect(aLog.coc.toObject()).toEqual({ type: COC_CONTENT_TYPES.CUSTOMER, diff --git a/src/data/resolvers/queries/activityLogForMonthQueryBuilder.js b/src/data/resolvers/queries/activityLogUtils.js similarity index 100% rename from src/data/resolvers/queries/activityLogForMonthQueryBuilder.js rename to src/data/resolvers/queries/activityLogUtils.js diff --git a/src/data/resolvers/queries/activityLogs.js b/src/data/resolvers/queries/activityLogs.js index 8e9818ae2..4c2f51394 100644 --- a/src/data/resolvers/queries/activityLogs.js +++ b/src/data/resolvers/queries/activityLogs.js @@ -3,7 +3,7 @@ import { moduleRequireLogin } from '../../permissions'; import { CustomerMonthActivityLogBuilder, CompanyMonthActivityLogBuilder, -} from './activityLogForMonthQueryBuilder'; +} from './activityLogUtils'; const activityLogQueries = { /** diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 2af3b0ef1..0b4ca512e 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -164,6 +164,7 @@ class ActivityLog { }, performer: { type: ACTIVITY_PERFORMER_TYPES.CUSTOMER, + id: cocId, }, coc: { type: cocType, From 7c66efbd18f0e1557a1abf79c1bff2d250a82112 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 12 Nov 2017 22:50:12 +0800 Subject: [PATCH 258/318] Little test fix --- src/__tests__/conversationMutations.test.js | 14 ++++++++++---- src/data/resolvers/mutations/conversations.js | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/__tests__/conversationMutations.test.js b/src/__tests__/conversationMutations.test.js index 7c8f80cbc..a28714e38 100644 --- a/src/__tests__/conversationMutations.test.js +++ b/src/__tests__/conversationMutations.test.js @@ -220,8 +220,11 @@ describe('Conversation message mutations', () => { jest.spyOn(utils, 'sendEmail'); // mock graph api requests - sinon.stub(graphRequest, 'get').callsFake(() => ({ access_token: 'access_token' })); - const stub = sinon.stub(graphRequest, 'post').callsFake(() => ({ id: 'id' })); + const getStub = sinon + .stub(graphRequest, 'get') + .callsFake(() => ({ access_token: 'access_token' })); + + const postStub = sinon.stub(graphRequest, 'post').callsFake(() => ({ id: 'id' })); // factories ============ const integration = await integrationFactory({ @@ -248,13 +251,16 @@ describe('Conversation message mutations', () => { await conversationMutations.conversationMessageAdd({}, _doc, { user: _user }); // check stub ============== - expect(stub.calledOnce).toBe(true); + expect(postStub.called).toBe(true); - const [arg1, arg2, arg3] = stub.firstCall.args; + const [arg1, arg2, arg3] = postStub.firstCall.args; expect(arg1).toBe('postId/comments'); expect(arg2).toBe('access_token'); expect(arg3).toEqual({ message: _doc.content }); + + getStub.restore(); + postStub.restore(); }); // if user assigned to conversation diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 2005eae0d..047e35a95 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -131,7 +131,7 @@ const conversationMutations = { // send reply to twitter if (kind === KIND_CHOICES.TWITTER) { - tweetReply(conversation, strip(doc.content)); + await tweetReply(conversation, strip(doc.content)); return message; } @@ -154,7 +154,7 @@ const conversationMutations = { // send reply to facebook if (kind === KIND_CHOICES.FACEBOOK) { // when facebook kind is feed, assign commentId in extraData - facebookReply(conversation, strip(doc.content), message._id); + await facebookReply(conversation, strip(doc.content), message._id); } // notify subscription From eb2657774c37250e22d75b8c9200e8b1fd5952d9 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 12 Nov 2017 23:36:12 +0800 Subject: [PATCH 259/318] Fix brand list queries --- src/data/resolvers/queries/brands.js | 6 +++--- src/data/schema/brand.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data/resolvers/queries/brands.js b/src/data/resolvers/queries/brands.js index c181b485f..5d419dda4 100644 --- a/src/data/resolvers/queries/brands.js +++ b/src/data/resolvers/queries/brands.js @@ -5,11 +5,11 @@ import { paginate } from './utils'; const brandQueries = { /** * Brands list - * @param {Object} params - Query params + * @param {Object} args - Query params * @return {Promise} sorted brands list */ - brands(root, { params = {} }) { - const brands = paginate(Brands.find({}), params); + brands(root, args) { + const brands = paginate(Brands.find({}), args); return brands.sort({ createdAt: -1 }); }, diff --git a/src/data/schema/brand.js b/src/data/schema/brand.js index bafd7f83b..4af3a01bc 100644 --- a/src/data/schema/brand.js +++ b/src/data/schema/brand.js @@ -11,7 +11,7 @@ export const types = ` `; export const queries = ` - brands(params: JSON): [Brand] + brands(page: Int, perPage: Int): [Brand] brandDetail(_id: String!): Brand brandsTotalCount: Int `; From cf52691f30a4c9ad95885f0495689dd47c58d460 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 12 Nov 2017 23:39:57 +0800 Subject: [PATCH 260/318] Fix channel list queries --- src/data/resolvers/queries/channels.js | 8 ++++---- src/data/schema/channel.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data/resolvers/queries/channels.js b/src/data/resolvers/queries/channels.js index 031a8879f..e8610c275 100644 --- a/src/data/resolvers/queries/channels.js +++ b/src/data/resolvers/queries/channels.js @@ -8,15 +8,15 @@ const channelQueries = { * @param {Object} args - Search params * @return {Promise} filtered channels list by given parameters */ - channels(root, { params = {} }) { + channels(root, { memberIds, ...queryParams }) { const query = {}; const sort = { createdAt: -1 }; - if (params.memberIds) { - query.memberIds = { $in: params.memberIds }; + if (memberIds) { + query.memberIds = { $in: memberIds }; } - const channels = paginate(Channels.find(query), params); + const channels = paginate(Channels.find(query), queryParams); return channels.sort(sort); }, diff --git a/src/data/schema/channel.js b/src/data/schema/channel.js index 21ae6b3bc..5e96e8803 100644 --- a/src/data/schema/channel.js +++ b/src/data/schema/channel.js @@ -13,7 +13,7 @@ export const types = ` `; export const queries = ` - channels(params: JSON): [Channel] + channels(page: Int, perPage: Int, memberIds: [String]): [Channel] channelsTotalCount: Int `; From 166010063dc9878b7e792502a6716ae591271762 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 12 Nov 2017 23:43:03 +0800 Subject: [PATCH 261/318] Fix user list queries --- src/data/resolvers/queries/users.js | 4 ++-- src/data/schema/user.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/resolvers/queries/users.js b/src/data/resolvers/queries/users.js index 725fc8192..2ec512bd3 100644 --- a/src/data/resolvers/queries/users.js +++ b/src/data/resolvers/queries/users.js @@ -10,8 +10,8 @@ const userQueries = { * @param {Object} object3.user - User making this request * @return {Promise} sorted and filtered users objects */ - users(root, { params = {} }) { - const users = paginate(Users.find({}), params); + users(root, args) { + const users = paginate(Users.find({}), args); return users.sort({ username: 1 }); }, diff --git a/src/data/schema/user.js b/src/data/schema/user.js index 786a8e85b..263c4d692 100644 --- a/src/data/schema/user.js +++ b/src/data/schema/user.js @@ -28,7 +28,7 @@ export const types = ` `; export const queries = ` - users(params: JSON): [User] + users(page: Int, perPage: Int): [User] userDetail(_id: String): User usersTotalCount: Int currentUser: User From 32799ce0f19bcd091a31089b98830cca7d69bc4c Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 12 Nov 2017 23:48:06 +0800 Subject: [PATCH 262/318] Fix response template list queries --- src/data/resolvers/queries/responseTemplates.js | 4 ++-- src/data/schema/responseTemplate.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/resolvers/queries/responseTemplates.js b/src/data/resolvers/queries/responseTemplates.js index f516c144f..b9f806529 100644 --- a/src/data/resolvers/queries/responseTemplates.js +++ b/src/data/resolvers/queries/responseTemplates.js @@ -8,8 +8,8 @@ const responseTemplateQueries = { * @param {Object} args - Search params * @return {Promise} response template objects */ - responseTemplates(root, { params = {} }) { - return paginate(ResponseTemplates.find({}), params); + responseTemplates(root, args) { + return paginate(ResponseTemplates.find({}), args); }, /** diff --git a/src/data/schema/responseTemplate.js b/src/data/schema/responseTemplate.js index 0929ce121..f322dd48a 100644 --- a/src/data/schema/responseTemplate.js +++ b/src/data/schema/responseTemplate.js @@ -11,7 +11,7 @@ export const types = ` `; export const queries = ` - responseTemplates(params: JSON): [ResponseTemplate] + responseTemplates(page: Int, perPage: Int): [ResponseTemplate] responseTemplatesTotalCount: Int `; From 00d88416f752142a5e3aed16ad4d1dfbf72ac14b Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 12 Nov 2017 23:53:26 +0800 Subject: [PATCH 263/318] Fix response template list queries --- src/data/resolvers/queries/emailTemplates.js | 4 ++-- src/data/schema/emailTemplate.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/resolvers/queries/emailTemplates.js b/src/data/resolvers/queries/emailTemplates.js index 7a63c30d8..e4fcc4d50 100644 --- a/src/data/resolvers/queries/emailTemplates.js +++ b/src/data/resolvers/queries/emailTemplates.js @@ -8,8 +8,8 @@ const emailTemplateQueries = { * @param {Object} args - Search params * @return {Promise} email template objects */ - emailTemplates(root, { params = {} }) { - return paginate(EmailTemplates.find({}), params); + emailTemplates(root, args) { + return paginate(EmailTemplates.find({}), args); }, /** diff --git a/src/data/schema/emailTemplate.js b/src/data/schema/emailTemplate.js index 745df3f5c..f2b46a2a7 100644 --- a/src/data/schema/emailTemplate.js +++ b/src/data/schema/emailTemplate.js @@ -7,7 +7,7 @@ export const types = ` `; export const queries = ` - emailTemplates(params: JSON): [EmailTemplate] + emailTemplates(page: Int, perPage: Int): [EmailTemplate] emailTemplatesTotalCount: Int `; From da0402f5ef7138d7198cace338e2cb8bcbb8ffc1 Mon Sep 17 00:00:00 2001 From: batamar Date: Sun, 12 Nov 2017 23:57:27 +0800 Subject: [PATCH 264/318] Fix form list queries --- src/data/resolvers/queries/forms.js | 6 +++--- src/data/schema/form.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data/resolvers/queries/forms.js b/src/data/resolvers/queries/forms.js index ed4669a73..775ce1c5c 100644 --- a/src/data/resolvers/queries/forms.js +++ b/src/data/resolvers/queries/forms.js @@ -5,11 +5,11 @@ import { paginate } from './utils'; const formQueries = { /** * Forms list - * @param {Object} params - Search params + * @param {Object} args - Search params * @return {Promise} sorted forms list */ - forms(root, { params = {} }) { - const forms = paginate(Forms.find({}), params); + forms(root, args) { + const forms = paginate(Forms.find({}), args); return forms.sort({ name: 1 }); }, diff --git a/src/data/schema/form.js b/src/data/schema/form.js index c857f7fe8..767f2f7e4 100644 --- a/src/data/schema/form.js +++ b/src/data/schema/form.js @@ -17,7 +17,7 @@ export const mutations = ` `; export const queries = ` - forms(params: JSON): [Form] + forms(page: Int, perPage: Int): [Form] formDetail(_id: String!): Form formsTotalCount: Int `; From 3b2f4e9f3170d1b8938feecf93350c183bfcd019 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 13 Nov 2017 00:01:14 +0800 Subject: [PATCH 265/318] Fix integration list queries --- src/data/resolvers/queries/integrations.js | 8 ++++---- src/data/schema/integration.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data/resolvers/queries/integrations.js b/src/data/resolvers/queries/integrations.js index 5fe2f749f..a8057579f 100644 --- a/src/data/resolvers/queries/integrations.js +++ b/src/data/resolvers/queries/integrations.js @@ -7,14 +7,14 @@ import { paginate } from './utils'; const integrationQueries = { /** * Integrations list - * @param {Object} params - Search params + * @param {Object} args - Search params * @return {Promise} filterd and sorted integrations list */ - integrations(root, { params = {} }) { + integrations(root, { kind, ...params }) { const query = {}; - if (params.kind) { - query.kind = params.kind; + if (kind) { + query.kind = kind; } const integrations = paginate(Integrations.find(query), params); diff --git a/src/data/schema/integration.js b/src/data/schema/integration.js index 38ac0f145..17b78ecc6 100644 --- a/src/data/schema/integration.js +++ b/src/data/schema/integration.js @@ -62,7 +62,7 @@ export const types = ` `; export const queries = ` - integrations(params: JSON): [Integration] + integrations(page: Int, perPage: Int, kind: String): [Integration] integrationDetail(_id: String!): Integration integrationsTotalCount(kind: String): Int integrationGetTwitterAuthUrl: String From cd6815c260a04b3e7eb1b98673786a4a8d7437bf Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 13 Nov 2017 00:08:56 +0800 Subject: [PATCH 266/318] Fix knowledgebase list queries --- src/data/resolvers/queries/knowledgeBase.js | 18 +++++++++--------- src/data/schema/knowledgeBase.js | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/data/resolvers/queries/knowledgeBase.js b/src/data/resolvers/queries/knowledgeBase.js index 0962e091a..acf97974b 100644 --- a/src/data/resolvers/queries/knowledgeBase.js +++ b/src/data/resolvers/queries/knowledgeBase.js @@ -10,11 +10,11 @@ import { paginate } from './utils'; const knowledgeBaseQueries = { /** * Article list - * @param {Object} params - Search params + * @param {Object} args - Search params * @return {Promise} sorted article list */ - knowledgeBaseArticles(root, { params = {} }) { - const articles = paginate(KnowledgeBaseArticles.find({}), params); + knowledgeBaseArticles(root, args) { + const articles = paginate(KnowledgeBaseArticles.find({}), args); return articles.sort({ createdDate: -1 }); }, @@ -38,11 +38,11 @@ const knowledgeBaseQueries = { /** * Category list - * @param {Object} params - Search params + * @param {Object} args - Search params * @return {Promise} sorted category list */ - knowledgeBaseCategories(root, { params = {} }) { - const categories = paginate(KnowledgeBaseCategories.find({}), params); + knowledgeBaseCategories(root, args) { + const categories = paginate(KnowledgeBaseCategories.find({}), args); return categories.sort({ createdDate: -1 }); }, @@ -68,11 +68,11 @@ const knowledgeBaseQueries = { /** * Topic list - * @param {Object} params - Search params + * @param {Object} args - Search params * @return {Promise} sorted topic list */ - knowledgeBaseTopics(root, { params = {} }) { - const topics = paginate(KnowledgeBaseTopics.find({}), params); + knowledgeBaseTopics(root, args) { + const topics = paginate(KnowledgeBaseTopics.find({}), args); return topics.sort({ createdDate: -1 }); }, diff --git a/src/data/schema/knowledgeBase.js b/src/data/schema/knowledgeBase.js index 314c8f8a9..05047522b 100644 --- a/src/data/schema/knowledgeBase.js +++ b/src/data/schema/knowledgeBase.js @@ -58,15 +58,15 @@ export const types = ` `; export const queries = ` - knowledgeBaseTopics(params: JSON): [KnowledgeBaseTopic] + knowledgeBaseTopics(page: Int, perPage: Int): [KnowledgeBaseTopic] knowledgeBaseTopicDetail(_id: String!): KnowledgeBaseTopic knowledgeBaseTopicsTotalCount: Int - knowledgeBaseCategories(params: JSON): [KnowledgeBaseCategory] + knowledgeBaseCategories(page: Int, perPage: Int): [KnowledgeBaseCategory] knowledgeBaseCategoryDetail(_id: String!): KnowledgeBaseCategory knowledgeBaseCategoriesTotalCount: Int - knowledgeBaseArticles(params: JSON): [KnowledgeBaseArticle] + knowledgeBaseArticles(page: Int, perPage: Int): [KnowledgeBaseArticle] knowledgeBaseArticleDetail(_id: String!): KnowledgeBaseArticle knowledgeBaseArticlesTotalCount: Int `; From b24e810a2534318171adba2cf67f5cd104111840 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 13 Nov 2017 00:21:32 +0800 Subject: [PATCH 267/318] Fix company list queries --- src/data/resolvers/queries/companies.js | 14 ++++++-------- src/data/schema/company.js | 19 ++++++++++--------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/data/resolvers/queries/companies.js b/src/data/resolvers/queries/companies.js index e88c643bf..fbc0632a1 100644 --- a/src/data/resolvers/queries/companies.js +++ b/src/data/resolvers/queries/companies.js @@ -20,13 +20,12 @@ const listQuery = async params => { const companyQueries = { /** * Companies list - * @param {Object} args - * @param {CompanyListParams} args.params + * @param {Object} args - Query params * @return {Promise} filtered companies list by given parameters */ - async companies(root, { params }) { + async companies(root, { ids, ...params }) { if (params.ids) { - return paginate(Companies.find({ _id: { $in: params.ids } }), params); + return paginate(Companies.find({ _id: { $in: ids } }), params); } const selector = await listQuery(params); @@ -36,13 +35,12 @@ const companyQueries = { /** * Group company counts by segments - * @param {Object} args - * @param {CompanyListParams} args.params + * @param {Object} args - Query params * @return {Object} counts map */ - async companyCounts(root, { params }) { + async companyCounts(root, args) { const counts = { bySegment: {}, byBrand: {}, byIntegrationType: {}, byTag: {} }; - const selector = await listQuery(params); + const selector = await listQuery(args); const count = query => { const findQuery = Object.assign({}, selector, query); diff --git a/src/data/schema/company.js b/src/data/schema/company.js index 794ec2621..f2f8326c2 100644 --- a/src/data/schema/company.js +++ b/src/data/schema/company.js @@ -1,11 +1,4 @@ export const types = ` - input CompanyListParams { - limit: Int, - page: String, - segment: String, - ids: [String] - } - type Company { _id: String! name: String @@ -23,9 +16,17 @@ export const types = ` } `; +const queryParams = ` + limit: Int, + page: Int, + perPage: Int, + segment: String, + ids: [String] +`; + export const queries = ` - companies(params: CompanyListParams): [Company] - companyCounts(params: CompanyListParams): JSON + companies(${queryParams}): [Company] + companyCounts(${queryParams}): JSON companyDetail(_id: String!): Company `; From 5fa25450afcb99d6c1bfe8e82e838874e5e2d92d Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 13 Nov 2017 00:29:29 +0800 Subject: [PATCH 268/318] Fix customer list queries --- src/data/resolvers/queries/customers.js | 9 ++++----- src/data/schema/company.js | 1 + src/data/schema/customer.js | 23 +++++++++++------------ 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/data/resolvers/queries/customers.js b/src/data/resolvers/queries/customers.js index bac8897c9..83f22564f 100644 --- a/src/data/resolvers/queries/customers.js +++ b/src/data/resolvers/queries/customers.js @@ -48,14 +48,13 @@ const customerQueries = { /** * Customers list * @param {Object} args - * @param {CustomerListParams} args.params * @return {Promise} filtered customers list by given parameters */ - async customers(root, { params }) { + async customers(root, { ids, ...params }) { const sort = { 'messengerData.lastSeenAt': -1 }; - if (params.ids) { - const selector = { _id: { $in: params.ids } }; + if (ids) { + const selector = { _id: { $in: ids } }; return paginate(Customers.find(selector), params).sort(sort); } @@ -70,7 +69,7 @@ const customerQueries = { * @param {CustomerListParams} args.params * @return {Object} counts map */ - async customerCounts(root, { params }) { + async customerCounts(root, params) { const counts = { bySegment: {}, byBrand: {}, byIntegrationType: {}, byTag: {} }; const selector = await listQuery(params); diff --git a/src/data/schema/company.js b/src/data/schema/company.js index f2f8326c2..f956dc12a 100644 --- a/src/data/schema/company.js +++ b/src/data/schema/company.js @@ -21,6 +21,7 @@ const queryParams = ` page: Int, perPage: Int, segment: String, + tag: String, ids: [String] `; diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index a43210dda..bc6b3d1d5 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -1,14 +1,4 @@ export const types = ` - input CustomerListParams { - brand: String, - integration: String, - tag: String, - limit: Int, - page: String, - segment: String, - ids: [String] - } - type Customer { _id: String! integrationId: String @@ -33,9 +23,18 @@ export const types = ` } `; +const queryParams = ` + limit: Int, + page: Int, + perPage: Int, + segment: String, + tag: String, + ids: [String] +`; + export const queries = ` - customers(params: CustomerListParams): [Customer] - customerCounts(params: CustomerListParams): JSON + customers(${queryParams}): [Customer] + customerCounts(${queryParams}): JSON customerDetail(_id: String!): Customer customerListForSegmentPreview(segment: JSON, limit: Int): [Customer] `; From 798d8c7ecfdfcc8a0478df85c127e00e8447e023 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 13 Nov 2017 00:37:06 +0800 Subject: [PATCH 269/318] Fix engage list queries --- src/data/resolvers/queries/engages.js | 2 +- src/data/schema/engage.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/resolvers/queries/engages.js b/src/data/resolvers/queries/engages.js index 216a64a3b..7b6fe3888 100644 --- a/src/data/resolvers/queries/engages.js +++ b/src/data/resolvers/queries/engages.js @@ -105,7 +105,7 @@ const engageQueries = { * @param {Object} params - Search params * @return {Promise} filtered messages list by given parameters */ - engageMessages(root, { params = {} }, { user }) { + engageMessages(root, params, { user }) { const { kind, status, tag, ids } = params; if (ids) { diff --git a/src/data/schema/engage.js b/src/data/schema/engage.js index 780072210..dc0b46825 100644 --- a/src/data/schema/engage.js +++ b/src/data/schema/engage.js @@ -46,7 +46,7 @@ export const types = ` `; export const queries = ` - engageMessages(params: JSON): [EngageMessage] + engageMessages(kind: String, status: String, tag: String, ids: [String]): [EngageMessage] engageMessageDetail(_id: String): EngageMessage engageMessageCounts(name: String!, kind: String, status: String): JSON engageMessagesTotalCount: Int From 27c6577fa0d2f657c57e33a6517ba53e771b1291 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 13 Nov 2017 00:51:18 +0800 Subject: [PATCH 270/318] Fix conversation list queries --- src/data/resolvers/queries/conversations.js | 11 ++++--- src/data/schema/conversation.js | 33 ++++++++++----------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/data/resolvers/queries/conversations.js b/src/data/resolvers/queries/conversations.js index 8d93ccaa1..762651a01 100644 --- a/src/data/resolvers/queries/conversations.js +++ b/src/data/resolvers/queries/conversations.js @@ -6,11 +6,10 @@ import { moduleRequireLogin } from '../../permissions'; const conversationQueries = { /** * Conversataions list - * @param {Object} args - * @param {ConversationListParams} args.params + * @param {Object} params - Query params * @return {Promise} filtered conversations list by given parameters */ - async conversations(root, { params }, { user }) { + async conversations(root, params, { user }) { // filter by ids of conversations if (params && params.ids) { return Conversations.find({ _id: { $in: params.ids } }).sort({ createdAt: -1 }); @@ -30,13 +29,13 @@ const conversationQueries = { * Group conversation counts by brands, channels, integrations, status * * @param {Object} args + * @param {Object} params - Query params * @param {Object} context - * @param {ConversationListParams} args.params * @param {Object} context.user * * @return {Object} counts map */ - async conversationCounts(root, { params }, { user }) { + async conversationCounts(root, params, { user }) { const response = { byChannels: {}, byIntegrationTypes: {}, @@ -158,7 +157,7 @@ const conversationQueries = { * Get all conversations count. We will use it in pager * @return {Promise} total count */ - async conversationsTotalCount(root, { params }, { user }) { + async conversationsTotalCount(root, params, { user }) { // initiate query builder const qb = new QueryBuilder(params, { _id: user._id }); diff --git a/src/data/schema/conversation.js b/src/data/schema/conversation.js index f8ee434bf..36e8eebcc 100644 --- a/src/data/schema/conversation.js +++ b/src/data/schema/conversation.js @@ -1,18 +1,4 @@ export const types = ` - input ConversationListParams { - limit: Int, - channelId: String - status: String - unassigned: String - brandId: String - tag: String - integrationType: String - participating: String - starred: String - ids: [String] - } - - type Conversation { _id: String! content: String @@ -81,11 +67,24 @@ export const types = ` } `; +const listParams = ` + limit: Int, + channelId: String + status: String + unassigned: String + brandId: String + tag: String + integrationType: String + participating: String + starred: String + ids: [String] +`; + export const queries = ` - conversations(params: ConversationListParams!): [Conversation] - conversationCounts(params: ConversationListParams): JSON + conversations(${listParams}): [Conversation] + conversationCounts(${listParams}): JSON + conversationsTotalCount(${listParams}): Int conversationDetail(_id: String!): Conversation - conversationsTotalCount(params: ConversationListParams): Int conversationsGetLast: Conversation `; From e196d2cd625e96279a766d10e0c5e5201c19b368 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 13 Nov 2017 00:54:42 +0800 Subject: [PATCH 271/318] Display all values if there is no pagination --- src/data/resolvers/queries/utils.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/data/resolvers/queries/utils.js b/src/data/resolvers/queries/utils.js index 1a3320192..a04f75ee6 100644 --- a/src/data/resolvers/queries/utils.js +++ b/src/data/resolvers/queries/utils.js @@ -3,6 +3,10 @@ export const paginate = (collection, params) => { const { page, perPage } = params || {}; + if (!page && !perPage) { + return collection; + } + const _page = Number(page || '1'); const _limit = Number(perPage || '20'); From 9ba0c331366f937d828989842af2cbb8b744f1bd Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 13 Nov 2017 01:05:09 +0800 Subject: [PATCH 272/318] Remove limit from customer list params --- src/data/schema/company.js | 1 - src/data/schema/customer.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/data/schema/company.js b/src/data/schema/company.js index f956dc12a..383b48b9e 100644 --- a/src/data/schema/company.js +++ b/src/data/schema/company.js @@ -17,7 +17,6 @@ export const types = ` `; const queryParams = ` - limit: Int, page: Int, perPage: Int, segment: String, diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index bc6b3d1d5..4fa6e59c7 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -24,7 +24,6 @@ export const types = ` `; const queryParams = ` - limit: Int, page: Int, perPage: Int, segment: String, From 292fdf0b6350ecd412067efd987dcf79944f0ea6 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 13 Nov 2017 16:42:51 +0800 Subject: [PATCH 273/318] Call conversationsChanged subscription when markAsRead --- src/data/resolvers/mutations/conversations.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 047e35a95..ad0f43159 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -302,7 +302,12 @@ const conversationMutations = { * @return {Promise} Conversation object with mark as read */ async conversationMarkAsRead(root, { _id }, { user }) { - return Conversations.markAsReadConversation(_id, user._id); + const conversation = await Conversations.markAsReadConversation(_id, user._id); + + // notify graphl subscription + conversationsChanged([_id], 'readState'); + + return conversation; }, }; From 4c8880d64bbdf9ada94ba0c36d96b7561bb90609 Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 14 Nov 2017 11:18:44 +0800 Subject: [PATCH 274/318] Close #26 --- src/__tests__/engageMessageMutations.test.js | 37 ++++----- src/cronJobs/engages.js | 23 ++++++ src/cronJobs/index.js | 2 + src/data/resolvers/mutations/conversations.js | 7 +- src/data/resolvers/mutations/engageUtils.js | 2 +- src/db/models/ConversationMessages.js | 1 + src/db/models/Engages.js | 82 +++++++++++-------- 7 files changed, 92 insertions(+), 62 deletions(-) create mode 100644 src/cronJobs/engages.js diff --git a/src/__tests__/engageMessageMutations.test.js b/src/__tests__/engageMessageMutations.test.js index ed1b46772..8d4135120 100644 --- a/src/__tests__/engageMessageMutations.test.js +++ b/src/__tests__/engageMessageMutations.test.js @@ -38,7 +38,7 @@ describe('engage message mutation tests', () => { beforeEach(async () => { _user = await userFactory({}); _segment = await segmentsFactory({}); - _message = await engageMessageFactory({}); + _message = await engageMessageFactory({ userId: _user._id }); _emailTemplate = await emailTemplateFactory({}); _customer = await customerFactory({}); _integration = await integrationFactory({ brandId: 'brandId' }); @@ -165,25 +165,15 @@ describe('engage message mutation tests', () => { }); test('set live manual', async () => { - EngageMessages.engageMessageSetLive = jest.fn(() => ({ - _id: _message._id, - method: 'messenger', - title: 'Send via messenger', - fromUserId: _user._id, - segmentId: _segment._id, - isLive: true, - customerIds: [_customer._id], - messenger: { - brandId: 'brandId', - content: 'messenger content', - }, - })); + EngageMessages.engageMessageSetLive = jest.fn(() => { + return _message; + }); const conversationObj = { userId: _user._id, customerId: _customer._id, integrationId: _integration._id, - content: 'messenger content', + content: _message.messenger.content, }; const conversationMessageObj = { @@ -191,24 +181,31 @@ describe('engage message mutation tests', () => { messageId: _message._id, fromUserId: _user._id, brandId: 'brandId', - content: 'messenger content', + rules: [], + content: _message.messenger.content, }, conversationId: 'convId', userId: _user._id, customerId: _customer._id, - content: 'messenger content', + content: _message.messenger.content, }; + _message.segmentId = _segment._id; + _message.messenger.brandId = _integration.brandId; + await _message.save(); + Conversations.createConversation = jest.fn(() => ({ _id: 'convId' })); ConversationMessages.createMessage = jest.fn(); await mutations.engageMessageSetLiveManual(null, _message._id, { user: _user }); - expect(EngageMessages.engageMessageSetLive).toBeCalledWith(_message._id); expect(EngageMessages.engageMessageSetLive.mock.calls.length).toBe(1); - expect(Conversations.createConversation).toBeCalledWith(conversationObj); + expect(EngageMessages.engageMessageSetLive).toBeCalledWith(_message._id); + expect(Conversations.createConversation.mock.calls.length).toBe(1); - expect(ConversationMessages.createMessage).toBeCalledWith(conversationMessageObj); + expect(Conversations.createConversation.mock.calls[0][0]).toEqual(conversationObj); + expect(ConversationMessages.createMessage.mock.calls.length).toBe(1); + expect(ConversationMessages.createMessage.mock.calls[0][0]).toEqual(conversationMessageObj); }); }); diff --git a/src/cronJobs/engages.js b/src/cronJobs/engages.js new file mode 100644 index 000000000..4b3045d47 --- /dev/null +++ b/src/cronJobs/engages.js @@ -0,0 +1,23 @@ +import schedule from 'node-schedule'; +import { EngageMessages } from '../db/models'; +import { send } from '../data/resolvers/mutations/engageUtils'; + +/** +* Send engage auto messages +*/ +export const sendAutoMessage = async () => { + const messages = await EngageMessages.find({ kind: 'auto', isLive: true }); + + for (let message of messages) { + send(message); + } +}; + +// every day at 23 45 +schedule.scheduleJob('* 45 23 * *', function() { + sendAutoMessage(); +}); + +export default { + sendAutoMessage, +}; diff --git a/src/cronJobs/index.js b/src/cronJobs/index.js index 398b4caba..afc520c59 100644 --- a/src/cronJobs/index.js +++ b/src/cronJobs/index.js @@ -1,7 +1,9 @@ import conversations from './conversations'; import activityLogs from './activityLogs'; +import sendAutoMessage from './engages'; export default { ...conversations, ...activityLogs, + ...sendAutoMessage, }; diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index ad0f43159..047e35a95 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -302,12 +302,7 @@ const conversationMutations = { * @return {Promise} Conversation object with mark as read */ async conversationMarkAsRead(root, { _id }, { user }) { - const conversation = await Conversations.markAsReadConversation(_id, user._id); - - // notify graphl subscription - conversationsChanged([_id], 'readState'); - - return conversation; + return Conversations.markAsReadConversation(_id, user._id); }, }; diff --git a/src/data/resolvers/mutations/engageUtils.js b/src/data/resolvers/mutations/engageUtils.js index 0c3cb77f9..83da535db 100644 --- a/src/data/resolvers/mutations/engageUtils.js +++ b/src/data/resolvers/mutations/engageUtils.js @@ -159,7 +159,7 @@ const sendViaMessenger = async message => { engageData: { messageId: message._id, fromUserId, - ...message.messenger, + ...message.messenger.toJSON(), }, conversationId: conversation._id, userId: fromUserId, diff --git a/src/db/models/ConversationMessages.js b/src/db/models/ConversationMessages.js index 843fb90a7..d557d91d1 100644 --- a/src/db/models/ConversationMessages.js +++ b/src/db/models/ConversationMessages.js @@ -75,6 +75,7 @@ class Message { */ static async createMessage(doc) { const message = await this.create({ + internal: false, ...doc, createdAt: new Date(), }); diff --git a/src/db/models/Engages.js b/src/db/models/Engages.js index 843c857ce..e5dc199e3 100644 --- a/src/db/models/Engages.js +++ b/src/db/models/Engages.js @@ -2,45 +2,57 @@ import mongoose from 'mongoose'; import { MESSENGER_KINDS, SENT_AS_CHOICES, METHODS } from '../../data/constants'; import { field } from './utils'; -const EmailSchema = mongoose.Schema({ - templateId: field({ type: String }), - subject: field({ type: String }), - content: field({ type: String }), -}); - -const RuleSchema = mongoose.Schema({ - _id: field({ type: String }), - - // browserLanguage, currentUrl, etc ... - kind: field({ type: String }), - - // Browser language, Current url etc ... - text: field({ type: String }), - - // is, isNot, startsWith - condition: field({ type: String }), - - value: field({ type: String }), -}); - -const MessengerSchema = mongoose.Schema({ - brandId: field({ type: String }), - kind: field({ - type: String, - enum: MESSENGER_KINDS.ALL, - }), - sentAs: field({ - type: String, - enum: SENT_AS_CHOICES.ALL, - }), - content: field({ type: String }), - rules: field({ type: [RuleSchema] }), -}); +const EmailSchema = mongoose.Schema( + { + templateId: field({ type: String }), + subject: field({ type: String }), + content: field({ type: String }), + }, + { _id: false }, +); + +const RuleSchema = mongoose.Schema( + { + _id: field({ type: String }), + + // browserLanguage, currentUrl, etc ... + kind: field({ type: String }), + + // Browser language, Current url etc ... + text: field({ type: String }), + + // is, isNot, startsWith + condition: field({ type: String }), + + value: field({ type: String }), + }, + { _id: false }, +); + +const MessengerSchema = mongoose.Schema( + { + brandId: field({ type: String }), + kind: field({ + type: String, + enum: MESSENGER_KINDS.ALL, + }), + sentAs: field({ + type: String, + enum: SENT_AS_CHOICES.ALL, + }), + content: field({ type: String }), + rules: field({ type: [RuleSchema] }), + }, + { _id: false }, +); const EngageMessageSchema = mongoose.Schema({ _id: field({ pkey: true }), kind: field({ type: String }), - segmentId: field({ type: String }), + segmentId: field({ + type: String, + optional: true, + }), customerIds: field({ type: [String] }), title: field({ type: String }), fromUserId: field({ type: String }), From 59869aace2f644169e27f335fe1b6e7ae2715753 Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 14 Nov 2017 11:55:36 +0800 Subject: [PATCH 275/318] Engage little fixes --- src/data/resolvers/mutations/engageUtils.js | 3 ++- src/data/resolvers/mutations/engages.js | 4 ++-- src/db/models/Engages.js | 5 ++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/data/resolvers/mutations/engageUtils.js b/src/data/resolvers/mutations/engageUtils.js index 83da535db..4896f1d39 100644 --- a/src/data/resolvers/mutations/engageUtils.js +++ b/src/data/resolvers/mutations/engageUtils.js @@ -98,8 +98,9 @@ const sendViaEmail = async message => { // add new delivery report EngageMessages.addNewDeliveryReport(message._id, mailMessageId, customer._id); - // send email + // send email ========= const transporter = await createTransporter(); + transporter.sendMail( { from: userEmail, diff --git a/src/data/resolvers/mutations/engages.js b/src/data/resolvers/mutations/engages.js index 826a41a09..9553566a1 100644 --- a/src/data/resolvers/mutations/engages.js +++ b/src/data/resolvers/mutations/engages.js @@ -20,7 +20,7 @@ const engageMutations = { * @return {Promise} message object */ async engageMessageAdd(root, doc) { - const engageMessage = EngageMessages.createEngageMessage(doc); + const engageMessage = await EngageMessages.createEngageMessage(doc); // if manual and live then send immediately if (doc.kind === MESSAGE_KINDS.MANUAL && doc.isLive) { @@ -82,7 +82,7 @@ const engageMutations = { * @return {Promise} updated message object */ async engageMessageSetLiveManual(root, _id) { - const engageMessage = EngageMessages.engageMessageSetLive(_id); + const engageMessage = await EngageMessages.engageMessageSetLive(_id); await send(engageMessage); diff --git a/src/db/models/Engages.js b/src/db/models/Engages.js index e5dc199e3..2376f6fb8 100644 --- a/src/db/models/Engages.js +++ b/src/db/models/Engages.js @@ -4,7 +4,10 @@ import { field } from './utils'; const EmailSchema = mongoose.Schema( { - templateId: field({ type: String }), + templateId: field({ + type: String, + optional: true, + }), subject: field({ type: String }), content: field({ type: String }), }, From 11eb501c939d14c08932f21aa063e8b994f9fada Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 14 Nov 2017 12:23:29 +0800 Subject: [PATCH 276/318] Close #27 --- src/__tests__/engageMessageMutations.test.js | 18 +++++++++++++++++- src/data/resolvers/mutations/engageUtils.js | 20 +++++++++++++------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/__tests__/engageMessageMutations.test.js b/src/__tests__/engageMessageMutations.test.js index 8d4135120..8b4d05895 100644 --- a/src/__tests__/engageMessageMutations.test.js +++ b/src/__tests__/engageMessageMutations.test.js @@ -3,7 +3,7 @@ import { connect, disconnect } from '../db/connection'; import mutations from '../data/resolvers/mutations'; -import { send } from '../data/resolvers/mutations/engageUtils'; +import { send, sendEmailCb } from '../data/resolvers/mutations/engageUtils'; import { EngageMessages, Users, @@ -208,4 +208,20 @@ describe('engage message mutation tests', () => { expect(ConversationMessages.createMessage.mock.calls.length).toBe(1); expect(ConversationMessages.createMessage.mock.calls[0][0]).toEqual(conversationMessageObj); }); + + test('set pause', async () => { + EngageMessages.changeDeliveryReportStatus = jest.fn(); + + const mailMessageId = 'DFDAFDFDF'; + + await sendEmailCb(_message._id, mailMessageId); + + expect(EngageMessages.changeDeliveryReportStatus.mock.calls.length).toBe(1); + + expect(EngageMessages.changeDeliveryReportStatus).toBeCalledWith( + _message._id, + mailMessageId, + 'sent', + ); + }); }); diff --git a/src/data/resolvers/mutations/engageUtils.js b/src/data/resolvers/mutations/engageUtils.js index 4896f1d39..b96dd3555 100644 --- a/src/data/resolvers/mutations/engageUtils.js +++ b/src/data/resolvers/mutations/engageUtils.js @@ -63,6 +63,17 @@ const findCustomers = async ({ customerIds, segmentId }) => { return await Customers.find(customerQuery); }; +/* + * Send email callback + */ +export const sendEmailCb = (messageId, mailMessageId, error) => { + // set new status + const status = error ? 'failed' : 'sent'; + + // update status + EngageMessages.changeDeliveryReportStatus(messageId, mailMessageId, status); +}; + /** * Send via email * @param {Object} engage message object @@ -108,13 +119,8 @@ const sendViaEmail = async message => { subject: replacedSubject, html: replacedContent, }, - error => { - // set new status - const status = error ? 'failed' : 'sent'; - - // update status - EngageMessages.changeDeliveryReportStatus(message._id, mailMessageId, status); - }, + /* istanbul ignore next */ + error => sendEmailCb(message._id, mailMessageId, error), ); } }; From 83efa79e22fb44f274e5570d6bcb62ff45fb97c1 Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 14 Nov 2017 14:10:47 +0800 Subject: [PATCH 277/318] Change conversationMessage log to conversation log --- src/__tests__/activityLogDb.test.js | 33 +++++++------- src/__tests__/activityLogQueries.test.js | 43 +++++++++--------- src/data/constants.js | 4 +- src/data/resolvers/mutations/activityLogs.js | 12 ++--- src/data/schema/activityLog.js | 2 +- src/db/models/ActivityLogs.js | 46 ++++++++++++++------ 6 files changed, 78 insertions(+), 62 deletions(-) diff --git a/src/__tests__/activityLogDb.test.js b/src/__tests__/activityLogDb.test.js index bedc221b3..b25c8bdf7 100644 --- a/src/__tests__/activityLogDb.test.js +++ b/src/__tests__/activityLogDb.test.js @@ -14,7 +14,7 @@ import { internalNoteFactory, customerFactory, companyFactory, - conversationMessageFactory, + conversationFactory, segmentFactory, } from '../db/factories'; @@ -125,12 +125,13 @@ describe('ActivityLogs model methods', () => { }); }); - test(`check if exceptions are being thrown as intended when calling createConversationMessageLog`, async () => { + test(`check if exceptions are being thrown as intended when + calling createConversationLog`, async () => { expect.assertions(2); - const message = await conversationMessageFactory({}); + const conversation = await conversationFactory({}); try { - await ActivityLogs.createConversationMessageLog(message, null); + await ActivityLogs.createConversationLog(conversation, null); } catch (e) { expect(e.message).toBe( `'customer' must be supplied when adding activity log for conversations`, @@ -138,7 +139,7 @@ describe('ActivityLogs model methods', () => { } try { - await ActivityLogs.createConversationMessageLog(message, {}); + await ActivityLogs.createConversationLog(conversation, {}); } catch (e) { expect(e.message).toBe( `'customer' must be supplied when adding activity log for conversations`, @@ -146,13 +147,13 @@ describe('ActivityLogs model methods', () => { } }); - test(`check if createConversationMessageLog is working as intended`, async () => { - const message = await conversationMessageFactory({}); + test(`check if createConversationLog is working as intended`, async () => { + const conversation = await conversationFactory({}); const companyA = await companyFactory({}); const companyB = await companyFactory({}); const customer = await customerFactory({ companyIds: [companyA._id, companyB._id] }); - let aLog = await ActivityLogs.createConversationMessageLog(message, customer); + let aLog = await ActivityLogs.createConversationLog(conversation, customer); // check customer conversation log expect(aLog.performedBy.toObject()).toEqual({ @@ -164,17 +165,17 @@ describe('ActivityLogs model methods', () => { id: customer._id, }); expect(aLog.activity.toObject()).toEqual({ - type: ACTIVITY_TYPES.CONVERSATION_MESSAGE, + type: ACTIVITY_TYPES.CONVERSATION, action: ACTIVITY_ACTIONS.CREATE, - content: message.content, - id: message._id, + content: conversation.content, + id: conversation._id, }); // check company conversation logs ===================================== aLog = await ActivityLogs.findOne({ - 'activity.type': ACTIVITY_TYPES.CONVERSATION_MESSAGE, + 'activity.type': ACTIVITY_TYPES.CONVERSATION, 'activity.action': ACTIVITY_ACTIONS.CREATE, - 'activity.id': message._id, + 'activity.id': conversation._id, 'coc.type': COC_CONTENT_TYPES.COMPANY, 'coc.id': companyA._id, }); @@ -183,9 +184,9 @@ describe('ActivityLogs model methods', () => { expect(aLog.coc.id).toBe(companyA._id); aLog = await ActivityLogs.findOne({ - 'activity.type': ACTIVITY_TYPES.CONVERSATION_MESSAGE, + 'activity.type': ACTIVITY_TYPES.CONVERSATION, 'activity.action': ACTIVITY_ACTIONS.CREATE, - 'activity.id': message._id, + 'activity.id': conversation._id, 'coc.type': COC_CONTENT_TYPES.COMPANY, 'coc.id': companyB._id, }); @@ -196,7 +197,7 @@ describe('ActivityLogs model methods', () => { expect(await ActivityLogs.find({}).count()).toBe(3); // test whether activity logs for this conversation is being duplicated or not ======== - await ActivityLogs.createConversationMessageLog(message, customer); + await ActivityLogs.createConversationLog(conversation, customer); expect(await ActivityLogs.find({}).count()).toBe(3); }); diff --git a/src/__tests__/activityLogQueries.test.js b/src/__tests__/activityLogQueries.test.js index 15c241a6e..6d21beb1d 100644 --- a/src/__tests__/activityLogQueries.test.js +++ b/src/__tests__/activityLogQueries.test.js @@ -4,12 +4,7 @@ import { connect, disconnect } from '../db/connection'; import { COC_CONTENT_TYPES } from '../data/constants'; import mutations from '../data/resolvers/mutations'; -import { - userFactory, - segmentFactory, - conversationMessageFactory, - customerFactory, -} from '../db/factories'; +import { userFactory, segmentFactory, conversationFactory, customerFactory } from '../db/factories'; import { ActivityLogs } from '../db/models'; import schema from '../data'; import cronJobs from '../cronJobs'; @@ -21,11 +16,11 @@ afterAll(() => disconnect()); describe('activityLogs', () => { let _user; - let _message; + let _conversation; beforeAll(async () => { _user = await userFactory({}); - _message = await conversationMessageFactory({}); + _conversation = await conversationFactory({}); }); afterEach(() => { @@ -44,10 +39,10 @@ describe('activityLogs', () => { { user: _user }, ); - // create conversation message - await mutations.activityLogsAddConversationMessageLog(null, { + // create conversation + await mutations.activityLogsAddConversationLog(null, { customerId: customer._id, - messageId: _message._id, + conversationId: _conversation._id, }); // create internal note @@ -121,9 +116,9 @@ describe('activityLogs', () => { expect(logs[0].list[1].action).toBe('internal_note-create'); expect(logs[0].list[1].content).toBe(internalNote.content); - expect(logs[0].list[2].id).toBe(_message._id); - expect(logs[0].list[2].action).toBe('conversation_message-create'); - expect(logs[0].list[2].content).toBe(_message.content); + expect(logs[0].list[2].id).toBe(_conversation._id); + expect(logs[0].list[2].action).toBe('conversation-create'); + expect(logs[0].list[2].content).toBe(_conversation.content); expect(logs[0].list[3].id).toBe(customer._id); expect(logs[0].list[3].action).toBe('customer-create'); @@ -156,7 +151,7 @@ describe('activityLogs', () => { await ActivityLogs.update( { - 'activity.type': 'conversation_message', + 'activity.type': 'conversation', 'activity.action': 'create', }, { @@ -197,11 +192,13 @@ describe('activityLogs', () => { expect(logs[yearMonthLength - 1].list[februaryLogLength].action).toBe('internal_note-create'); expect(logs[yearMonthLength - 1].list[februaryLogLength].content).toBe(internalNote.content); - expect(logs[yearMonthLength - 1].list[februaryLogLength - 1].id).toBe(_message._id); + expect(logs[yearMonthLength - 1].list[februaryLogLength - 1].id).toBe(_conversation._id); expect(logs[yearMonthLength - 1].list[februaryLogLength - 1].action).toBe( - 'conversation_message-create', + 'conversation-create', + ); + expect(logs[yearMonthLength - 1].list[februaryLogLength - 1].content).toBe( + _conversation.content, ); - expect(logs[yearMonthLength - 1].list[februaryLogLength - 1].content).toBe(_message.content); expect(logs[yearMonthLength - 1].list[februaryLogLength - 2].id).toBe(customer._id); expect(logs[yearMonthLength - 1].list[februaryLogLength - 2].action).toBe('customer-create'); @@ -219,9 +216,9 @@ describe('activityLogs', () => { ); const customer = await customerFactory({ companyIds: [company._id] }); - await mutations.activityLogsAddConversationMessageLog(null, { + await mutations.activityLogsAddConversationLog(null, { customerId: customer._id, - messageId: _message._id, + conversationId: _conversation._id, }); const query = ` @@ -258,9 +255,9 @@ describe('activityLogs', () => { const logs = result.data.activityLogsCompany; // test values =========================== - expect(logs[0].list[0].id).toBe(_message._id); - expect(logs[0].list[0].action).toBe('conversation_message-create'); - expect(logs[0].list[0].content).toBe(_message.content); + expect(logs[0].list[0].id).toBe(_conversation._id); + expect(logs[0].list[0].action).toBe('conversation-create'); + expect(logs[0].list[0].content).toBe(_conversation.content); expect(logs[0].list[1].id).toBe(company._id); expect(logs[0].list[1].action).toBe('company-create'); diff --git a/src/data/constants.js b/src/data/constants.js index 07e64fec6..0ad54a361 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -191,10 +191,10 @@ export const ACTIVITY_TYPES = { CUSTOMER: 'customer', COMPANY: 'company', INTERNAL_NOTE: 'internal_note', - CONVERSATION_MESSAGE: 'conversation_message', + CONVERSATION: 'conversation', SEGMENT: 'segment', - ALL: ['customer', 'company', 'internal_note', 'conversation_message', 'segment'], + ALL: ['customer', 'company', 'internal_note', 'conversation', 'segment'], }; export const ACTIVITY_ACTIONS = { diff --git a/src/data/resolvers/mutations/activityLogs.js b/src/data/resolvers/mutations/activityLogs.js index 0073c3691..7e7f31ec6 100644 --- a/src/data/resolvers/mutations/activityLogs.js +++ b/src/data/resolvers/mutations/activityLogs.js @@ -1,18 +1,18 @@ -import { ActivityLogs, Customers, Companies, ConversationMessages } from '../../../db/models'; +import { ActivityLogs, Customers, Companies, Conversations } from '../../../db/models'; export default { /** - * Add conversation message log + * Add conversation log * @param {Object} root * @param {Object} object2 - arguments * @param {string} customerId - id of customer - * @param {string} messageId - id of message + * @param {string} conversationId - id of conversation */ - async activityLogsAddConversationMessageLog(root, { customerId, messageId }) { + async activityLogsAddConversationLog(root, { customerId, conversationId }) { const customer = await Customers.findOne({ _id: customerId }); - const message = await ConversationMessages.findOne({ _id: messageId }); + const conversation = await Conversations.findOne({ _id: conversationId }); - return ActivityLogs.createConversationMessageLog(message, customer); + return ActivityLogs.createConversationLog(conversation, customer); }, /** diff --git a/src/data/schema/activityLog.js b/src/data/schema/activityLog.js index 3f89ea61e..88383127e 100644 --- a/src/data/schema/activityLog.js +++ b/src/data/schema/activityLog.js @@ -37,7 +37,7 @@ export const queries = ` `; export const mutations = ` - activityLogsAddConversationMessageLog(customerId: String!, messageId: String!): ActivityLog + activityLogsAddConversationLog(customerId: String!, conversationId: String!): ActivityLog activityLogsAddCustomerLog(_id: String!): ActivityLog activityLogsAddCompanyLog(_id: String!): ActivityLog `; diff --git a/src/db/models/ActivityLogs.js b/src/db/models/ActivityLogs.js index 0b4ca512e..d4a8842d1 100644 --- a/src/db/models/ActivityLogs.js +++ b/src/db/models/ActivityLogs.js @@ -143,24 +143,24 @@ class ActivityLog { }); } - static cocFindOne(messageId, cocId, cocType) { + static cocFindOne(conversationId, cocId, cocType) { return this.findOne({ - 'activity.type': ACTIVITY_TYPES.CONVERSATION_MESSAGE, + 'activity.type': ACTIVITY_TYPES.CONVERSATION, 'activity.action': ACTIVITY_ACTIONS.CREATE, - 'activity.id': messageId, + 'activity.id': conversationId, 'coc.type': cocType, 'performedBy.type': ACTIVITY_PERFORMER_TYPES.CUSTOMER, 'coc.id': cocId, }); } - static cocCreate(messageId, content, cocId, cocType) { + static cocCreate(conversationId, content, cocId, cocType) { return this.createDoc({ activity: { - type: ACTIVITY_TYPES.CONVERSATION_MESSAGE, + type: ACTIVITY_TYPES.CONVERSATION, action: ACTIVITY_ACTIONS.CREATE, content: content, - id: messageId, + id: conversationId, }, performer: { type: ACTIVITY_PERFORMER_TYPES.CUSTOMER, @@ -174,16 +174,16 @@ class ActivityLog { } /** - * Create a conversation message log for a given customer, + * Create a conversation log for a given customer, * if the customer is related to companies, * then create conversation log with all related companies - * @param {Object} message - Conversation object - * @param {string} message._id - Conversation document id + * @param {Object} conversation - Conversation object + * @param {string} conversation._id - Conversation document id * @param {Object} customer - Customer object * @param {string} customer.type - One of COC_CONTENT_TYPES choices * @param {string} customer.id - Customer document id */ - static async createConversationMessageLog(message, customer) { + static async createConversationLog(conversation, customer) { if (customer == null || (customer && !customer._id)) { throw new Error(`'customer' must be supplied when adding activity log for conversations`); } @@ -191,19 +191,37 @@ class ActivityLog { if (customer.companyIds && customer.companyIds.length > 0) { for (let companyId of customer.companyIds) { // check against duplication - const foundLog = await this.cocFindOne(message._id, companyId, COC_CONTENT_TYPES.COMPANY); + const foundLog = await this.cocFindOne( + conversation._id, + companyId, + COC_CONTENT_TYPES.COMPANY, + ); if (!foundLog) { - await this.cocCreate(message._id, message.content, companyId, COC_CONTENT_TYPES.COMPANY); + await this.cocCreate( + conversation._id, + conversation.content, + companyId, + COC_CONTENT_TYPES.COMPANY, + ); } } } // check against duplication ====== - const foundLog = await this.cocFindOne(message._id, customer._id, COC_CONTENT_TYPES.CUSTOMER); + const foundLog = await this.cocFindOne( + conversation._id, + customer._id, + COC_CONTENT_TYPES.CUSTOMER, + ); if (!foundLog) { - return this.cocCreate(message._id, message.content, customer._id, COC_CONTENT_TYPES.CUSTOMER); + return this.cocCreate( + conversation._id, + conversation.content, + customer._id, + COC_CONTENT_TYPES.CUSTOMER, + ); } } From f4d4d510a65f18fb27a97df8deb83f7fe0a17a43 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Tue, 14 Nov 2017 17:24:28 +0800 Subject: [PATCH 278/318] Add notification list query --- src/__tests__/notificationQueries.test.js | 5 ++- src/data/resolvers/queries/notifications.js | 39 ++++++++++++++++++++- src/data/schema/notification.js | 10 ++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/__tests__/notificationQueries.test.js b/src/__tests__/notificationQueries.test.js index 9b9c5ed31..b8da9915f 100644 --- a/src/__tests__/notificationQueries.test.js +++ b/src/__tests__/notificationQueries.test.js @@ -9,7 +9,7 @@ afterAll(() => disconnect()); describe('notificationsQueries', () => { test(`test if Error('Login required') exception is working as intended`, async () => { - expect.assertions(1); + expect.assertions(4); const expectError = async func => { try { @@ -19,7 +19,10 @@ describe('notificationsQueries', () => { } }; + expectError(notificationsQueries.notifications); + expectError(notificationsQueries.notificationsCount); expectError(notificationsQueries.notificationsModules); + expectError(notificationsQueries.notificationsGetConfigurations); }); test('test of getting notification list with success', () => { diff --git a/src/data/resolvers/queries/notifications.js b/src/data/resolvers/queries/notifications.js index 414076d82..67f1e086b 100644 --- a/src/data/resolvers/queries/notifications.js +++ b/src/data/resolvers/queries/notifications.js @@ -1,8 +1,45 @@ import { NOTIFICATION_MODULES } from '../../constants'; import { moduleRequireLogin } from '../../permissions'; -import { NotificationConfigurations } from '../../../db/models'; +import { Notifications, NotificationConfigurations } from '../../../db/models'; +import { paginate } from './utils'; const notificationQueries = { + /** + * Notifications list + * @param {Object} args + * @return {Promise} filtered notifications list by given parameters + */ + notifications(root, { requireRead, title, limit, ...params }, { user }) { + const sort = { date: -1 }; + const selector = { receiver: user._id }; + + if (requireRead) { + selector.isRead = false; + } + + if (title) { + selector.title = title; + } + + if (limit) { + return Notifications.find(selector) + .sort(sort) + .limit(limit); + } + + return paginate(Notifications.find(selector), params).sort(sort); + }, + + notificationsCount(root, { requireRead }, { user }) { + const selector = { receiver: user._id }; + + if (requireRead) { + selector.isRead = false; + } + + return Notifications.find(selector).count(); + }, + /** * Module list used in notifications * @param {Object} args diff --git a/src/data/schema/notification.js b/src/data/schema/notification.js index edcdd6ce0..4dce294ce 100644 --- a/src/data/schema/notification.js +++ b/src/data/schema/notification.js @@ -19,7 +19,17 @@ export const types = ` } `; +const params = ` + limit: Int, + page: Int, + perPage: Int, + requireRead: Boolean, + title: String +`; + export const queries = ` + notifications(${params}): [Notification] + notificationsCount(requireRead: Boolean): Int notificationsModules : [JSON] notificationsGetConfigurations : [NotificationConfiguration] `; From 38f942aa92338e72703e37499fb723e84a7a4b0f Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Wed, 15 Nov 2017 12:27:33 +0800 Subject: [PATCH 279/318] Add notification subscription --- src/data/resolvers/mutations/notifications.js | 11 ++++++++--- src/data/resolvers/queries/notifications.js | 6 +++++- src/data/resolvers/subscriptions/index.js | 2 ++ src/data/schema/index.js | 1 + src/data/schema/notification.js | 2 +- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/data/resolvers/mutations/notifications.js b/src/data/resolvers/mutations/notifications.js index 664877fd4..39666ab6a 100644 --- a/src/data/resolvers/mutations/notifications.js +++ b/src/data/resolvers/mutations/notifications.js @@ -1,6 +1,6 @@ import { NotificationConfigurations, Notifications } from '../../../db/models'; - import { moduleRequireLogin } from '../../permissions'; +import { pubsub } from '../subscriptions'; const notificationMutations = { /** @@ -28,8 +28,13 @@ const notificationMutations = { * @return {Promise} * @throws {Error} throws Error('Login required') if user is not logged in */ - notificationsMarkAsRead(root, { ids }) { - return Notifications.markAsRead(ids); + notificationsMarkAsRead(root, { _ids }) { + // subscribe + pubsub.publish('notificationsChanged', { + notificationsChanged: { notificationIds: _ids }, + }); + + return Notifications.markAsRead(_ids); }, }; diff --git a/src/data/resolvers/queries/notifications.js b/src/data/resolvers/queries/notifications.js index 67f1e086b..b74843997 100644 --- a/src/data/resolvers/queries/notifications.js +++ b/src/data/resolvers/queries/notifications.js @@ -30,7 +30,11 @@ const notificationQueries = { return paginate(Notifications.find(selector), params).sort(sort); }, - notificationsCount(root, { requireRead }, { user }) { + /** + * Notification counts + * @return {Int} notification counts + */ + notificationCounts(root, { requireRead }, { user }) { const selector = { receiver: user._id }; if (requireRead) { diff --git a/src/data/resolvers/subscriptions/index.js b/src/data/resolvers/subscriptions/index.js index ac92d036b..8cf2cc880 100644 --- a/src/data/resolvers/subscriptions/index.js +++ b/src/data/resolvers/subscriptions/index.js @@ -1,9 +1,11 @@ import { PubSub } from 'graphql-subscriptions'; import conversations from './conversations'; +import notifications from './notifications'; export const pubsub = new PubSub(); export default { ...conversations, + ...notifications, }; diff --git a/src/data/schema/index.js b/src/data/schema/index.js index fef16028c..2c0a54d24 100755 --- a/src/data/schema/index.js +++ b/src/data/schema/index.js @@ -165,5 +165,6 @@ export const subscriptions = ` conversationChanged(_id: String!): ConversationChangedResponse conversationMessageInserted(_id: String!): ConversationMessage conversationsChanged(customerId: String): ConversationsChangedResponse + notificationsChanged(ids: [String]): Boolean } `; diff --git a/src/data/schema/notification.js b/src/data/schema/notification.js index 4dce294ce..279fd3dad 100644 --- a/src/data/schema/notification.js +++ b/src/data/schema/notification.js @@ -29,7 +29,7 @@ const params = ` export const queries = ` notifications(${params}): [Notification] - notificationsCount(requireRead: Boolean): Int + notificationCounts(requireRead: Boolean): Int notificationsModules : [JSON] notificationsGetConfigurations : [NotificationConfiguration] `; From f230cf909078db21cc21d6b7925810ad51a765ef Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 15 Nov 2017 12:30:21 +0800 Subject: [PATCH 280/318] Add _id to activity log model --- src/data/schema/activityLog.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/schema/activityLog.js b/src/data/schema/activityLog.js index 88383127e..622bd2df0 100644 --- a/src/data/schema/activityLog.js +++ b/src/data/schema/activityLog.js @@ -23,6 +23,7 @@ export const types = ` } type ActivityLog { + _id: String! action: String! id: String! createdAt: Date! From 582fcdfa7d77a40618482e4114966e1df63fc2a4 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Wed, 15 Nov 2017 12:33:46 +0800 Subject: [PATCH 281/318] Add notification subscription --- .../resolvers/subscriptions/notifications.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/data/resolvers/subscriptions/notifications.js diff --git a/src/data/resolvers/subscriptions/notifications.js b/src/data/resolvers/subscriptions/notifications.js new file mode 100644 index 000000000..fb021dd47 --- /dev/null +++ b/src/data/resolvers/subscriptions/notifications.js @@ -0,0 +1,25 @@ +import { withFilter } from 'graphql-subscriptions'; +import { pubsub } from './'; + +export default { + /* + * Listen for any notifications read state + */ + notificationsChanged: { + subscribe: withFilter( + () => pubsub.asyncIterator('notificationsChanged'), + // filter by notificationIds + (payload, variables) => { + const notificationIds = payload.notificationsChanged.notificationIds; + const ids = variables.ids; + + return ( + notificationIds.length == ids.length && + notificationIds.every((element, index) => { + return element === ids[index]; + }) + ); + }, + ), + }, +}; From 3f68d37e0f97a35440f56ef3569d2f2a7a3f6a34 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Wed, 15 Nov 2017 12:39:35 +0800 Subject: [PATCH 282/318] Change notification query name --- src/__tests__/notificationQueries.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/notificationQueries.test.js b/src/__tests__/notificationQueries.test.js index b8da9915f..46d427fd2 100644 --- a/src/__tests__/notificationQueries.test.js +++ b/src/__tests__/notificationQueries.test.js @@ -20,7 +20,7 @@ describe('notificationsQueries', () => { }; expectError(notificationsQueries.notifications); - expectError(notificationsQueries.notificationsCount); + expectError(notificationsQueries.notificationCounts); expectError(notificationsQueries.notificationsModules); expectError(notificationsQueries.notificationsGetConfigurations); }); From 62a1173a55b9e4403460b8f32c5bc22c8b0bb0d6 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Wed, 15 Nov 2017 12:49:48 +0800 Subject: [PATCH 283/318] Change notification variable name --- src/__tests__/notificationMutations.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/notificationMutations.test.js b/src/__tests__/notificationMutations.test.js index 803ccd649..f5264c3bf 100644 --- a/src/__tests__/notificationMutations.test.js +++ b/src/__tests__/notificationMutations.test.js @@ -52,11 +52,11 @@ describe('testing mutations', () => { test('testing if notifications are being marked as read successfully', async () => { Notifications.markAsRead = jest.fn(); - const args = { ids: ['11111', '22222'] }; + const args = { _ids: ['11111', '22222'] }; await notificationMutations.notificationsMarkAsRead(null, args, { user: _user }); - expect(Notifications.markAsRead).toBeCalledWith(args['ids']); + expect(Notifications.markAsRead).toBeCalledWith(args['_ids']); expect(Notifications.markAsRead.mock.calls.length).toBe(1); }); }); From 73c2482cd5a534a2fa303680e25547e500933dee Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 15 Nov 2017 16:34:58 +0800 Subject: [PATCH 284/318] Close #29 --- src/data/resolvers/index.js | 3 +++ src/data/resolvers/mutations/conversations.js | 20 ++++++++++++++++--- src/data/resolvers/mutations/notifications.js | 6 ++---- src/data/resolvers/notification.js | 7 +++++++ .../resolvers/subscriptions/notifications.js | 17 +--------------- src/data/schema/notification.js | 2 +- 6 files changed, 31 insertions(+), 24 deletions(-) create mode 100644 src/data/resolvers/notification.js diff --git a/src/data/resolvers/index.js b/src/data/resolvers/index.js index 7a16111ad..9670a0e6e 100644 --- a/src/data/resolvers/index.js +++ b/src/data/resolvers/index.js @@ -12,6 +12,7 @@ import Company from './company'; import Segment from './segment'; import Conversation from './conversation'; import ConversationMessage from './conversationMessage'; +import Notification from './notification'; import KnowledgeBaseCategory from './knowledgeBaseCategory'; import KnowledgeBaseTopic from './knowledgeBaseTopic'; import ActivityLog from './activityLog'; @@ -38,6 +39,8 @@ export default { KnowledgeBaseCategory, KnowledgeBaseTopic, + Notification, + ActivityLog, ActivityLogForMonth, }; diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 047e35a95..895aa1d3b 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -73,6 +73,20 @@ export const conversationMessageCreated = async (message, conversationId) => { pubsub.publish('conversationsChanged', { conversationsChanged: { customerId: conversation.customerId, type: 'newMessage' }, }); + + // notify notification subscription + pubsub.publish('notificationsChanged'); +}; + +/* + * Send notificatio helper + */ +const sendNotification = doc => { + // send notification + utils.sendNotification(doc); + + // notify notification subscription + pubsub.publish('notificationsChanged'); }; const conversationMutations = { @@ -104,7 +118,7 @@ const conversationMutations = { const title = 'You have a new message.'; // send notification - utils.sendNotification({ + sendNotification({ createdUser: user._id, notifType: NOTIFICATION_TYPES.CONVERSATION_ADD_MESSAGE, title, @@ -182,7 +196,7 @@ const conversationMutations = { const content = 'Assigned user has changed'; // send notification - utils.sendNotification({ + sendNotification({ createdUser: user._id, notifType: NOTIFICATION_TYPES.CONVERSATION_ASSIGNEE_CHANGE, title: content, @@ -251,7 +265,7 @@ const conversationMutations = { const content = 'Conversation status has changed.'; - utils.sendNotification({ + sendNotification({ createdUser: user._id, notifType: NOTIFICATION_TYPES.CONVERSATION_STATE_CHANGE, title: content, diff --git a/src/data/resolvers/mutations/notifications.js b/src/data/resolvers/mutations/notifications.js index 39666ab6a..daff9d4a4 100644 --- a/src/data/resolvers/mutations/notifications.js +++ b/src/data/resolvers/mutations/notifications.js @@ -29,10 +29,8 @@ const notificationMutations = { * @throws {Error} throws Error('Login required') if user is not logged in */ notificationsMarkAsRead(root, { _ids }) { - // subscribe - pubsub.publish('notificationsChanged', { - notificationsChanged: { notificationIds: _ids }, - }); + // notify subscription + pubsub.publish('notificationsChanged'); return Notifications.markAsRead(_ids); }, diff --git a/src/data/resolvers/notification.js b/src/data/resolvers/notification.js new file mode 100644 index 000000000..e8449509a --- /dev/null +++ b/src/data/resolvers/notification.js @@ -0,0 +1,7 @@ +import { Users } from '../../db/models'; + +export default { + createdUser(notif) { + return Users.findOne({ _id: notif.createdUser }); + }, +}; diff --git a/src/data/resolvers/subscriptions/notifications.js b/src/data/resolvers/subscriptions/notifications.js index fb021dd47..939afb7b7 100644 --- a/src/data/resolvers/subscriptions/notifications.js +++ b/src/data/resolvers/subscriptions/notifications.js @@ -1,4 +1,3 @@ -import { withFilter } from 'graphql-subscriptions'; import { pubsub } from './'; export default { @@ -6,20 +5,6 @@ export default { * Listen for any notifications read state */ notificationsChanged: { - subscribe: withFilter( - () => pubsub.asyncIterator('notificationsChanged'), - // filter by notificationIds - (payload, variables) => { - const notificationIds = payload.notificationsChanged.notificationIds; - const ids = variables.ids; - - return ( - notificationIds.length == ids.length && - notificationIds.every((element, index) => { - return element === ids[index]; - }) - ); - }, - ), + subscribe: () => pubsub.asyncIterator('notificationsChanged'), }, }; diff --git a/src/data/schema/notification.js b/src/data/schema/notification.js index 279fd3dad..8d28d29ac 100644 --- a/src/data/schema/notification.js +++ b/src/data/schema/notification.js @@ -5,7 +5,7 @@ export const types = ` title: String link: String content: String - createdUser: String + createdUser: User receiver: String date: Date isRead: Boolean From 0d19bb1cd7418315e9be07772f31d133936a0f35 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 15 Nov 2017 17:02:24 +0800 Subject: [PATCH 285/318] Close #30 --- src/data/resolvers/queries/conversations.js | 12 +++++++++--- src/data/schema/conversation.js | 10 +++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/data/resolvers/queries/conversations.js b/src/data/resolvers/queries/conversations.js index 762651a01..cbe42d871 100644 --- a/src/data/resolvers/queries/conversations.js +++ b/src/data/resolvers/queries/conversations.js @@ -168,10 +168,16 @@ const conversationQueries = { /** * Get last conversation - * @return {Promise} - Conversation object + * @param {Object} params - Query params + * @return {Promise} filtered conversations list by given parameters */ - async conversationsGetLast() { - return Conversations.findOne({}).sort({ createdAt: -1 }); + async conversationsGetLast(root, params, { user }) { + // initiate query builder + const qb = new QueryBuilder(params, { _id: user._id }); + + await qb.buildAllQueries(); + + return Conversations.findOne(qb.mainQuery()).sort({ createdAt: -1 }); }, }; diff --git a/src/data/schema/conversation.js b/src/data/schema/conversation.js index 36e8eebcc..8f1da8e2b 100644 --- a/src/data/schema/conversation.js +++ b/src/data/schema/conversation.js @@ -67,7 +67,7 @@ export const types = ` } `; -const listParams = ` +const filterParams = ` limit: Int, channelId: String status: String @@ -81,11 +81,11 @@ const listParams = ` `; export const queries = ` - conversations(${listParams}): [Conversation] - conversationCounts(${listParams}): JSON - conversationsTotalCount(${listParams}): Int + conversations(${filterParams}): [Conversation] + conversationCounts(${filterParams}): JSON + conversationsTotalCount(${filterParams}): Int conversationDetail(_id: String!): Conversation - conversationsGetLast: Conversation + conversationsGetLast(${filterParams}): Conversation `; export const mutations = ` From 68ba7e25f8070ab09a254b7f6d80e871716b15a9 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 16 Nov 2017 12:32:24 +0800 Subject: [PATCH 286/318] Check owner in requireAdmin --- src/data/permissions.js | 2 +- src/db/models/Users.js | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/data/permissions.js b/src/data/permissions.js index ae3d22040..f691c7385 100644 --- a/src/data/permissions.js +++ b/src/data/permissions.js @@ -19,7 +19,7 @@ export const checkLogin = user => { * @return {null} */ export const checkAdmin = user => { - if (user.role != ROLES.ADMIN) { + if (!user.isOwner && user.role !== ROLES.ADMIN) { throw new Error('Permission required'); } }; diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 835acc334..474a09ef7 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -279,7 +279,13 @@ class User { * @return [String] - list of tokens */ static async createTokens(_user, secret) { - const user = { _id: _user._id, email: _user.email, details: _user.details }; + const user = { + _id: _user._id, + email: _user.email, + details: _user.details, + role: _user.role, + isOwner: _user.isOwner, + }; const createToken = await jwt.sign({ user }, secret, { expiresIn: '20m' }); From bfe7d13f461b05451a365b8e5112b3dc81f6ec6e Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 16 Nov 2017 16:40:52 +0800 Subject: [PATCH 287/318] Begining --- package.json | 1 + src/data/utils.js | 49 +++++++++++++++++++++++++++++++ src/index.js | 15 ++++++++++ yarn.lock | 75 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 138 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a9315d738..9d307fc35 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ ] }, "dependencies": { + "aws-sdk": "^2.151.0", "bcrypt": "^1.0.3", "body-parser": "^1.17.1", "cors": "^2.8.1", diff --git a/src/data/utils.js b/src/data/utils.js index afe76b56f..45eec3db8 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -1,8 +1,57 @@ +import AWS from 'aws-sdk'; import fs from 'fs'; import nodemailer from 'nodemailer'; import Handlebars from 'handlebars'; import { Notifications, Users } from '../db/models'; +/* + * Save binary data to amazon s3 + * @param {String} name - File name + * @param {Object} data - File binary data + * @return {String} - Uploaded file url + */ +export const uploadFile = async ({ name = 'test.jpeg', data }) => { + const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_BUCKET, AWS_PREFIX = '' } = process.env; + + // check credentials + if (!(AWS_ACCESS_KEY_ID || AWS_SECRET_ACCESS_KEY || AWS_BUCKET)) { + throw new Error('Security credentials are not configured'); + } + + // initialize s3 + const s3 = new AWS.S3({ + accessKeyId: AWS_ACCESS_KEY_ID, + secretAccessKey: AWS_SECRET_ACCESS_KEY, + }); + + // generate unique name + const fileName = `${AWS_PREFIX}${Math.random()}${name}`; + + // create buffer from file data + const buffer = new Buffer(data); + + // call putObject + await new Promise((resolve, reject) => { + s3.putObject( + { + Bucket: AWS_BUCKET, + Key: fileName, + Body: buffer, + ACL: 'public-read', + }, + (error, response) => { + if (error) { + return reject(error); + } + + return resolve(response); + }, + ); + }); + + return `https://s3.amazonaws.com/${AWS_BUCKET}/${fileName}`; +}; + /** * Read contents of a file * @param {string} filename - relative file path diff --git a/src/index.js b/src/index.js index f363f7270..e0bc4331b 100755 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ import { Customers } from './db/models'; import { connect } from './db/connection'; import { userMiddleware } from './auth'; import schema from './data'; +import { uploadFile } from './data/utils'; import { init } from './startup'; // load environment variables @@ -27,6 +28,20 @@ app.use(bodyParser.json()); app.use(cors()); +// file upload +app.use('/upload-file', async (req, res) => { + let data = ''; + + req.on('data', chunk => { + data += chunk; + }); + + req.on('end', async () => { + const url = await uploadFile({ data }); + res.end(url); + }); +}); + app.use( '/graphql', userMiddleware, diff --git a/yarn.lock b/yarn.lock index 8e622acb1..78bc19064 100644 --- a/yarn.lock +++ b/yarn.lock @@ -269,6 +269,21 @@ asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" +aws-sdk@^2.151.0: + version "2.151.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.151.0.tgz#5e73d778b76514f36183b91400477778ab5e58d8" + dependencies: + buffer "4.9.1" + crypto-browserify "1.0.9" + events "^1.1.1" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.1.0" + xml2js "0.4.17" + xmlbuilder "4.2.1" + aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" @@ -813,6 +828,10 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" +base64-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" + base64url@2.0.0, base64url@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" @@ -962,6 +981,14 @@ buffer-shims@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" +buffer@4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1267,6 +1294,10 @@ cryptiles@3.x.x: dependencies: boom "5.x.x" +crypto-browserify@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-1.0.9.tgz#cc5449685dfb85eb11c9828acc7cb87ab5bbfcc0" + crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" @@ -1675,6 +1706,10 @@ eventemitter3@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" +events@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + exec-sh@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38" @@ -2304,6 +2339,10 @@ iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + ignore-by-default@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" @@ -2891,6 +2930,10 @@ jest@^21.2.1: dependencies: jest-cli "^21.2.1" +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" @@ -3991,6 +4034,10 @@ pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -4019,7 +4066,7 @@ qs@~6.2.0: version "6.2.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.3.tgz#1cfcb25c10a9b2b483053ff39f5dfc9233908cfe" -querystring@^0.2.0: +querystring@0.2.0, querystring@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" @@ -4445,6 +4492,10 @@ sane@^2.0.0: optionalDependencies: fsevents "^1.1.1" +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + sax@>=0.6.0, sax@^1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -5013,6 +5064,13 @@ url-parse-lax@^1.0.0: dependencies: prepend-http "^1.0.1" +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + user-home@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" @@ -5031,7 +5089,7 @@ utils-merge@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" -uuid@^3.0.1, uuid@^3.1.0: +uuid@3.1.0, uuid@^3.0.1, uuid@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" @@ -5184,6 +5242,13 @@ xml-name-validator@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" +xml2js@0.4.17: + version "0.4.17" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.17.tgz#17be93eaae3f3b779359c795b419705a8817e868" + dependencies: + sax ">=0.6.0" + xmlbuilder "^4.1.0" + xml2js@^0.4.15: version "0.4.19" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" @@ -5191,6 +5256,12 @@ xml2js@^0.4.15: sax ">=0.6.0" xmlbuilder "~9.0.1" +xmlbuilder@4.2.1, xmlbuilder@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.2.1.tgz#aa58a3041a066f90eaa16c2f5389ff19f3f461a5" + dependencies: + lodash "^4.0.0" + xmlbuilder@~9.0.1: version "9.0.4" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f" From 168f5e6e31d0fff65726ed487b1b79b41a07c0f8 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 16 Nov 2017 17:06:51 +0800 Subject: [PATCH 288/318] Add updatedAt, closedAt, closedUserId fields on Conversation model --- src/__tests__/conversationDb.test.js | 35 ++++++++++++++++--- src/__tests__/conversationMutations.test.js | 6 +++- src/data/resolvers/mutations/conversations.js | 2 +- src/db/models/ConversationMessages.js | 14 ++++++-- src/db/models/Conversations.js | 34 ++++++++++++++++-- 5 files changed, 80 insertions(+), 11 deletions(-) diff --git a/src/__tests__/conversationDb.test.js b/src/__tests__/conversationDb.test.js index dcb6338c4..4c9565d43 100644 --- a/src/__tests__/conversationDb.test.js +++ b/src/__tests__/conversationDb.test.js @@ -45,6 +45,8 @@ describe('Conversation db', () => { expect(conversation).toBeDefined(); expect(conversation.content).toBe(_conversation.content); + expect(conversation.createdAt).toEqual(expect.any(Date)); + expect(conversation.updatedAt).toEqual(expect.any(Date)); expect(conversation.status).toBe(CONVERSATION_STATUSES.NEW); expect(conversation.number).toBe(_number); expect(conversation.messageCount).toBe(0); @@ -64,14 +66,24 @@ describe('Conversation db', () => { expect(e.message).toEqual('Conversation not found.'); } }); + test('Create conversation message', async () => { - expect.assertions(17); + expect.assertions(18); + + // setting updatedAt to null to check when new message updatedAt field + // must be setted + _conversation.updatedAt = null; + await _conversation.save(); // get messageCount before add message const prevConversationObj = await Conversations.findOne({ _id: _doc.conversationId }); const messageObj = await ConversationMessages.addMessage(_doc, _user); + // checking updated conversation + const updatedConversation = await Conversations.findOne({ _id: _doc.conversationId }); + expect(updatedConversation.updatedAt).toEqual(expect.any(Date)); + expect(messageObj.content).toBe(_conversationMessage.content); expect(messageObj.attachments).toBe(_conversationMessage.attachments); expect(messageObj.status).toBe(_conversationMessage.status); @@ -146,11 +158,22 @@ describe('Conversation db', () => { }); test('Change conversation status', async () => { - await Conversations.changeStatusConversation([_conversation._id], 'new'); + // try closing ======================== + await Conversations.changeStatusConversation([_conversation._id], 'closed'); - const conversationObj = await Conversations.findOne({ _id: _conversation._id }); + let conversationObj = await Conversations.findOne({ _id: _conversation._id }); + + expect(conversationObj.closedAt).toEqual(expect.any(Date)); + expect(conversationObj.status).toBe('closed'); + + // try reopening ======================== + await Conversations.changeStatusConversation([_conversation._id], 'open'); + + conversationObj = await Conversations.findOne({ _id: _conversation._id }); - expect(conversationObj.status).toBe('new'); + expect(conversationObj.closedAt).toBeNull(); + expect(conversationObj.closedUserId).toBeNull(); + expect(conversationObj.status).toBe('open'); }); test('Conversation star', async () => { @@ -263,6 +286,8 @@ describe('Conversation db', () => { test('Reopen', async () => { const conversation = await conversationFactory({ status: 'closed', + closedAt: new Date(), + closedUserId: 'DFAFSAFDSFSFSAFD', readUserIds: ['DFJAKSFJDKFJSDF'], }); @@ -270,5 +295,7 @@ describe('Conversation db', () => { expect(updatedConversation.status).toBe('open'); expect(updatedConversation.readUserIds.length).toBe(0); + expect(updatedConversation.closedAt).toBeNull(); + expect(updatedConversation.closedUserId).toBeNull(); }); }); diff --git a/src/__tests__/conversationMutations.test.js b/src/__tests__/conversationMutations.test.js index a28714e38..e23f119ed 100644 --- a/src/__tests__/conversationMutations.test.js +++ b/src/__tests__/conversationMutations.test.js @@ -323,7 +323,11 @@ describe('Conversation message mutations', () => { ); expect(Conversations.changeStatusConversation.mock.calls.length).toBe(1); - expect(Conversations.changeStatusConversation).toBeCalledWith([_conversation._id], status); + expect(Conversations.changeStatusConversation).toBeCalledWith( + [_conversation._id], + status, + _user._id, + ); }); test('Conversation star', async () => { diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 895aa1d3b..5ceabf86d 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -232,7 +232,7 @@ const conversationMutations = { async conversationsChangeStatus(root, { _ids, status }, { user }) { const { conversations } = await Conversations.checkExistanceConversations(_ids); - await Conversations.changeStatusConversation(_ids, status); + await Conversations.changeStatusConversation(_ids, status, user._id); // notify graphl subscription await conversationsChanged(_ids, 'statusChanged'); diff --git a/src/db/models/ConversationMessages.js b/src/db/models/ConversationMessages.js index d557d91d1..7d099f107 100644 --- a/src/db/models/ConversationMessages.js +++ b/src/db/models/ConversationMessages.js @@ -118,8 +118,18 @@ class Message { // if there is no attachments and no content then throw content required error if (attachments.length === 0 && !strip(content)) throw new Error('Content is required'); - // setting conversation's content to last message - await this.update({ _id: doc.conversationId }, { $set: { content } }); + await Conversations.update( + { _id: doc.conversationId }, + { + $set: { + // setting conversation's content to last message + content, + + // updating updatedAt + updatedAt: new Date(), + }, + }, + ); return this.createMessage({ ...doc, userId }); } diff --git a/src/db/models/Conversations.js b/src/db/models/Conversations.js index cd4de55b4..dc91176ac 100644 --- a/src/db/models/Conversations.js +++ b/src/db/models/Conversations.js @@ -84,6 +84,18 @@ const ConversationSchema = mongoose.Schema({ participatedUserIds: field({ type: [String] }), readUserIds: field({ type: [String] }), createdAt: field({ type: Date }), + updatedAt: field({ type: Date }), + + closedAt: field({ + type: Date, + optional: true, + }), + + closedUserId: field({ + type: String, + optional: true, + }), + status: field({ type: String, enum: CONVERSATION_STATUSES.ALL, @@ -120,10 +132,13 @@ class Conversation { * @return {Promise} Newly created conversation object */ static async createConversation(doc) { + const now = new Date(); + return this.create({ status: CONVERSATION_STATUSES.NEW, ...doc, - createdAt: new Date(), + createdAt: now, + updatedAt: now, number: (await this.find().count()) + 1, messageCount: 0, }); @@ -144,6 +159,9 @@ class Conversation { // if closed, reopen status: CONVERSATION_STATUSES.OPEN, + + closedAt: null, + closedUserId: null, }, }, ); @@ -196,8 +214,18 @@ class Conversation { * @param {String} status * @return {Promise} Updated conversation id */ - static changeStatusConversation(conversationIds, status) { - return this.update({ _id: { $in: conversationIds } }, { $set: { status } }, { multi: true }); + static changeStatusConversation(conversationIds, status, userId) { + const query = { status }; + + if (status === CONVERSATION_STATUSES.CLOSED) { + query.closedAt = new Date(); + query.closedUserId = userId; + } else { + query.closedAt = null; + query.closedUserId = null; + } + + return this.update({ _id: { $in: conversationIds } }, { $set: query }, { multi: true }); } /** From 6de6d5a1910086569692923f8bbb47a1f5387d69 Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 16 Nov 2017 17:59:04 +0800 Subject: [PATCH 289/318] Use updatedAt for sorts --- src/data/resolvers/queries/conversations.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/resolvers/queries/conversations.js b/src/data/resolvers/queries/conversations.js index cbe42d871..9ef477c08 100644 --- a/src/data/resolvers/queries/conversations.js +++ b/src/data/resolvers/queries/conversations.js @@ -21,7 +21,7 @@ const conversationQueries = { await qb.buildAllQueries(); return Conversations.find(qb.mainQuery()) - .sort({ createdAt: -1 }) + .sort({ updatedAt: -1 }) .limit(params.limit); }, @@ -177,7 +177,7 @@ const conversationQueries = { await qb.buildAllQueries(); - return Conversations.findOne(qb.mainQuery()).sort({ createdAt: -1 }); + return Conversations.findOne(qb.mainQuery()).sort({ updatedAt: -1 }); }, }; From 3d4edce0b254d397f9d8026dc05ffd056e776f5e Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 16 Nov 2017 19:18:54 +0800 Subject: [PATCH 290/318] Add firstName, lastName fields on customer --- src/__tests__/customerDb.test.js | 9 ++++++++- src/data/schema/customer.js | 5 ++++- src/db/models/Customers.js | 4 +++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/__tests__/customerDb.test.js b/src/__tests__/customerDb.test.js index e6e67634c..a70e0baa1 100644 --- a/src/__tests__/customerDb.test.js +++ b/src/__tests__/customerDb.test.js @@ -31,11 +31,18 @@ describe('Customers model tests', () => { expect(e.message).toBe('Duplicated email'); } - const doc = { name: 'name', email: 'dombo@yahoo.com' }; + const doc = { + name: 'name', + email: 'dombo@yahoo.com', + firstName: 'firstName', + lastName: 'lastName', + }; const customerObj = await Customers.createCustomer(doc); expect(customerObj.name).toBe(doc.name); + expect(customerObj.firstName).toBe(doc.fistName); + expect(customerObj.lastName).toBe(doc.lastName); expect(customerObj.email).toBe(doc.email); }); diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index 4fa6e59c7..ba6d98c85 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -3,6 +3,8 @@ export const types = ` _id: String! integrationId: String name: String + firstName: String + lastName: String email: String phone: String isUser: Boolean @@ -39,7 +41,8 @@ export const queries = ` `; const fields = ` - name: String + firstName: String + lastName: String email: String phone: String customFieldsData: JSON diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index 6412418cf..fcce17c86 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -77,7 +77,9 @@ const facebookSchema = mongoose.Schema( const CustomerSchema = mongoose.Schema({ _id: field({ pkey: true }), - name: field({ type: String, label: 'Name' }), + name: field({ type: String }), + firstName: field({ type: String, label: 'First name' }), + lastName: field({ type: String, label: 'Last name' }), email: field({ type: String, label: 'Email' }), phone: field({ type: String, label: 'Phone' }), isUser: field({ type: Boolean, label: 'Is user' }), From 36c1ec285daec611ef36dc08ea0d83ce3399c765 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Thu, 16 Nov 2017 20:32:02 +0800 Subject: [PATCH 291/318] Add insight close report query --- src/__tests__/insightQueries.test.js | 3 +- src/data/resolvers/queries/insightUtils.js | 48 ++++++++++ src/data/resolvers/queries/insights.js | 105 +++++++++++++++++---- src/data/schema/insight.js | 17 ++-- 4 files changed, 147 insertions(+), 26 deletions(-) diff --git a/src/__tests__/insightQueries.test.js b/src/__tests__/insightQueries.test.js index 21bbb80c0..8481a4eef 100644 --- a/src/__tests__/insightQueries.test.js +++ b/src/__tests__/insightQueries.test.js @@ -4,7 +4,7 @@ import insightQueries from '../data/resolvers/queries/insights'; describe('insightQueries', () => { test(`test if Error('Login required') exception is working as intended`, async () => { - expect.assertions(4); + expect.assertions(5); const expectError = async func => { try { @@ -18,5 +18,6 @@ describe('insightQueries', () => { expectError(insightQueries.insightsPunchCard); expectError(insightQueries.insightsMain); expectError(insightQueries.insightsFirstResponse); + expectError(insightQueries.insightsResponseClose); }); }); diff --git a/src/data/resolvers/queries/insightUtils.js b/src/data/resolvers/queries/insightUtils.js index 4f22971fe..75be41605 100644 --- a/src/data/resolvers/queries/insightUtils.js +++ b/src/data/resolvers/queries/insightUtils.js @@ -1,5 +1,6 @@ import { Users, Integrations, Conversations } from '../../../db/models'; import moment from 'moment'; +import _ from 'underscore'; /** * Builds messages find query selector. @@ -216,3 +217,50 @@ export const generateUserSelector = type => { return volumeOrResponse; }; + +/** + * Generate response chart data. + * @param {Object} args + * @param {[Object]} args.responsData + * @param {[Object]} args.responseUserData + * @param {Integer} args.allResponseTime + * @param {Integer} args.duration + * @param {Integer} args.starTime + * @return {Object} Data { trend: [Object], teamMembers: [Object], time: Integer } + */ +export const generateResponseData = async ( + responsData, + responseUserData, + allResponseTime, + duration, + startTime, +) => { + // preparing trend chart data + const trend = generateChartData(responsData, 10, duration, startTime); + + // Average response time for all messages + const time = parseInt(allResponseTime / responsData.length); + + const teamMembers = []; + + const userIds = _.uniq(_.pluck(responsData, 'userId')); + + for (let userId of userIds) { + // Average response time for users. + let time = responseUserData[userId].responseTime / responseUserData[userId].count; + + // preparing each team member's chart data + teamMembers.push({ + data: await generateUserChartData({ + userId, + userMessages: responsData.filter(message => userId === message.userId), + duration, + startTime, + }), + + time: parseInt(time), + }); + } + + return { trend, time, teamMembers }; +}; diff --git a/src/data/resolvers/queries/insights.js b/src/data/resolvers/queries/insights.js index d4eec7142..0a47a0a17 100644 --- a/src/data/resolvers/queries/insights.js +++ b/src/data/resolvers/queries/insights.js @@ -11,6 +11,7 @@ import { generateDuration, generateChartData, generateUserSelector, + generateResponseData, getTime, formatTime, } from './insightUtils'; @@ -189,7 +190,7 @@ const insightQueries = { * @param {String} args.brandId * @param {String} args.startDate * @param {String} args.endDate - * @return {Promise} Object data { trend: [Object], teamMembers: [Object], summary: [] } + * @return {Promise} Object data { trend: [Object], teamMembers: [Object], time: Integer } */ async insightsFirstResponse(root, { integrationType, brandId, startDate, endDate }) { const { start, end } = fixDates(startDate, endDate); @@ -270,32 +271,98 @@ const insightQueries = { } } - // preparing trend chart data - insightData.trend = generateChartData(firstResponseData, 10, duration, startTime); + return generateResponseData( + firstResponseData, + responseUserData, + allResponseTime, + duration, + startTime, + ); + }, - // Average response time for all messages - insightData.time = parseInt(allResponseTime / firstResponseData.length); + /** + * Calculates average response close time for each team members. + * @param {Object} args + * @param {String} args.brandId + * @param {String} args.startDate + * @param {String} args.endDate + * @return {Promise} Object data { trend: [Object], teamMembers: [Object], time: Integer } + */ + async insightsResponseClose(root, { integrationType, brandId, startDate, endDate }) { + const { start, end } = fixDates(startDate, endDate); + const { duration, startTime } = generateDuration({ start, end }); - const userIds = _.uniq(_.pluck(firstResponseData, 'userId')); + const conversationSelector = { + createdAt: { $gte: start, $lte: end }, + closedAt: { $ne: null }, + closedUserId: { $ne: null }, + }; - for (let userId of userIds) { - // Average response time for users. - let time = responseUserData[userId].responseTime / responseUserData[userId].count; + const integrationSelector = {}; - // preparing each team member's chart data - insightData.teamMembers.push({ - data: await generateUserChartData({ - userId, - userMessages: firstResponseData.filter(message => userId === message.userId), - duration, - startTime, - }), + if (brandId) { + integrationSelector.brandId = brandId; + } + + if (integrationType) { + integrationSelector.kind = integrationType; + } + + const integrationIds = await Integrations.find(integrationSelector).select('_id'); + const conversations = await Conversations.find({ + ...conversationSelector, + integrationId: { $in: integrationIds }, + }); + + const insightData = { teamMembers: [], trend: [] }; + + // If conversation not found. + if (conversations.length < 1) { + return insightData; + } + + // Variable that holds all responded conversation messages + const ResponseCloseData = []; + + // Variables holds every user's response time. + const responseUserData = {}; + + let allResponseTime = 0; - time: parseInt(time), + // Processes total first response time for each users. + for (let conversation of conversations) { + let responseTime = getTime(conversation.closedAt) - getTime(conversation.createdAt); + responseTime = parseInt(responseTime / 1000); + + const userId = conversation.closedUserId; + + // collecting each user's respond information + ResponseCloseData.push({ + createdAt: conversation.createdAt, + userId, + responseTime, }); + + allResponseTime += responseTime; + + let count = 1; + + // Builds every users's response time and conversation message count. + if (responseUserData[userId]) { + responseTime = responseTime + responseUserData[userId].responseTime; + count = responseUserData[userId].count + 1; + } + + responseUserData[userId] = { responseTime, count }; } - return insightData; + return generateResponseData( + ResponseCloseData, + responseUserData, + allResponseTime, + duration, + startTime, + ); }, }; diff --git a/src/data/schema/insight.js b/src/data/schema/insight.js index 8048b65b9..36f92b594 100644 --- a/src/data/schema/insight.js +++ b/src/data/schema/insight.js @@ -5,12 +5,17 @@ export const types = ` } `; +const params = ` + integrationType: String, + brandId: String, + startDate: String, + endDate: String +`; + export const queries = ` insights(brandId: String, startDate: String, endDate: String): [InsightData] - insightsPunchCard(type: String, integrationType: String, - brandId: String, endDate: String): JSON - insightsMain(type: String, integrationType: String, - brandId: String, startDate: String, endDate: String): JSON - insightsFirstResponse(integrationType: String, brandId: String, - startDate: String, endDate: String): JSON + insightsPunchCard(type: String, integrationType: String, brandId: String, endDate: String): JSON + insightsMain(type: String, ${params}): JSON + insightsFirstResponse(${params}): JSON + insightsResponseClose(${params}): JSON `; From f17683b94c908362a321217f50ed52b884bd31bc Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 17 Nov 2017 00:53:56 +0800 Subject: [PATCH 292/318] Close #25 --- package.json | 1 + src/data/schema/conversation.js | 4 ++-- src/data/utils.js | 19 ++++++------------- src/index.js | 14 ++++++-------- yarn.lock | 4 ++++ 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 9d307fc35..33c1bc4e3 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "dotenv": "^4.0.0", "express": "^4.15.2", "fbgraph": "^1.4.1", + "formidable": "^1.1.1", "graphql": "^0.10.1", "graphql-server-core": "^0.8.2", "graphql-server-express": "^0.8.2", diff --git a/src/data/schema/conversation.js b/src/data/schema/conversation.js index 8f1da8e2b..5d0e38752 100644 --- a/src/data/schema/conversation.js +++ b/src/data/schema/conversation.js @@ -29,7 +29,7 @@ export const types = ` type ConversationMessage { _id: String! content: String - attachments: JSON + attachments: [JSON] mentionedUserIds: [String] conversationId: String internal: Boolean @@ -94,7 +94,7 @@ export const mutations = ` content: String, mentionedUserIds: [String], internal: Boolean, - attachments: [String], + attachments: [JSON], ): ConversationMessage conversationsAssign(conversationIds: [String]!, assignedUserId: String): [Conversation] diff --git a/src/data/utils.js b/src/data/utils.js index 45eec3db8..351d6e57d 100644 --- a/src/data/utils.js +++ b/src/data/utils.js @@ -10,7 +10,7 @@ import { Notifications, Users } from '../db/models'; * @param {Object} data - File binary data * @return {String} - Uploaded file url */ -export const uploadFile = async ({ name = 'test.jpeg', data }) => { +export const uploadFile = async file => { const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_BUCKET, AWS_PREFIX = '' } = process.env; // check credentials @@ -25,12 +25,12 @@ export const uploadFile = async ({ name = 'test.jpeg', data }) => { }); // generate unique name - const fileName = `${AWS_PREFIX}${Math.random()}${name}`; + const fileName = `${AWS_PREFIX}${Math.random()}${file.name}`; - // create buffer from file data - const buffer = new Buffer(data); + // read file + const buffer = await fs.readFileSync(file.path); - // call putObject + // upload to s3 await new Promise((resolve, reject) => { s3.putObject( { @@ -60,14 +60,7 @@ export const uploadFile = async ({ name = 'test.jpeg', data }) => { export const readFile = filename => { const filePath = `${__dirname}/../private/emailTemplates/${filename}.html`; - return new Promise((resolve, reject) => { - fs.readFile(filePath, 'utf8', (err, data) => { - if (err) { - reject(err); - } - resolve(data); - }); - }); + return fs.readFileSync(filePath, 'utf8'); }; /** diff --git a/src/index.js b/src/index.js index e0bc4331b..bc212d744 100755 --- a/src/index.js +++ b/src/index.js @@ -8,6 +8,7 @@ import { createServer } from 'http'; import { execute, subscribe } from 'graphql'; import { graphqlExpress, graphiqlExpress } from 'graphql-server-express'; import { SubscriptionServer } from 'subscriptions-transport-ws'; +import formidable from 'formidable'; import { Customers } from './db/models'; import { connect } from './db/connection'; import { userMiddleware } from './auth'; @@ -29,16 +30,13 @@ app.use(bodyParser.json()); app.use(cors()); // file upload -app.use('/upload-file', async (req, res) => { - let data = ''; +app.post('/upload-file', async (req, res) => { + const form = new formidable.IncomingForm(); - req.on('data', chunk => { - data += chunk; - }); + form.parse(req, async (err, fields, response) => { + const url = await uploadFile(response.file); - req.on('end', async () => { - const url = await uploadFile({ data }); - res.end(url); + return res.end(url); }); }); diff --git a/yarn.lock b/yarn.lock index 78bc19064..5f914f9ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1946,6 +1946,10 @@ formatio@1.2.0, formatio@^1.2.0: dependencies: samsam "1.x" +formidable@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9" + forwarded@~0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" From 71511ea08085dc9f3ce64a48aea5f5ee413a0a85 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 17 Nov 2017 00:58:10 +0800 Subject: [PATCH 293/318] Little test fix --- src/__tests__/customerDb.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/customerDb.test.js b/src/__tests__/customerDb.test.js index a70e0baa1..d8f48eec2 100644 --- a/src/__tests__/customerDb.test.js +++ b/src/__tests__/customerDb.test.js @@ -22,7 +22,7 @@ describe('Customers model tests', () => { }); test('Create customer', async () => { - expect.assertions(3); + expect.assertions(5); // check duplication try { @@ -41,7 +41,7 @@ describe('Customers model tests', () => { const customerObj = await Customers.createCustomer(doc); expect(customerObj.name).toBe(doc.name); - expect(customerObj.firstName).toBe(doc.fistName); + expect(customerObj.firstName).toBe(doc.firstName); expect(customerObj.lastName).toBe(doc.lastName); expect(customerObj.email).toBe(doc.email); }); From 98168fe359e501ae3c2916824c7a6d3de8b5ebb0 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 18 Nov 2017 15:25:36 +0800 Subject: [PATCH 294/318] Change conversation detail notification link --- src/__tests__/conversationMutations.test.js | 4 ++-- src/data/resolvers/mutations/conversations.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/__tests__/conversationMutations.test.js b/src/__tests__/conversationMutations.test.js index e23f119ed..1cc8c8ee5 100644 --- a/src/__tests__/conversationMutations.test.js +++ b/src/__tests__/conversationMutations.test.js @@ -128,7 +128,7 @@ describe('Conversation message mutations', () => { notifType: 'conversationAddMessage', title: 'You have a new message.', content: _doc.content, - link: `/inbox/details/${_conversation._id}`, + link: `/inbox?_id=${_conversation._id}`, receivers: [], }); @@ -286,7 +286,7 @@ describe('Conversation message mutations', () => { notifType: 'conversationAssigneeChange', title: content, content, - link: `/inbox/details/${_conversation._id}`, + link: `/inbox?_id=${_conversation._id}`, receivers: [], }); }); diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 5ceabf86d..bfd2c4f66 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -123,7 +123,7 @@ const conversationMutations = { notifType: NOTIFICATION_TYPES.CONVERSATION_ADD_MESSAGE, title, content: doc.content, - link: `/inbox/details/${conversation._id}`, + link: `/inbox?_id=${conversation._id}`, receivers: conversationNotifReceivers(conversation, user._id), }); @@ -201,7 +201,7 @@ const conversationMutations = { notifType: NOTIFICATION_TYPES.CONVERSATION_ASSIGNEE_CHANGE, title: content, content, - link: `/inbox/details/${conversation._id}`, + link: `/inbox?_id=${conversation._id}`, receivers: conversationNotifReceivers(conversation, user._id), }); } @@ -270,7 +270,7 @@ const conversationMutations = { notifType: NOTIFICATION_TYPES.CONVERSATION_STATE_CHANGE, title: content, content, - link: `/inbox/details/${conversation._id}`, + link: `/inbox?_id=${conversation._id}`, receivers: conversationNotifReceivers(conversation, user._id), }); } From 0fa1a14ff6f93e099b7c6a086ce67198e5c6f98b Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 18 Nov 2017 16:07:05 +0800 Subject: [PATCH 295/318] Close #265, Close #264 --- src/__tests__/userDb.test.js | 23 +++++++++++++++++++++-- src/db/models/Users.js | 16 +++++++++------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index 4a7eda693..bd8b1bc4b 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -25,16 +25,35 @@ describe('User db utils', () => { }); test('Create user: twitter handler duplication', async () => { - expect.assertions(1); + expect.assertions(3); + + _user.details.twitterUsername = undefined; + await _user.save(); + + // don not check duplication when there is not twitter username specified + let createdUser = await Users.createUser({ password: 'password', details: {} }); + expect(createdUser.details.twitterUsername).toBe(undefined); + + // duplicated ================== + _user.details.twitterUsername = 'twitter'; + await _user.save(); try { await Users.createUser({ password: 'password', - details: { twitterUsername: _user.details.twitterUsername }, + details: { twitterUsername: 'twitter' }, }); } catch (e) { expect(e.message).toBe('Duplicated twitter username'); } + + // not duplicated ========== + createdUser = await Users.createUser({ + password: 'password', + details: { twitterUsername: 'other twitter' }, + }); + + expect(createdUser.details.twitterUsername).toBe('other twitter'); }); test('Create user', async () => { diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 474a09ef7..eee14f66f 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -147,13 +147,15 @@ class User { * Check duplications */ static async checkDuplications({ userId, twitterUsername }) { - const previousEntry = await Users.findOne({ - _id: { $ne: userId }, - 'details.twitterUsername': twitterUsername, - }); - - if (previousEntry) { - throw new Error('Duplicated twitter username'); + if (twitterUsername) { + const previousEntry = await Users.findOne({ + _id: { $ne: userId }, + 'details.twitterUsername': twitterUsername, + }); + + if (previousEntry) { + throw new Error('Duplicated twitter username'); + } } } From e6416ba47f17fcd85826e027b43aca7d7e96d4e4 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 18 Nov 2017 16:07:05 +0800 Subject: [PATCH 296/318] Close #33, Close #34 --- src/__tests__/userDb.test.js | 23 +++++++++++++++++++++-- src/db/models/Users.js | 16 +++++++++------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index 4a7eda693..bd8b1bc4b 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -25,16 +25,35 @@ describe('User db utils', () => { }); test('Create user: twitter handler duplication', async () => { - expect.assertions(1); + expect.assertions(3); + + _user.details.twitterUsername = undefined; + await _user.save(); + + // don not check duplication when there is not twitter username specified + let createdUser = await Users.createUser({ password: 'password', details: {} }); + expect(createdUser.details.twitterUsername).toBe(undefined); + + // duplicated ================== + _user.details.twitterUsername = 'twitter'; + await _user.save(); try { await Users.createUser({ password: 'password', - details: { twitterUsername: _user.details.twitterUsername }, + details: { twitterUsername: 'twitter' }, }); } catch (e) { expect(e.message).toBe('Duplicated twitter username'); } + + // not duplicated ========== + createdUser = await Users.createUser({ + password: 'password', + details: { twitterUsername: 'other twitter' }, + }); + + expect(createdUser.details.twitterUsername).toBe('other twitter'); }); test('Create user', async () => { diff --git a/src/db/models/Users.js b/src/db/models/Users.js index 474a09ef7..eee14f66f 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -147,13 +147,15 @@ class User { * Check duplications */ static async checkDuplications({ userId, twitterUsername }) { - const previousEntry = await Users.findOne({ - _id: { $ne: userId }, - 'details.twitterUsername': twitterUsername, - }); - - if (previousEntry) { - throw new Error('Duplicated twitter username'); + if (twitterUsername) { + const previousEntry = await Users.findOne({ + _id: { $ne: userId }, + 'details.twitterUsername': twitterUsername, + }); + + if (previousEntry) { + throw new Error('Duplicated twitter username'); + } } } From 99d93dc27cae612254e90d3fa62a08e34c4615e7 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 18 Nov 2017 17:22:24 +0800 Subject: [PATCH 297/318] Close #35 --- src/__tests__/userDb.test.js | 4 ++-- src/db/models/Users.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index bd8b1bc4b..4593aef1d 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -16,7 +16,7 @@ describe('User db utils', () => { beforeEach(async () => { // Creating test data - _user = await userFactory({ email: 'info@erxes.io' }); + _user = await userFactory({ email: 'Info@erxes.io' }); }); afterEach(async () => { @@ -245,7 +245,7 @@ describe('User db utils', () => { // valid const { token, refreshToken } = await Users.login({ - email: _user.email, + email: _user.email.toUpperCase(), password: 'pass', }); diff --git a/src/db/models/Users.js b/src/db/models/Users.js index eee14f66f..d3c05ecce 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -335,7 +335,7 @@ class User { * @return {Object} - generated tokens */ static async login({ email, password }) { - const user = await Users.findOne({ email }); + const user = await Users.findOne({ email: { $regex: new RegExp(email, 'i') } }); if (!user) { // user with provided email not found From a05c38615a66381e1822be2bb814a3d1a23fd8c5 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 18 Nov 2017 17:32:48 +0800 Subject: [PATCH 298/318] Check email value before duplication check in customer --- src/db/models/Customers.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index fcce17c86..4d54016d5 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -102,11 +102,13 @@ class Customer { * @return {Promise} Newly created customer object */ static async createCustomer(doc) { - const previousEntry = await this.findOne({ email: doc.email }); + if (doc.email) { + const previousEntry = await this.findOne({ email: doc.email }); - // check duplication - if (previousEntry) { - throw new Error('Duplicated email'); + // check duplication + if (previousEntry) { + throw new Error('Duplicated email'); + } } // clean custom field values @@ -122,14 +124,16 @@ class Customer { * @return {Promise} updated customer object */ static async updateCustomer(_id, doc) { - const previousEntry = await this.findOne({ - _id: { $ne: _id }, - email: doc.email, - }); - - // check duplication - if (previousEntry) { - throw new Error('Duplicated email'); + if (doc.email) { + const previousEntry = await this.findOne({ + _id: { $ne: _id }, + email: doc.email, + }); + + // check duplication + if (previousEntry) { + throw new Error('Duplicated email'); + } } // clean custom field values From e0764baffc04a8031ee9fcb53c80b1b81bb97a12 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 20 Nov 2017 10:57:33 +0800 Subject: [PATCH 299/318] Close #36 --- src/__tests__/userDb.test.js | 16 ++++++++++++++-- src/db/models/Users.js | 4 ++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/__tests__/userDb.test.js b/src/__tests__/userDb.test.js index 4593aef1d..9cb7bb102 100644 --- a/src/__tests__/userDb.test.js +++ b/src/__tests__/userDb.test.js @@ -82,7 +82,7 @@ describe('User db utils', () => { const testPassword = 'updatedPass'; - // try using exisiting one + // try with password ============ await Users.updateUser(_user._id, { email: updateDoc.email, username: updateDoc.username, @@ -90,7 +90,7 @@ describe('User db utils', () => { details: { ...updateDoc._doc.details.toJSON(), twitterUsername: 'tw' }, }); - const userObj = await Users.findOne({ _id: _user._id }); + let userObj = await Users.findOne({ _id: _user._id }); expect(userObj.username).toBe(updateDoc.username); expect(userObj.email).toBe(updateDoc.email); @@ -100,6 +100,18 @@ describe('User db utils', () => { expect(userObj.details.twitterUsername).toBe('tw'); expect(userObj.details.fullName).toBe(updateDoc.details.fullName); expect(userObj.details.avatar).toBe(updateDoc.details.avatar); + + // try without password ============ + await Users.updateUser(_user._id, { + email: updateDoc.email, + username: updateDoc.username, + details: { ...updateDoc._doc.details.toJSON(), twitterUsername: 'tw' }, + }); + + userObj = await Users.findOne({ _id: _user._id }); + + // password must stay untouched + expect(bcrypt.compare(testPassword, userObj.password)).toBeTruthy(); }); test('Remove user', async () => { diff --git a/src/db/models/Users.js b/src/db/models/Users.js index d3c05ecce..a35a600f0 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -91,6 +91,10 @@ class User { // change password if (password) { doc.password = await this.generatePassword(password); + + // if there is no password specified then leave password field alone + } else { + delete doc.password; } await this.update({ _id }, { $set: doc }); From e206fb88b3af641c4ef3143d4a7441d7dda7f2f0 Mon Sep 17 00:00:00 2001 From: Mungunshagai Date: Mon, 20 Nov 2017 14:50:14 +0800 Subject: [PATCH 300/318] Mark all read notifications --- src/__tests__/notificationMutations.test.js | 2 +- src/data/resolvers/mutations/notifications.js | 4 ++-- src/data/schema/notification.js | 2 +- src/db/models/Notifications.js | 11 +++++++++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/__tests__/notificationMutations.test.js b/src/__tests__/notificationMutations.test.js index f5264c3bf..864f0fb03 100644 --- a/src/__tests__/notificationMutations.test.js +++ b/src/__tests__/notificationMutations.test.js @@ -56,7 +56,7 @@ describe('testing mutations', () => { await notificationMutations.notificationsMarkAsRead(null, args, { user: _user }); - expect(Notifications.markAsRead).toBeCalledWith(args['_ids']); + expect(Notifications.markAsRead).toBeCalledWith(args['_ids'], _user._id); expect(Notifications.markAsRead.mock.calls.length).toBe(1); }); }); diff --git a/src/data/resolvers/mutations/notifications.js b/src/data/resolvers/mutations/notifications.js index daff9d4a4..21a410708 100644 --- a/src/data/resolvers/mutations/notifications.js +++ b/src/data/resolvers/mutations/notifications.js @@ -28,11 +28,11 @@ const notificationMutations = { * @return {Promise} * @throws {Error} throws Error('Login required') if user is not logged in */ - notificationsMarkAsRead(root, { _ids }) { + notificationsMarkAsRead(root, { _ids }, { user }) { // notify subscription pubsub.publish('notificationsChanged'); - return Notifications.markAsRead(_ids); + return Notifications.markAsRead(_ids, user._id); }, }; diff --git a/src/data/schema/notification.js b/src/data/schema/notification.js index 8d28d29ac..7408dc74c 100644 --- a/src/data/schema/notification.js +++ b/src/data/schema/notification.js @@ -36,5 +36,5 @@ export const queries = ` export const mutations = ` notificationsSaveConfig (notifType: String!, isAllowed: Boolean): NotificationConfiguration - notificationsMarkAsRead (_ids: [String]!) : Boolean + notificationsMarkAsRead (_ids: [String]) : Boolean `; diff --git a/src/db/models/Notifications.js b/src/db/models/Notifications.js index 2edf59d27..97cb2b4a7 100644 --- a/src/db/models/Notifications.js +++ b/src/db/models/Notifications.js @@ -28,10 +28,17 @@ class Notification { /** * Marks notifications as read * @param {String[]} ids - Notification ids + * @param {String} userId - Reciever user id * @return {Promise} */ - static markAsRead(ids) { - return this.update({ _id: { $in: ids } }, { $set: { isRead: true } }, { multi: true }); + static markAsRead(ids, userId) { + let selector = { receiver: userId }; + + if (ids) { + selector = { _id: { $in: ids } }; + } + + return this.update(selector, { $set: { isRead: true } }, { multi: true }); } /** From e98e69fffd88663aa7149a7b2fd316d528ea13c0 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 20 Nov 2017 18:11:21 +0800 Subject: [PATCH 301/318] Upgrade subscription-transport-ws version --- package.json | 2 +- src/data/schema/conversation.js | 12 +++++++++++- src/data/schema/user.js | 9 ++++++++- src/db/models/Integrations.js | 5 ++++- yarn.lock | 32 +++++++++++++------------------- 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 33c1bc4e3..f3348ca57 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "sinon": "^4.0.1", "social-oauth-client": "^0.1.6", "strip": "^3.0.0", - "subscriptions-transport-ws": "^0.7.3", + "subscriptions-transport-ws": "0.8.3", "twit": "^2.2.9", "underscore": "^1.8.3", "validator": "^9.0.0" diff --git a/src/data/schema/conversation.js b/src/data/schema/conversation.js index 5d0e38752..1961787d6 100644 --- a/src/data/schema/conversation.js +++ b/src/data/schema/conversation.js @@ -26,6 +26,16 @@ export const types = ` participatorCount: Int } + type EngageData { + messageId: String + brandId: String + content: String + fromUserId: String + fromUser: User + kind: String + sentAs: String + } + type ConversationMessage { _id: String! content: String @@ -37,7 +47,7 @@ export const types = ` userId: String createdAt: Date isCustomerRead: Boolean - engageData: JSON + engageData: EngageData formWidgetData: JSON facebookData: JSON diff --git a/src/data/schema/user.js b/src/data/schema/user.js index 263c4d692..f090ba230 100644 --- a/src/data/schema/user.js +++ b/src/data/schema/user.js @@ -6,6 +6,13 @@ export const types = ` twitterUsername: String } + type UserDetailsType { + avatar: String + fullName: String + position: String + twitterUsername: String + } + input EmailSignature { brandId: String signature: String @@ -16,7 +23,7 @@ export const types = ` username: String email: String role: String - details: JSON + details: UserDetailsType emailSignatures: JSON getNotificationByEmail: Boolean } diff --git a/src/db/models/Integrations.js b/src/db/models/Integrations.js index 957e00dd7..367d0f9d0 100644 --- a/src/db/models/Integrations.js +++ b/src/db/models/Integrations.js @@ -34,7 +34,10 @@ const MessengerDataSchema = mongoose.Schema( type: Boolean, }), onlineHours: field({ type: [MessengerOnlineHoursSchema] }), - timezone: field({ type: String }), + timezone: field({ + type: String, + optional: true, + }), welcomeMessage: field({ type: String }), awayMessage: field({ type: String }), thankYouMessage: field({ type: String }), diff --git a/yarn.lock b/yarn.lock index 5f914f9ed..62d192407 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,12 +34,6 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" -"@types/ws@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-3.0.2.tgz#b538b6a16daee454ac04054991271f3da38772de" - dependencies: - "@types/node" "*" - abab@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -2152,7 +2146,7 @@ graphql-server-module-graphiql@^0.8.2, graphql-server-module-graphiql@^0.8.5: version "0.8.5" resolved "https://registry.yarnpkg.com/graphql-server-module-graphiql/-/graphql-server-module-graphiql-0.8.5.tgz#bf43caa20e7e7f912003a67bb31710c6ebc7ab18" -graphql-subscriptions@^0.4.3: +graphql-subscriptions@^0.4.3, graphql-subscriptions@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.4.4.tgz#39cff32d08dd3c990113864bab77154403727e9b" dependencies: @@ -2160,9 +2154,9 @@ graphql-subscriptions@^0.4.3: es6-promise "^4.0.5" iterall "^1.1.1" -graphql-tag@^2.0.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.4.2.tgz#6a63297d8522d03a2b72d26f1b239aab343840cd" +graphql-tag@^2.4.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.5.0.tgz#b43bfd8b5babcd2c205ad680c03e98b238934e0f" graphql-tools@^1.0.0: version "1.2.2" @@ -4796,19 +4790,19 @@ strip@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip/-/strip-3.0.0.tgz#750fc933152a7d35af0b7420e651789b914cc35e" -subscriptions-transport-ws@^0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.7.3.tgz#15858f03e013e1fc28f8c2d631014ec1548d38f0" +subscriptions-transport-ws@0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.8.3.tgz#d4b22db7f5951bd30e7c6f4dc4774d34ed9f1751" dependencies: - "@types/ws" "^3.0.0" backo2 "^1.0.2" eventemitter3 "^2.0.3" - graphql-subscriptions "^0.4.3" - graphql-tag "^2.0.0" + graphql-subscriptions "^0.4.4" + graphql-tag "^2.4.2" iterall "^1.1.1" lodash.assign "^4.2.0" lodash.isobject "^3.0.2" lodash.isstring "^4.0.1" + symbol-observable "^1.0.4" ws "^3.0.0" supports-color@^2.0.0: @@ -4833,7 +4827,7 @@ supports-color@^4.4.0: dependencies: has-flag "^2.0.0" -symbol-observable@^1.0.1: +symbol-observable@^1.0.1, symbol-observable@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" @@ -5231,8 +5225,8 @@ write@^0.2.1: mkdirp "^0.5.1" ws@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-3.2.0.tgz#d5d3d6b11aff71e73f808f40cc69d52bb6d4a185" + version "3.3.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.1.tgz#d97e34dee06a1190c61ac1e95f43cb60b78cf939" dependencies: async-limiter "~1.0.0" safe-buffer "~5.1.0" From d39e74ef50a18247b8d5f42c9db9eaa682711796 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 20 Nov 2017 19:16:06 +0800 Subject: [PATCH 302/318] Check integration existance in conversation cron --- src/cronJobs/conversations.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cronJobs/conversations.js b/src/cronJobs/conversations.js index e90a55e68..b6c9441e5 100644 --- a/src/cronJobs/conversations.js +++ b/src/cronJobs/conversations.js @@ -22,6 +22,10 @@ export const sendMessageEmail = async () => { const customer = await Customers.findOne({ _id: conversation.customerId }); const integration = await Integrations.findOne({ _id: conversation.integrationId }); + if (!integration) { + return; + } + const brand = await Brands.findOne({ _id: integration.brandId }); if (!customer || !customer.email) { From 4db64a6e0d525fb2bd6fac1f78fcce13b1a61761 Mon Sep 17 00:00:00 2001 From: batamar Date: Wed, 22 Nov 2017 02:41:22 +0800 Subject: [PATCH 303/318] Implement redis pubsub --- package.json | 3 +- src/data/resolvers/customScalars.js | 6 +- .../resolvers/subscriptions/conversations.js | 2 +- src/data/resolvers/subscriptions/index.js | 5 +- src/data/resolvers/subscriptions/pubsub.js | 26 ++++ yarn.lock | 119 +++++++++++++++++- 6 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 src/data/resolvers/subscriptions/pubsub.js diff --git a/package.json b/package.json index f3348ca57..6eabd652a 100644 --- a/package.json +++ b/package.json @@ -50,10 +50,11 @@ "fbgraph": "^1.4.1", "formidable": "^1.1.1", "graphql": "^0.10.1", + "graphql-redis-subscriptions": "^1.3.1", "graphql-server-core": "^0.8.2", "graphql-server-express": "^0.8.2", "graphql-server-module-graphiql": "^0.8.2", - "graphql-subscriptions": "^0.4.3", + "graphql-subscriptions": "^0.4.4", "graphql-tools": "^1.0.0", "handlebars": "^4.0.10", "jsonwebtoken": "^8.1.0", diff --git a/src/data/resolvers/customScalars.js b/src/data/resolvers/customScalars.js index ca7910003..4e56d53ed 100644 --- a/src/data/resolvers/customScalars.js +++ b/src/data/resolvers/customScalars.js @@ -36,7 +36,11 @@ export default { return new Date(value); // value from the client }, serialize(value) { - return value.getTime(); // value sent to the client + if (value.getTime) { + return value.getTime(); // value sent to the client + } + + return new Date(value).getTime(); }, parseLiteral(ast) { if (ast.kind === Kind.INT) { diff --git a/src/data/resolvers/subscriptions/conversations.js b/src/data/resolvers/subscriptions/conversations.js index 173bbe7c1..d50654c45 100644 --- a/src/data/resolvers/subscriptions/conversations.js +++ b/src/data/resolvers/subscriptions/conversations.js @@ -1,5 +1,5 @@ import { withFilter } from 'graphql-subscriptions'; -import { pubsub } from './'; +import pubsub from './pubsub'; export default { /* diff --git a/src/data/resolvers/subscriptions/index.js b/src/data/resolvers/subscriptions/index.js index 8cf2cc880..314483d84 100644 --- a/src/data/resolvers/subscriptions/index.js +++ b/src/data/resolvers/subscriptions/index.js @@ -1,9 +1,8 @@ -import { PubSub } from 'graphql-subscriptions'; - +import pubsub from './pubsub'; import conversations from './conversations'; import notifications from './notifications'; -export const pubsub = new PubSub(); +export { pubsub }; export default { ...conversations, diff --git a/src/data/resolvers/subscriptions/pubsub.js b/src/data/resolvers/subscriptions/pubsub.js new file mode 100644 index 000000000..081abcf5f --- /dev/null +++ b/src/data/resolvers/subscriptions/pubsub.js @@ -0,0 +1,26 @@ +import { RedisPubSub } from 'graphql-redis-subscriptions'; + +const redisConnectionListener = error => { + if (error) { + console.error(error); // eslint-disable-line no-console + } + + console.info('Successfuly connected to redis'); // eslint-disable-line no-console +}; + +// Docs on the different redis options +// https://github.com/NodeRedis/node_redis#options-object-properties +const redisOptions = { + host: 'localhost', + port: 6379, + connect_timeout: 15000, + enable_offline_queue: true, + retry_unfulfilled_commands: true, +}; + +const pubsub = new RedisPubSub({ + connection: redisOptions, + connectionListener: redisConnectionListener, +}); + +export default pubsub; diff --git a/yarn.lock b/yarn.lock index 62d192407..fb89aa6de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -880,7 +880,7 @@ bluebird@2.10.2: version "2.10.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.10.2.tgz#024a5517295308857f14f91f1106fc3b555f446b" -bluebird@^3.1.5: +bluebird@^3.1.5, bluebird@^3.3.4: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -1125,6 +1125,10 @@ cliui@^3.2.0: strip-ansi "^3.0.1" wrap-ansi "^2.0.0" +cluster-key-slot@^1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.0.8.tgz#7654556085a65330932a2e8b5976f8e2d0b3e414" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -1395,6 +1399,10 @@ delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" +denque@^1.1.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.2.2.tgz#e06cf7cf0da8badc88cbdaabf8fc0a70d659f1d4" + depd@1.1.1, depd@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" @@ -1894,6 +1902,10 @@ flat-cache@^1.2.1: graceful-fs "^4.1.2" write "^0.2.1" +flexbuffer@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/flexbuffer/-/flexbuffer-0.0.6.tgz#039fdf23f8823e440c38f3277e6fef1174215b30" + for-each@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4" @@ -2126,6 +2138,15 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" +graphql-redis-subscriptions@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/graphql-redis-subscriptions/-/graphql-redis-subscriptions-1.3.1.tgz#a8790fbb1bd22103713befdcf1f27d5a14306509" + dependencies: + graphql-subscriptions "^0.4.2" + iterall "^1.1.1" + optionalDependencies: + ioredis "^3.1.2" + graphql-server-core@^0.8.2, graphql-server-core@^0.8.5: version "0.8.5" resolved "https://registry.yarnpkg.com/graphql-server-core/-/graphql-server-core-0.8.5.tgz#6d03e9d7d0ceebdbcfbad57c9f8a61eb0d3fb6da" @@ -2146,7 +2167,7 @@ graphql-server-module-graphiql@^0.8.2, graphql-server-module-graphiql@^0.8.5: version "0.8.5" resolved "https://registry.yarnpkg.com/graphql-server-module-graphiql/-/graphql-server-module-graphiql-0.8.5.tgz#bf43caa20e7e7f912003a67bb31710c6ebc7ab18" -graphql-subscriptions@^0.4.3, graphql-subscriptions@^0.4.4: +graphql-subscriptions@^0.4.2, graphql-subscriptions@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.4.4.tgz#39cff32d08dd3c990113864bab77154403727e9b" dependencies: @@ -2422,6 +2443,34 @@ invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" +ioredis@^3.1.2: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-3.2.1.tgz#4c10bcce9659fdb0af923b0e7915208fe023d3f0" + dependencies: + bluebird "^3.3.4" + cluster-key-slot "^1.0.6" + debug "^2.2.0" + denque "^1.1.0" + flexbuffer "0.0.6" + lodash.assign "^4.2.0" + lodash.bind "^4.2.1" + lodash.clone "^4.5.0" + lodash.clonedeep "^4.5.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.foreach "^4.5.0" + lodash.isempty "^4.4.0" + lodash.keys "^4.2.0" + lodash.noop "^3.0.1" + lodash.partial "^4.2.1" + lodash.pick "^4.4.0" + lodash.sample "^4.2.1" + lodash.shuffle "^4.2.0" + lodash.values "^4.3.0" + redis-commands "^1.2.0" + redis-parser "^2.4.0" + ipaddr.js@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0" @@ -3242,10 +3291,22 @@ lodash.assign@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" +lodash.bind@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35" + lodash.chunk@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" +lodash.clone@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + lodash.defaults@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-3.1.2.tgz#c7308b18dbf8bc9372d701a73493c61192bd2e2c" @@ -3253,6 +3314,22 @@ lodash.defaults@^3.1.2: lodash.assign "^3.0.0" lodash.restparam "^3.0.0" +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + +lodash.foreach@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -3273,6 +3350,10 @@ lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" +lodash.isempty@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" + lodash.isinteger@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" @@ -3301,7 +3382,11 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" -lodash.noop@~3.0.0: +lodash.keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-4.2.0.tgz#a08602ac12e4fb83f91fc1fb7a360a4d9ba35205" + +lodash.noop@^3.0.1, lodash.noop@~3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c" @@ -3309,10 +3394,30 @@ lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" +lodash.partial@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.partial/-/lodash.partial-4.2.1.tgz#49f3d8cfdaa3bff8b3a91d127e923245418961d4" + +lodash.pick@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" +lodash.sample@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.sample/-/lodash.sample-4.2.1.tgz#5e4291b0c753fa1abeb0aab8fb29df1b66f07f6d" + +lodash.shuffle@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.shuffle/-/lodash.shuffle-4.2.0.tgz#145b5053cf875f6f5c2a33f48b6e9948c6ec7b4b" + +lodash.values@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" + lodash@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" @@ -4194,6 +4299,14 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" +redis-commands@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.1.tgz#81d826f45fa9c8b2011f4cd7a0fe597d241d442b" + +redis-parser@^2.4.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" + regenerate@^1.2.1: version "1.3.3" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" From 8d23952cdf774d384ed144ac93f03b2020b73dba Mon Sep 17 00:00:00 2001 From: Ganzorig Bayarsaikhan Date: Thu, 23 Nov 2017 09:20:21 +0800 Subject: [PATCH 304/318] Add reset password email template --- src/data/resolvers/mutations/users.js | 2 +- src/private/emailTemplates/base.html | 20 ++++++------ src/private/emailTemplates/resetPassword.html | 31 +++++++++++++++++++ 3 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 src/private/emailTemplates/resetPassword.html diff --git a/src/data/resolvers/mutations/users.js b/src/data/resolvers/mutations/users.js index e3aaaa187..09098b241 100644 --- a/src/data/resolvers/mutations/users.js +++ b/src/data/resolvers/mutations/users.js @@ -32,7 +32,7 @@ const userMutations = { fromEmail: COMPANY_EMAIL_FROM, title: 'Reset password', template: { - name: 'base', + name: 'resetPassword', data: { content: link, }, diff --git a/src/private/emailTemplates/base.html b/src/private/emailTemplates/base.html index 56a8def36..32fd235b4 100644 --- a/src/private/emailTemplates/base.html +++ b/src/private/emailTemplates/base.html @@ -83,15 +83,15 @@
- +
-
+
-
+ @@ -110,20 +110,20 @@
- +
- +
{{{ content }}}{{{ content }}}
- +
-
+ @@ -134,15 +134,15 @@
- +
-
+
-
+

{{{ signature }}} diff --git a/src/private/emailTemplates/resetPassword.html b/src/private/emailTemplates/resetPassword.html new file mode 100644 index 000000000..e5ad978b5 --- /dev/null +++ b/src/private/emailTemplates/resetPassword.html @@ -0,0 +1,31 @@ + + + + + + +
+
+

+ You recently requested a password reset. Click the link below to continue. +
+

+ + + + + + + +
+ + Reset password + +
+ +

+

or click here
+ {{{ content }}} +

+
+
From 1a6c09acb82a795e58f6f8ef11b46b25d5f6fa8e Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 23 Nov 2017 12:12:51 +0800 Subject: [PATCH 305/318] Change conversationChanged subscription type argument value --- src/data/resolvers/mutations/conversations.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index bfd2c4f66..a8bd428b5 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -190,7 +190,7 @@ const conversationMutations = { ); // notify graphl subscription - await conversationsChanged(conversationIds, 'statusChanged'); + await conversationsChanged(conversationIds, 'assigneeChanged'); for (let conversation of updatedConversations) { const content = 'Assigned user has changed'; @@ -218,7 +218,7 @@ const conversationMutations = { const conversations = await Conversations.unassignUserConversation(_ids); // notify graphl subscription - conversationsChanged(_ids, 'statusChanged'); + conversationsChanged(_ids, 'assigneeChanged'); return conversations; }, @@ -235,7 +235,7 @@ const conversationMutations = { await Conversations.changeStatusConversation(_ids, status, user._id); // notify graphl subscription - await conversationsChanged(_ids, 'statusChanged'); + await conversationsChanged(_ids, status); for (let conversation of conversations) { if (status === CONVERSATION_STATUSES.CLOSED) { From 19d82f2156539257a2f34a35e2189297a50293ab Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 23 Nov 2017 13:08:23 +0800 Subject: [PATCH 306/318] Close #38 --- src/data/schema/customer.js | 3 ++- src/db/models/Customers.js | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/data/schema/customer.js b/src/data/schema/customer.js index ba6d98c85..553bd742e 100644 --- a/src/data/schema/customer.js +++ b/src/data/schema/customer.js @@ -10,8 +10,9 @@ export const types = ` isUser: Boolean createdAt: Date tagIds: [String] + remoteAddress: String internalNotes: JSON - + location: JSON customFieldsData: JSON messengerData: JSON twitterData: JSON diff --git a/src/db/models/Customers.js b/src/db/models/Customers.js index 4d54016d5..8b95c53d5 100644 --- a/src/db/models/Customers.js +++ b/src/db/models/Customers.js @@ -89,6 +89,9 @@ const CustomerSchema = mongoose.Schema({ tagIds: field({ type: [String] }), companyIds: field({ type: [String] }), + remoteAddress: field({ type: String }), + location: field({ type: Object }), + customFieldsData: field({ type: Object }), messengerData: field({ type: messengerSchema }), twitterData: field({ type: twitterSchema }), From bea1eb9a8f96e00ebad5e6df159dc125ec75fd6f Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 23 Nov 2017 13:24:14 +0800 Subject: [PATCH 307/318] Close #39 --- src/db/models/Segments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/models/Segments.js b/src/db/models/Segments.js index 86599e0c7..8a231fe97 100644 --- a/src/db/models/Segments.js +++ b/src/db/models/Segments.js @@ -28,7 +28,7 @@ const SegmentSchema = mongoose.Schema({ enum: COC_CONTENT_TYPES.ALL, }), name: field({ type: String }), - description: field({ type: String }), + description: field({ type: String, optional: true }), subOf: field({ type: String }), color: field({ type: String }), connector: field({ type: String }), From 8130ba8bfa7c0fc1f09e87d2bf20cd95691b5cff Mon Sep 17 00:00:00 2001 From: batamar Date: Thu, 23 Nov 2017 13:43:59 +0800 Subject: [PATCH 308/318] Made twitterUsername optional --- src/db/models/Users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/models/Users.js b/src/db/models/Users.js index a35a600f0..e3a75a314 100644 --- a/src/db/models/Users.js +++ b/src/db/models/Users.js @@ -22,7 +22,7 @@ const DetailSchema = mongoose.Schema( avatar: field({ type: String }), fullName: field({ type: String }), position: field({ type: String }), - twitterUsername: field({ type: String }), + twitterUsername: field({ type: String, optional: true }), }, { _id: false }, ); From e5206f52ff739010851966615dea285bf9649dc0 Mon Sep 17 00:00:00 2001 From: batamar Date: Mon, 27 Nov 2017 20:41:25 +0800 Subject: [PATCH 309/318] Close #41 --- src/data/resolvers/mutations/conversations.js | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index a8bd428b5..497e2a79e 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -112,12 +112,24 @@ const conversationMutations = { * @return {Promise} newly created message object */ async conversationMessageAdd(root, doc, { user }) { - const message = await ConversationMessages.addMessage(doc, user._id); - const conversation = await Conversations.findOne({ _id: doc.conversationId }); + const integration = await Integrations.findOne({ _id: conversation.integrationId }); + + if (!integration) { + throw new Error('Integration not found'); + } + + const kind = integration.kind; + + // send reply to twitter + if (kind === KIND_CHOICES.TWITTER) { + await tweetReply(conversation, strip(doc.content)); + return null; + } + + // send notification ======= const title = 'You have a new message.'; - // send notification sendNotification({ createdUser: user._id, notifType: NOTIFICATION_TYPES.CONVERSATION_ADD_MESSAGE, @@ -127,6 +139,8 @@ const conversationMutations = { receivers: conversationNotifReceivers(conversation, user._id), }); + const message = await ConversationMessages.addMessage(doc, user._id); + // do not send internal message to third service integrations if (doc.internal) { // notify subscription @@ -135,20 +149,6 @@ const conversationMutations = { return message; } - const integration = await Integrations.findOne({ _id: conversation.integrationId }); - - if (!integration) { - throw new Error('Integration not found'); - } - - const kind = integration.kind; - - // send reply to twitter - if (kind === KIND_CHOICES.TWITTER) { - await tweetReply(conversation, strip(doc.content)); - return message; - } - const customer = await Customers.findOne({ _id: conversation.customerId }); // if conversation's integration kind is form then send reply to From cc5c4b8825d73e678aaf6029ef54243319a0d85f Mon Sep 17 00:00:00 2001 From: batamar Date: Tue, 28 Nov 2017 17:08:33 +0800 Subject: [PATCH 310/318] Add some await in facebook tracker --- src/data/resolvers/mutations/conversations.js | 1 + src/social/facebook.js | 15 +++++++------ src/social/facebookTracker.js | 21 +++++++------------ 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/data/resolvers/mutations/conversations.js b/src/data/resolvers/mutations/conversations.js index 497e2a79e..46d137a5c 100644 --- a/src/data/resolvers/mutations/conversations.js +++ b/src/data/resolvers/mutations/conversations.js @@ -54,6 +54,7 @@ const conversationsChanged = async (_ids, type) => { }); } } + return _ids; }; diff --git a/src/social/facebook.js b/src/social/facebook.js index fd0e478d5..c329dfe14 100755 --- a/src/social/facebook.js +++ b/src/social/facebook.js @@ -197,7 +197,7 @@ export class SaveWebhookResponse { let postId = value.post_id; // get page access token - let response = graphRequest.get( + let response = await graphRequest.get( `${this.currentPageId}/?fields=access_token`, this.userAccessToken, ); @@ -208,7 +208,7 @@ export class SaveWebhookResponse { } // get post object - response = graphRequest.get(postId, response.access_token); + response = await graphRequest.get(postId, response.access_token); postId = response.id; @@ -313,10 +313,13 @@ export class SaveWebhookResponse { } // get page access token - let res = graphRequest.get(`${this.currentPageId}/?fields=access_token`, this.userAccessToken); + let res = await graphRequest.get( + `${this.currentPageId}/?fields=access_token`, + this.userAccessToken, + ); // get user info - res = graphRequest.get(`/${fbUserId}`, res.access_token); + res = await graphRequest.get(`/${fbUserId}`, res.access_token); // when feed response will contain name field // when messeger response will not contain name field @@ -395,7 +398,7 @@ export const facebookReply = async (conversation, text, messageId) => { const app = FACEBOOK_APPS.find(a => a.id === integration.facebookData.appId); // page access token - const response = graphRequest.get( + const response = await graphRequest.get( `${conversation.facebookData.pageId}/?fields=access_token`, app.accessToken, ); @@ -419,7 +422,7 @@ export const facebookReply = async (conversation, text, messageId) => { const postId = conversation.facebookData.postId; // post reply - const commentResponse = graphRequest.post(`${postId}/comments`, response.access_token, { + const commentResponse = await graphRequest.post(`${postId}/comments`, response.access_token, { message: text, }); diff --git a/src/social/facebookTracker.js b/src/social/facebookTracker.js index c29442cb6..2b738b458 100644 --- a/src/social/facebookTracker.js +++ b/src/social/facebookTracker.js @@ -10,22 +10,15 @@ export const graphRequest = { // set access token graph.setAccessToken(accessToken); - try { - return new Promise((resolve, reject) => { - graph[method](path, ...otherParams, (error, response) => { - if (error) { - return reject(error); - } + return new Promise((resolve, reject) => { + graph[method](path, ...otherParams, (error, response) => { + if (error) { + return reject(error); + } - return resolve(response); - }); + return resolve(response); }); - - // catch session expired or some other error - } catch (e) { - console.log(e.message); // eslint-disable-line no-console - return e.message; - } + }); }, get(...args) { From 0de95ab325c2fc5a9612cfea665492827b57e867 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 1 Dec 2017 13:02:51 +0800 Subject: [PATCH 311/318] Add getTags resolver to engage message --- src/data/resolvers/engage.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/data/resolvers/engage.js b/src/data/resolvers/engage.js index 685ae2ca9..ca03e3f8e 100644 --- a/src/data/resolvers/engage.js +++ b/src/data/resolvers/engage.js @@ -1,4 +1,4 @@ -import { Segments, Users } from '../../db/models'; +import { Segments, Users, Tags } from '../../db/models'; export default { segment(engageMessage) { @@ -8,4 +8,8 @@ export default { fromUser(engageMessage) { return Users.findOne({ _id: engageMessage.fromUserId }); }, + + getTags(engageMessage) { + return Tags.find({ _id: { $in: engageMessage.tagIds || [] } }); + }, }; From 523ee286bc0b8705b93f0e8477c2ef0359f83195 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 1 Dec 2017 13:06:47 +0800 Subject: [PATCH 312/318] Add getTags to engage schema --- src/data/schema/engage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/schema/engage.js b/src/data/schema/engage.js index dc0b46825..5c2bce8b0 100644 --- a/src/data/schema/engage.js +++ b/src/data/schema/engage.js @@ -20,6 +20,7 @@ export const types = ` segment: Segment fromUser: User + getTags: [Tag] } input EngageMessageMessengerRule { From 02c37f9b49c0a89977cf98bb498ee41ed0a00665 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 1 Dec 2017 13:36:26 +0800 Subject: [PATCH 313/318] Remove unnessary call in conversation cronjob --- src/cronJobs/conversations.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cronJobs/conversations.js b/src/cronJobs/conversations.js index b6c9441e5..1e65664fd 100644 --- a/src/cronJobs/conversations.js +++ b/src/cronJobs/conversations.js @@ -104,5 +104,3 @@ export const sendMessageEmail = async () => { schedule.scheduleJob('*/10 * * * *', function() { sendMessageEmail(); }); - -sendMessageEmail(); From cb2f147250d4433ca6c249e63f54810f7d20a96d Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 1 Dec 2017 14:04:26 +0800 Subject: [PATCH 314/318] Remove callback from graphRequest.post --- src/social/facebook.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/social/facebook.js b/src/social/facebook.js index c329dfe14..a839d4846 100755 --- a/src/social/facebook.js +++ b/src/social/facebook.js @@ -405,16 +405,10 @@ export const facebookReply = async (conversation, text, messageId) => { // messenger reply if (conversation.facebookData.kind === FACEBOOK_DATA_KINDS.MESSENGER) { - return graphRequest.post( - 'me/messages', - response.access_token, - { - recipient: { id: conversation.facebookData.senderId }, - message: { text }, - }, - /* istanbul ignore next */ - () => {}, - ); + return graphRequest.post('me/messages', response.access_token, { + recipient: { id: conversation.facebookData.senderId }, + message: { text }, + }); } // feed reply From a71e848f1b8db1e0c8d0d6c1ccab86ca23c0a06d Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 1 Dec 2017 14:55:56 +0800 Subject: [PATCH 315/318] Check already saved messanger message --- src/__tests__/social/facebook.reply.test.js | 8 +++- .../social/facebook.saveResponse.test.js | 37 ++++++++++++++++++- src/data/resolvers/subscriptions/pubsub.js | 2 - src/db/models/ConversationMessages.js | 6 +++ src/social/facebook.js | 18 ++++++++- 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/__tests__/social/facebook.reply.test.js b/src/__tests__/social/facebook.reply.test.js index 0f6a76e6e..860c6ff4e 100755 --- a/src/__tests__/social/facebook.reply.test.js +++ b/src/__tests__/social/facebook.reply.test.js @@ -64,7 +64,13 @@ describe('facebook integration: reply', () => { const text = 'to messenger'; // mock post messenger reply - const stub = sinon.stub(graphRequest, 'post').callsFake(() => {}); + const stub = sinon.stub(graphRequest, 'post').callsFake(() => { + return new Promise(resolve => { + resolve({ + message_id: 'message_id', + }); + }); + }); // reply await facebookReply(conversation, text); diff --git a/src/__tests__/social/facebook.saveResponse.test.js b/src/__tests__/social/facebook.saveResponse.test.js index f126e0abb..2887aeeab 100755 --- a/src/__tests__/social/facebook.saveResponse.test.js +++ b/src/__tests__/social/facebook.saveResponse.test.js @@ -49,11 +49,22 @@ describe('facebook integration: save webhook response', () => { }; }); + sinon.stub(graphRequest, 'post').callsFake(() => { + return new Promise(resolve => { + resolve({ + id: 'commentId', + message_id: 'message_id', + }); + }); + }); + saveWebhookResponse = new SaveWebhookResponse('access_token', integration, {}); }); afterEach(async () => { - graphRequest.get.restore(); // unwraps the spy + // unwraps the spy + graphRequest.get.restore(); + graphRequest.post.restore(); // clear previous datas await Conversations.remove({}); @@ -91,6 +102,7 @@ describe('facebook integration: save webhook response', () => { sender: { id: senderId }, recipient: { id: recipientId }, message: { + mid: 'mid0', text: messageText, attachments, }, @@ -147,6 +159,7 @@ describe('facebook integration: save webhook response', () => { recipient: { id: recipientId }, message: { + mid: 'mid', text: messageText, }, }, @@ -176,6 +189,28 @@ describe('facebook integration: save webhook response', () => { expect(newMessage.customerId).toBe(customer._id); expect(newMessage.internal).toBe(false); expect(newMessage.content).toBe(messageText); + + // receiving already saved info ======================== + saveWebhookResponse.data = { + object: 'page', + entry: [ + { + id: pageId, + messaging: [ + { + sender: { id: senderId }, + recipient: { id: recipientId }, + message: { mid: 'mid' }, + }, + ], + }, + ], + }; + + await saveWebhookResponse.start(); + + // must not be created new message + expect(await ConversationMessages.find().count()).toBe(2); }); test('via feed event', async () => { diff --git a/src/data/resolvers/subscriptions/pubsub.js b/src/data/resolvers/subscriptions/pubsub.js index 081abcf5f..6f911e578 100644 --- a/src/data/resolvers/subscriptions/pubsub.js +++ b/src/data/resolvers/subscriptions/pubsub.js @@ -4,8 +4,6 @@ const redisConnectionListener = error => { if (error) { console.error(error); // eslint-disable-line no-console } - - console.info('Successfuly connected to redis'); // eslint-disable-line no-console }; // Docs on the different redis options diff --git a/src/db/models/ConversationMessages.js b/src/db/models/ConversationMessages.js index 7d099f107..cc134a78d 100644 --- a/src/db/models/ConversationMessages.js +++ b/src/db/models/ConversationMessages.js @@ -10,6 +10,12 @@ const FacebookSchema = mongoose.Schema( optional: true, }), + // messenger message id + messageId: field({ + type: String, + optional: true, + }), + // comment, reaction, etc ... item: field({ type: String, diff --git a/src/social/facebook.js b/src/social/facebook.js index a839d4846..7417b5afb 100755 --- a/src/social/facebook.js +++ b/src/social/facebook.js @@ -256,6 +256,7 @@ export class SaveWebhookResponse { const senderId = event.sender.id; const senderName = event.sender.name; const recipientId = event.recipient.id; + const messageId = event.message.mid; const messageText = event.message.text || 'attachment'; // collect attachment's url, type fields @@ -264,6 +265,11 @@ export class SaveWebhookResponse { url: attachment.payload ? attachment.payload.url : '', })); + // if this is already saved then ignore it + if (await ConversationMessages.findOne({ 'facebookData.messageId': messageId })) { + return null; + } + await this.getOrCreateConversation({ // try to find conversation by senderId, recipientId keys findSelector: { @@ -291,7 +297,9 @@ export class SaveWebhookResponse { // message data content: messageText, attachments, - msgFacebookData: {}, + msgFacebookData: { + messageId, + }, }); } @@ -405,10 +413,16 @@ export const facebookReply = async (conversation, text, messageId) => { // messenger reply if (conversation.facebookData.kind === FACEBOOK_DATA_KINDS.MESSENGER) { - return graphRequest.post('me/messages', response.access_token, { + const messageResponse = await graphRequest.post('me/messages', response.access_token, { recipient: { id: conversation.facebookData.senderId }, message: { text }, }); + + // save commentId in message object + await ConversationMessages.update( + { _id: messageId }, + { $set: { 'facebookData.messageId': messageResponse.message_id } }, + ); } // feed reply From 44ca10bb1149753474af50386b4e6d6e7efb6424 Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 1 Dec 2017 16:02:58 +0800 Subject: [PATCH 316/318] Remove twitPost callback --- src/__tests__/conversationMutations.test.js | 9 ++++-- src/__tests__/social/twitter.test.js | 15 +++++++--- src/social/twitter.js | 33 +++++++-------------- src/social/twitterTracker.js | 27 ++++++++++++----- 4 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/__tests__/conversationMutations.test.js b/src/__tests__/conversationMutations.test.js index 1cc8c8ee5..6f5db98ca 100644 --- a/src/__tests__/conversationMutations.test.js +++ b/src/__tests__/conversationMutations.test.js @@ -14,6 +14,7 @@ import { } from '../db/factories'; import conversationMutations from '../data/resolvers/mutations/conversations'; import { TwitMap } from '../social/twitter'; +import { socUtils } from '../social/twitterTracker'; import { graphRequest } from '../social/facebookTracker'; import utils from '../data/utils'; import { FACEBOOK_DATA_KINDS } from '../data/constants'; @@ -173,7 +174,11 @@ describe('Conversation message mutations', () => { // mock twitter request const sandbox = sinon.sandbox.create(); - const stub = sandbox.stub(twit, 'post').callsFake(() => {}); + const stub = sandbox.stub(socUtils, 'twitPost').callsFake(() => { + return new Promise(resolve => { + resolve({}); + }); + }); // creating doc =================== const integration = await integrationFactory({ kind: 'twitter' }); @@ -197,7 +202,7 @@ describe('Conversation message mutations', () => { // check twit post params expect( - stub.calledWith('direct_messages/new', { + stub.calledWith(twit, 'direct_messages/new', { user_id: 'sender_id', text: _doc.content, }), diff --git a/src/__tests__/social/twitter.test.js b/src/__tests__/social/twitter.test.js index 47c68c892..cd21d4c3f 100755 --- a/src/__tests__/social/twitter.test.js +++ b/src/__tests__/social/twitter.test.js @@ -14,6 +14,8 @@ import { getOrCreateDirectMessageConversation, } from '../../social/twitter'; +import { socUtils } from '../../social/twitterTracker'; + beforeAll(() => connect()); afterAll(() => disconnect()); @@ -137,12 +139,17 @@ describe('twitter integration', () => { TwitMap[_integration._id] = twit; // twit.post - stub = sandbox.stub(twit, 'post').callsFake(() => {}); + stub = sandbox.stub(socUtils, 'twitPost').callsFake(() => { + return new Promise(resolve => { + return resolve({}); + }); + }); }); afterEach(async () => { // unwrap the spy - twit.post.restore(); + socUtils.twitPost.restore(); + await Conversations.remove({}); await Integrations.remove({}); await ConversationMessages.remove({}); @@ -171,7 +178,7 @@ describe('twitter integration', () => { // check twit post params expect( - stub.calledWith('direct_messages/new', { + stub.calledWith(twit, 'direct_messages/new', { user_id: senderId.toString(), text, }), @@ -197,7 +204,7 @@ describe('twitter integration', () => { // check twit post params expect( - stub.calledWith('statuses/update', { + stub.calledWith(twit, 'statuses/update', { status: `@${screenName} ${text}`, // replying tweet id diff --git a/src/social/twitter.js b/src/social/twitter.js index ab438b8d9..72b3bf100 100755 --- a/src/social/twitter.js +++ b/src/social/twitter.js @@ -1,5 +1,6 @@ import { Customers, ConversationMessages, Conversations, Integrations } from '../db/models'; import { conversationMessageCreated } from '../data/resolvers/mutations/conversations'; +import { socUtils } from './twitterTracker'; /* * Get or create customer using twitter data @@ -220,31 +221,17 @@ export const tweetReply = (conversation, text) => { // send direct message if (conversation.twitterData.isDirectMessage) { - return twit.post( - 'direct_messages/new', - { - user_id: twitterData.directMessage.senderIdStr, - text, - }, - /* istanbul ignore next */ - e => { - if (e) throw Error(e.message); - }, - ); + return socUtils.twitPost(twit, 'direct_messages/new', { + user_id: twitterData.directMessage.senderIdStr, + text, + }); } // send reply - return twit.post( - 'statuses/update', - { - status: `@${twitterData.screenName} ${text}`, + return socUtils.twitPost(twit, 'statuses/update', { + status: `@${twitterData.screenName} ${text}`, - // replying tweet id - in_reply_to_status_id: twitterData.idStr, - }, - /* istanbul ignore next */ - e => { - if (e) throw Error(e.message); - }, - ); + // replying tweet id + in_reply_to_status_id: twitterData.idStr, + }); }; diff --git a/src/social/twitterTracker.js b/src/social/twitterTracker.js index 3e53be33b..fd445c4bb 100644 --- a/src/social/twitterTracker.js +++ b/src/social/twitterTracker.js @@ -39,13 +39,6 @@ const socTwitter = new soc.Twitter({ const authenticate = queryParams => socTwitter.callback({ query: queryParams }); -// doing this to mock authenticate function in test -export const socUtils = { - authenticate, - trackIntegration, - getTwitterAuthorizeUrl: () => socTwitter.getAuthorizeUrl(), -}; - /* * Track all twitter integrations for the first time */ @@ -56,3 +49,23 @@ export const trackIntegrations = () => { } }); }; + +export const twitPost = (twit, path, data) => { + return new Promise((resolve, reject) => { + twit.post(path, data, (e, response) => { + if (e) { + return reject(e); + } + + return resolve(response); + }); + }); +}; + +// doing this to mock authenticate function in test +export const socUtils = { + authenticate, + trackIntegration, + twitPost, + getTwitterAuthorizeUrl: () => socTwitter.getAuthorizeUrl(), +}; From 17d58f8bff21d781ad3d250c6d33e49f3353bafb Mon Sep 17 00:00:00 2001 From: batamar Date: Fri, 1 Dec 2017 21:50:18 +0800 Subject: [PATCH 317/318] Little twit request refactor --- src/__tests__/social/twitter.test.js | 6 +++--- src/social/twitter.js | 6 +++--- src/social/twitterTracker.js | 22 +++++++++++++--------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/__tests__/social/twitter.test.js b/src/__tests__/social/twitter.test.js index cd21d4c3f..453275b78 100755 --- a/src/__tests__/social/twitter.test.js +++ b/src/__tests__/social/twitter.test.js @@ -14,7 +14,7 @@ import { getOrCreateDirectMessageConversation, } from '../../social/twitter'; -import { socUtils } from '../../social/twitterTracker'; +import { twitRequest } from '../../social/twitterTracker'; beforeAll(() => connect()); afterAll(() => disconnect()); @@ -139,7 +139,7 @@ describe('twitter integration', () => { TwitMap[_integration._id] = twit; // twit.post - stub = sandbox.stub(socUtils, 'twitPost').callsFake(() => { + stub = sandbox.stub(twitRequest, 'post').callsFake(() => { return new Promise(resolve => { return resolve({}); }); @@ -148,7 +148,7 @@ describe('twitter integration', () => { afterEach(async () => { // unwrap the spy - socUtils.twitPost.restore(); + twitRequest.post.restore(); await Conversations.remove({}); await Integrations.remove({}); diff --git a/src/social/twitter.js b/src/social/twitter.js index 72b3bf100..f8695a98a 100755 --- a/src/social/twitter.js +++ b/src/social/twitter.js @@ -1,6 +1,6 @@ import { Customers, ConversationMessages, Conversations, Integrations } from '../db/models'; import { conversationMessageCreated } from '../data/resolvers/mutations/conversations'; -import { socUtils } from './twitterTracker'; +import { twitRequest } from './twitterTracker'; /* * Get or create customer using twitter data @@ -221,14 +221,14 @@ export const tweetReply = (conversation, text) => { // send direct message if (conversation.twitterData.isDirectMessage) { - return socUtils.twitPost(twit, 'direct_messages/new', { + return twitRequest.post(twit, 'direct_messages/new', { user_id: twitterData.directMessage.senderIdStr, text, }); } // send reply - return socUtils.twitPost(twit, 'statuses/update', { + return twitRequest.post(twit, 'statuses/update', { status: `@${twitterData.screenName} ${text}`, // replying tweet id diff --git a/src/social/twitterTracker.js b/src/social/twitterTracker.js index fd445c4bb..9690ac3e1 100644 --- a/src/social/twitterTracker.js +++ b/src/social/twitterTracker.js @@ -50,22 +50,26 @@ export const trackIntegrations = () => { }); }; -export const twitPost = (twit, path, data) => { - return new Promise((resolve, reject) => { - twit.post(path, data, (e, response) => { - if (e) { - return reject(e); - } +/* + * Promisify twit post util + */ +export const twitRequest = { + post(twit, path, data) { + return new Promise((resolve, reject) => { + twit.post(path, data, (e, response) => { + if (e) { + return reject(e); + } - return resolve(response); + return resolve(response); + }); }); - }); + }, }; // doing this to mock authenticate function in test export const socUtils = { authenticate, trackIntegration, - twitPost, getTwitterAuthorizeUrl: () => socTwitter.getAuthorizeUrl(), }; From 6eee2c4fe824d5959aa4bbb42060dec968637039 Mon Sep 17 00:00:00 2001 From: batamar Date: Sat, 2 Dec 2017 03:46:48 +0800 Subject: [PATCH 318/318] Bumped version number to 0.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6eabd652a..734fff5a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "erxes-app-api", - "version": "0.8.1", + "version": "0.9.0", "description": "GraphQL API for erxes main project", "homepage": "https://erxes.io", "repository": "https://github.com/erxes/erxes-app-api",