diff --git a/contacts-endpoints-lifecycle.png b/contacts-endpoints-lifecycle.png new file mode 100644 index 0000000..2db4a9b Binary files /dev/null and b/contacts-endpoints-lifecycle.png differ diff --git a/package-lock.json b/package-lock.json index 3fb12fd..23780a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,13 @@ "license": "ISC", "dependencies": { "cors": "^2.8.5", - "express": "^4.18.2", - "morgan": "^1.10.0", - "nodemon": "^3.0.1" + "express": "^4.19.2", + "joi": "^13.1.0", + "morgan": "^1.10.0" }, "devDependencies": { "jest": "^29.7.0", + "nodemon": "^3.1.3", "supertest": "^6.2.3" } }, @@ -1182,7 +1183,8 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true }, "node_modules/accepts": { "version": "1.3.8", @@ -1239,6 +1241,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1383,7 +1386,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/basic-auth": { "version": "2.0.1", @@ -1405,17 +1409,18 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, "engines": { "node": ">=8" } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -1423,7 +1428,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -1436,6 +1441,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1445,6 +1451,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -1587,6 +1594,7 @@ "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, "funding": [ { "type": "individual", @@ -1702,7 +1710,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/content-disposition": { "version": "0.5.4", @@ -1730,9 +1739,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -2032,16 +2041,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -2097,6 +2106,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2189,6 +2199,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -2283,6 +2294,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -2378,6 +2390,15 @@ "node": ">=8" } }, + "node_modules/hoek": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", + "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2422,7 +2443,8 @@ "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true }, "node_modules/import-local": { "version": "3.1.0", @@ -2485,6 +2507,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -2508,6 +2531,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -2534,6 +2558,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -2545,6 +2570,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "engines": { "node": ">=0.12.0" } @@ -2561,6 +2587,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isemail": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", + "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", + "dependencies": { + "punycode": "2.x.x" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3277,6 +3314,20 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joi": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.1.0.tgz", + "integrity": "sha512-x6pGmDYI6hwNi3skP6irQqRaJntzeaWmZ4rsnjc/NTlf6P5Gp3Aw/O8REe8oLJ6wPhrzd9K3RW1m3Yz/Hx4Weg==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "dependencies": { + "hoek": "5.x.x", + "isemail": "3.x.x", + "topo": "3.x.x" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3511,6 +3562,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3576,9 +3628,10 @@ "dev": true }, "node_modules/nodemon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.2.tgz", - "integrity": "sha512-9qIN2LNTrEzpOPBaWHTm4Asy1LxXLSickZStAQ4IZe7zsoIpD/A7LWxhZV3t4Zu352uBcqVnRsDXSMR2Sc3lTA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.3.tgz", + "integrity": "sha512-m4Vqs+APdKzDFpuaL9F9EVOF85+h070FnkHVEoU4+rmT6Vw0bmNl7s61VEkY/cJkL7RCv1p4urnUDUMrS5rk2w==", + "dev": true, "dependencies": { "chokidar": "^3.5.2", "debug": "^4", @@ -3606,6 +3659,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -3622,6 +3676,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "engines": { "node": ">=4" } @@ -3630,6 +3685,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -3640,12 +3696,14 @@ "node_modules/nodemon/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/nodemon/node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -3660,6 +3718,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -3670,12 +3729,14 @@ "node_modules/nodemon/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/nopt": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, "dependencies": { "abbrev": "1" }, @@ -3690,6 +3751,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -3890,6 +3952,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -3972,7 +4035,16 @@ "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } }, "node_modules/pure-rand": { "version": "6.0.4", @@ -4013,9 +4085,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -4036,6 +4108,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -4237,6 +4310,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, "dependencies": { "semver": "^7.5.3" }, @@ -4248,6 +4322,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -4259,6 +4334,7 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -4272,7 +4348,8 @@ "node_modules/simple-update-notifier/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/sisteransi": { "version": "1.0.5", @@ -4562,6 +4639,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -4577,10 +4655,26 @@ "node": ">=0.6" } }, + "node_modules/topo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", + "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==", + "deprecated": "This module has moved and is now available at @hapi/topo. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.", + "dependencies": { + "hoek": "6.x.x" + } + }, + "node_modules/topo/node_modules/hoek": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", + "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==", + "deprecated": "This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues." + }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, "dependencies": { "nopt": "~1.0.10" }, @@ -4624,7 +4718,8 @@ "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true }, "node_modules/undici-types": { "version": "5.26.5", diff --git a/package.json b/package.json index ec6510a..fe2feeb 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,13 @@ "homepage": "https://github.com/boolean-uk/api-address-book#readme", "dependencies": { "cors": "^2.8.5", - "express": "^4.18.2", - "morgan": "^1.10.0", - "nodemon": "^3.0.1" + "express": "^4.19.2", + "joi": "^13.1.0", + "morgan": "^1.10.0" }, "devDependencies": { "jest": "^29.7.0", + "nodemon": "^3.1.3", "supertest": "^6.2.3" } } diff --git a/src/server.js b/src/server.js index 30f0ce3..f4f3a2e 100644 --- a/src/server.js +++ b/src/server.js @@ -1,13 +1,166 @@ -const express = require("express") -const morgan = require("morgan") -const cors = require("cors") -const app = express() +const express = require("express"); +const Joi = require("joi"); +const morgan = require("morgan"); +const cors = require("cors"); +const app = express(); -app.use(morgan("dev")) -app.use(cors()) -app.use(express.json()) +app.use(morgan("dev")); +app.use(cors()); +app.use(express.json()); -// write your app code here +const contacts = require("../data/contacts"); +const meetings = require("../data/meetings"); +// 1- Contacts +// a) Helper functions +function findContact(req, res) { + const id = Number.parseInt(req.params.id, 10); + const contact = contacts.find((contact) => contact.id === id); -module.exports = app + if (!contact) + return res.status(404).send(`The contact with ID ${id} is not found.`); + + return contact; +} + +function validateContact(req, res) { + const schema = { + firstName: Joi.string().required(), + lastName: Joi.string().required(), + street: Joi.string().required(), + city: Joi.string().required(), + type: Joi.string().required(), + email: Joi.string().required(), + linkedin: Joi.string().required(), + twitter: Joi.string().required(), + }; + + const { error } = Joi.validate(req.body, schema); + + if (error) { + return res.status(400).send(error.details[0].message); + } +} + +// b) API verbs +app.get("/contacts", (req, res) => { + res.status(200).json({ contacts: contacts }); +}); + +app.post("/contacts", (req, res) => { + validateContact(req, res); + + const id = contacts[contacts.length - 1].id + 1; + const contact = Object.assign({ id }, req.body); + + contacts.push(contact); + res.status(201).json({ contact }); +}); + +app.get("/contacts/:id", (req, res) => { + const contact = findContact(req, res); + + res.status(200).json({ contact }); +}); + +app.delete("/contacts/:id", (req, res) => { + const contact = findContact(req, res); + + const index = contacts.indexOf(contact); + contacts.splice(index, 1); + + for (let key of meetings) { + if (key?.contactId === contact.id) { + meetings.splice(meetings.indexOf(key), 1); + } + } + + res.status(200).json({ contact }); +}); + +app.put("/contacts/:id", (req, res) => { + const contact = findContact(req, res); + + validateContact(req, res); + + contacts[contacts.indexOf(contact)] = { ...contact, ...req.body }; + + res.status(200).json({ contact: { ...contact, ...req.body } }); +}); + +// 2- Meetings +// a) Helper functions +function findMeeting(req, res) { + const id = Number.parseInt(req.params.id, 10); + const meeting = meetings.find((meeting) => meeting.id === id); + + if (!meeting) + return res.status(404).send(`The meeting with ID ${id} is not found.`); + + return meeting; +} + +function validateMeeting(req, res) { + const schema = { + name: Joi.string().required(), + }; + + const { error } = Joi.validate(req.body, schema); + + if (error) return res.status(400).send(error.details[0].message); +} + +// b) API verbs +app.get("/meetings", (req, res) => { + res.status(200).json({ meetings }); +}); + +app.get("/meetings/:id", (req, res) => { + const meeting = findMeeting(req, res); + + res.status(200).json({ meeting }); +}); + +app.delete("/meetings/:id", (req, res) => { + const meeting = findMeeting(req, res); + + const index = meetings.indexOf(meeting); + + meetings.splice(index, 1); + res.status(200).json({ meeting }); +}); + +app.put("/meetings/:id", (req, res) => { + const meeting = findMeeting(req, res); + + validateMeeting(req, res); + + meeting.name = req.body.name; + res.status(200).json({ meeting }); +}); + +app.get("/contacts/:id/meetings", (req, res) => { + const contactMeetings = meetings.filter( + (meeting) => meeting.contactId === findContact(req, res).id + ); + + if (!contactMeetings) + return res.status(404).send(`No meetings found for this contact.`); + + res.status(200).json({ meetings: contactMeetings }); +}); + +app.post("/contacts/:id/meetings", (req, res) => { + validateMeeting(req, res); + + const meetingId = meetings[meetings.length - 1].id + 1; + const contactId = Number.parseInt(req.params.id, 10); + + const newMeeting = Object.assign({ id: meetingId, contactId }, req.body); + + meetings.push(newMeeting); + + res.status(201).json({ meeting: newMeeting }); +}); + +module.exports = app; diff --git a/test/api/standard/contacts.spec.js b/test/api/standard/contacts.spec.js index 4c430a8..4ff2de5 100644 --- a/test/api/standard/contacts.spec.js +++ b/test/api/standard/contacts.spec.js @@ -1,135 +1,129 @@ -const supertest = require('supertest') +const supertest = require("supertest"); -describe('Address Book API', () => { - let app, createTestFormData, updateTestFormData +describe("Address Book API", () => { + let app, createTestFormData, updateTestFormData; beforeEach(() => { - app = require('../../../src/server.js') - createTestFormData = require('../../fixtures/contacts/createTestFormData.js') - updateTestFormData = require('../../fixtures/contacts/updateTestFormData.js') - }) - - describe('GET /contacts', () => { - it('returns default contacts', async () => { - const response = await supertest(app).get('/contacts') - - expect(response.status).toEqual(200) - expect(response.body.contacts).not.toEqual(undefined) - expect(response.body.contacts.length).toEqual(2) - - const [contact1, contact2] = response.body.contacts - expect(contact1.firstName).toEqual("John") - expect(contact1.lastName).toEqual("Carmack") - expect(contact2.firstName).toEqual("Grace") - expect(contact2.lastName).toEqual("Hopper") - }) - }) - - describe('GET /contacts/:id', () => { - it('returns contact, id 2', async () => { - const response = await supertest(app).get('/contacts/2') - - expect(response.status).toEqual(200) - expect(response.body.contact).not.toEqual(undefined) - - const contact = response.body.contact - expect(contact.id).toEqual(2) - expect(contact.firstName).toEqual("Grace") - expect(contact.lastName).toEqual("Hopper") - }) - }) - - describe('POST /contacts', () => { - it('returns created contact', async () => { + app = require("../../../src/server.js"); + createTestFormData = require("../../fixtures/contacts/createTestFormData.js"); + updateTestFormData = require("../../fixtures/contacts/updateTestFormData.js"); + }); + + describe("GET /contacts", () => { + it("returns default contacts", async () => { + const response = await supertest(app).get("/contacts"); + + expect(response.status).toEqual(200); + expect(response.body.contacts).not.toEqual(undefined); + expect(response.body.contacts.length).toEqual(2); + + const [contact1, contact2] = response.body.contacts; + expect(contact1.firstName).toEqual("John"); + expect(contact1.lastName).toEqual("Carmack"); + expect(contact2.firstName).toEqual("Grace"); + expect(contact2.lastName).toEqual("Hopper"); + }); + }); + + describe("GET /contacts/:id", () => { + it("returns contact, id 2", async () => { + const response = await supertest(app).get("/contacts/2"); + + expect(response.status).toEqual(200); + expect(response.body.contact).not.toEqual(undefined); + + const contact = response.body.contact; + expect(contact.id).toEqual(2); + expect(contact.firstName).toEqual("Grace"); + expect(contact.lastName).toEqual("Hopper"); + }); + }); + + describe("POST /contacts", () => { + it("returns created contact", async () => { const response = await supertest(app) - .post('/contacts') - .send(createTestFormData) + .post("/contacts") + .send(createTestFormData); - expect(response.status).toEqual(201) - expect(response.body.contact).not.toEqual(undefined) + expect(response.status).toEqual(201); + expect(response.body.contact).not.toEqual(undefined); - const contact = response.body.contact - expect(contact.id).toEqual(3) - expect(contact.firstName).toEqual(createTestFormData.firstName) - expect(contact.lastName).toEqual(createTestFormData.lastName) - }) + const contact = response.body.contact; + expect(contact.id).toEqual(3); + expect(contact.firstName).toEqual(createTestFormData.firstName); + expect(contact.lastName).toEqual(createTestFormData.lastName); + }); - it('adds contact to data store', async () => { - await supertest(app) - .post('/contacts') - .send(createTestFormData) + it("adds contact to data store", async () => { + await supertest(app).post("/contacts").send(createTestFormData); - const response = await supertest(app).get('/contacts') + const response = await supertest(app).get("/contacts"); - expect(response.status).toEqual(200) - expect(response.body.contacts).not.toEqual(undefined) - expect(response.body.contacts.length).toEqual(3) + expect(response.status).toEqual(200); + expect(response.body.contacts).not.toEqual(undefined); + expect(response.body.contacts.length).toEqual(3); - const [contact1, contact2, contact3] = response.body.contacts - expect(contact3.firstName).toEqual(createTestFormData.firstName) - expect(contact3.lastName).toEqual(createTestFormData.lastName) - }) - }) + const [contact1, contact2, contact3] = response.body.contacts; + expect(contact3.firstName).toEqual(createTestFormData.firstName); + expect(contact3.lastName).toEqual(createTestFormData.lastName); + }); + }); - describe('PUT /contacts', () => { - let updatedContact + describe("PUT /contacts", () => { + let updatedContact; beforeEach(() => { updatedContact = { ...updateTestFormData, - id: 1 - } - }) - it('returns updated contact', async () => { + id: 1, + }; + }); + it("returns updated contact", async () => { const response = await supertest(app) .put(`/contacts/1`) - .send(updateTestFormData) + .send(updateTestFormData); - expect(response.status).toEqual(200) - expect(response.body.contact).not.toEqual(undefined) + expect(response.status).toEqual(200); + expect(response.body.contact).not.toEqual(undefined); - const contact = response.body.contact - expect(contact).toMatchObject(updatedContact) - }) + const contact = response.body.contact; + expect(contact).toMatchObject(updatedContact); + }); - it('updated contact is in data store', async () => { - await supertest(app) - .put(`/contacts/1`) - .send(updateTestFormData) + it("updated contact is in data store", async () => { + await supertest(app).put(`/contacts/1`).send(updateTestFormData); - const response = await supertest(app).get('/contacts') + const response = await supertest(app).get("/contacts"); - expect(response.status).toEqual(200) - expect(response.body.contacts).not.toEqual(undefined) - expect(response.body.contacts.length).toEqual(2) + expect(response.status).toEqual(200); + expect(response.body.contacts).not.toEqual(undefined); + expect(response.body.contacts.length).toEqual(2); - const [contact1, contact2] = response.body.contacts - expect(contact1.firstName).toEqual(updatedContact.firstName) - expect(contact1.lastName).toEqual(updatedContact.lastName) - }) - }) + const [contact1, contact2] = response.body.contacts; + expect(contact1.firstName).toEqual(updatedContact.firstName); + expect(contact1.lastName).toEqual(updatedContact.lastName); + }); + }); - describe('DELETE /contacts', () => { - it('returns deleted contact', async () => { - const response = await supertest(app) - .delete(`/contacts/1`) + describe("DELETE /contacts", () => { + it("returns deleted contact", async () => { + const response = await supertest(app).delete(`/contacts/1`); - expect(response.status).toEqual(200) - expect(response.body.contact).not.toEqual(undefined) - expect(response.body.contact.id).toEqual(1) - }) + expect(response.status).toEqual(200); + expect(response.body.contact).not.toEqual(undefined); + expect(response.body.contact.id).toEqual(1); + }); - it('removes contact from data store', async () => { - const response = await supertest(app) - .delete(`/contacts/1`) + it("removes contact from data store", async () => { + const response = await supertest(app).delete(`/contacts/1`); - const deletedContact = response.body.contact + const deletedContact = response.body.contact; - const response2 = await supertest(app).get('/contacts') + const response2 = await supertest(app).get("/contacts"); - expect(response2.status).toEqual(200) - expect(response2.body.contacts).not.toEqual(undefined) - expect(response2.body.contacts.length).toEqual(1) - expect(response2.body.contacts).not.toContain(deletedContact) - }) - }) -}) + expect(response2.status).toEqual(200); + expect(response2.body.contacts).not.toEqual(undefined); + expect(response2.body.contacts.length).toEqual(1); + expect(response2.body.contacts).not.toContain(deletedContact); + }); + }); +});