diff --git a/.env.sample b/.env.sample deleted file mode 100644 index 4cc714a2..00000000 --- a/.env.sample +++ /dev/null @@ -1 +0,0 @@ -DATABASE_URL= \ No newline at end of file diff --git a/knexfile.js b/knexfile.js new file mode 100644 index 00000000..f5d4beaf --- /dev/null +++ b/knexfile.js @@ -0,0 +1,59 @@ +// Update with your config settings. +const path = require("path"); +const dotenv = require("dotenv").config(); +const { DATABASE_URL } = process.env; + +module.exports = { + development: { + client: "postgresql", + connection: DATABASE_URL, + migrations: { + directory: path.join(__dirname, "src", "db", "migrations"), + }, + seeds: { + directory: path.join(__dirname, "src", "db", "seeds"), + }, + }, + + // test: { + // client: "postgresql", + // pool: { min: 1, max: 5 }, + // connection: { + // database: "db_test", + // user: "username_test", + // password: "password_test", + // }, + // }, + + // staging: { + // client: "postgresql", + // connection: { + // database: "my_db", + // user: "username", + // password: "password", + // }, + // pool: { + // min: 2, + // max: 10, + // }, + // migrations: { + // tableName: "knex_migrations", + // }, + // }, + + // production: { + // client: "postgresql", + // connection: { + // database: "my_db", + // user: "username", + // password: "password", + // }, + // pool: { + // min: 2, + // max: 10, + // }, + // migrations: { + // tableName: "knex_migrations", + // }, + // }, +}; diff --git a/package-lock.json b/package-lock.json index 1e3d92c1..e8962904 100644 --- a/package-lock.json +++ b/package-lock.json @@ -141,6 +141,11 @@ "concat-map": "0.0.1" } }, + "buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -298,6 +303,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==" + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -395,6 +410,11 @@ "is-obj": "^2.0.0" } }, + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -426,6 +446,11 @@ "once": "^1.4.0" } }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, "escape-goat": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", @@ -437,6 +462,11 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -510,6 +540,11 @@ "dev": true, "optional": true }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -519,6 +554,11 @@ "pump": "^3.0.0" } }, + "getopts": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.2.5.tgz", + "integrity": "sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA==" + }, "glob-parent": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", @@ -562,6 +602,14 @@ "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "dev": true }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -629,6 +677,11 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true }, + "interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -652,6 +705,14 @@ "ci-info": "^2.0.0" } }, + "is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "requires": { + "has": "^1.0.3" + } + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -728,6 +789,41 @@ "json-buffer": "3.0.0" } }, + "knex": { + "version": "0.95.14", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.95.14.tgz", + "integrity": "sha512-j4qLjWySrC/JRRVtOpoR2LcS1yBOsd7Krc6mEukPvmTDX/w11pD52Pq9FYR56/kLXGeAV8jFdWBjsZFi1mscWg==", + "requires": { + "colorette": "2.0.16", + "commander": "^7.1.0", + "debug": "4.3.2", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "getopts": "2.2.5", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.5.0", + "rechoir": "0.7.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.1", + "tildify": "2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "latest-version": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", @@ -737,6 +833,11 @@ "package-json": "^6.3.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -923,22 +1024,109 @@ } } }, + "packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, + "pg": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz", + "integrity": "sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA==", + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.5.0", + "pg-pool": "^3.4.1", + "pg-protocol": "^1.5.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + } + }, + "pg-connection-string": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", + "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" + }, + "pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" + }, + "pg-pool": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz", + "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==" + }, + "pg-protocol": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", + "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" + }, + "pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "requires": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + } + }, + "pgpass": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz", + "integrity": "sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==", + "requires": { + "split2": "^3.1.1" + } + }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", "dev": true }, + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + }, + "postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" + }, + "postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" + }, + "postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "requires": { + "xtend": "^4.0.0" + } + }, "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", @@ -1012,6 +1200,16 @@ "strip-json-comments": "~2.0.1" } }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "readdirp": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", @@ -1021,6 +1219,14 @@ "picomatch": "^2.2.1" } }, + "rechoir": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.0.tgz", + "integrity": "sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q==", + "requires": { + "resolve": "^1.9.0" + } + }, "registry-auth-token": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", @@ -1039,6 +1245,20 @@ "rc": "^1.2.8" } }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + }, "responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", @@ -1130,6 +1350,14 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "requires": { + "readable-stream": "^3.0.0" + } + }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -1175,6 +1403,21 @@ } } }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -1199,12 +1442,22 @@ "has-flag": "^3.0.0" } }, + "tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==" + }, "term-size": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", "dev": true }, + "tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==" + }, "to-readable-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", @@ -1302,6 +1555,11 @@ "prepend-http": "^2.0.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1344,6 +1602,11 @@ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" } } } diff --git a/package.json b/package.json index 735d2834..3dd548db 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,17 @@ "description": "Starter code for the Node.js, Express, and PostgreSQL module", "main": "src/app.js", "scripts": { - "start": "node src/server.js", + "start": "nodemon src/server.js", "start:dev": "nodemon src/server.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { - "express": "^4.17.1" + "dotenv": "^10.0.0", + "express": "^4.17.1", + "knex": "^0.95.14", + "pg": "^8.7.1" }, "devDependencies": { "nodemon": "^2.0.6" diff --git a/src/categories/categories.controller.js b/src/categories/categories.controller.js index 54354238..b5585705 100644 --- a/src/categories/categories.controller.js +++ b/src/categories/categories.controller.js @@ -1,13 +1,15 @@ -async function list(req, res, next) { - res.json({ - data: [ - { category_name: "category 1" }, - { category_name: "category 2" }, - { category_name: "category 3" }, - ], - }); -} +const categoriesService = require("./categories.service"); +const asyncErrorBoundary = require("../errors/asyncErrorBoundary"); + +const list = async (req, res, next) => { + try { + const data = await categoriesService.list(); + res.json({ data }); + } catch (error) { + next(error); + } +}; module.exports = { - list: [list], + list: asyncErrorBoundary(list), }; diff --git a/src/categories/categories.service.js b/src/categories/categories.service.js new file mode 100644 index 00000000..b706862e --- /dev/null +++ b/src/categories/categories.service.js @@ -0,0 +1,7 @@ +const knex = require("../db/connection"); + +const list = () => { + return knex("categories").select("*"); +}; + +module.exports = { list }; diff --git a/src/db/connection.js b/src/db/connection.js new file mode 100644 index 00000000..25733ec1 --- /dev/null +++ b/src/db/connection.js @@ -0,0 +1,5 @@ +const env = process.env.NODE_ENV || "development"; +const config = require("../../knexfile")[env]; +const knex = require("knex")(config); + +module.exports = knex; diff --git a/src/db/migrations/20211204211512_createSuppliersTable.js b/src/db/migrations/20211204211512_createSuppliersTable.js new file mode 100644 index 00000000..846b5413 --- /dev/null +++ b/src/db/migrations/20211204211512_createSuppliersTable.js @@ -0,0 +1,23 @@ +exports.up = function (knex) { + return knex.schema.createTable("suppliers", (table) => { + table.increments("supplier_id").primary(); // sets supplier_id as the primary key + table.string("supplier_name"); + table.string("supplier_address_line_1"); + table.string("supplier_address_line_2"); + table.string("supplier_city"); + table.string("supplier_state"); + table.string("supplier_zip"); + table.string("supplier_phone"); + table.string("supplier_email"); + table.text("supplier_notes"); + table.string("supplier_type_of_goods"); + table.timestamps(true, true); // Adds `created_at` and `updated_at` columns; + // passing `true` as the first argument sets the columns to be a timestamp type + // while passing `true` as the second argument sets those columns to be non-nullable + // and to use the current timestamp by default + }); +}; + +exports.down = function (knex) { + return knex.schema.dropTable("suppliers"); +}; diff --git a/src/db/migrations/20211204213838_createProductsTable.js b/src/db/migrations/20211204213838_createProductsTable.js new file mode 100644 index 00000000..d459773a --- /dev/null +++ b/src/db/migrations/20211204213838_createProductsTable.js @@ -0,0 +1,22 @@ +exports.up = function (knex) { + return knex.schema.createTable("products", (table) => { + table.increments("product_id").primary(); // Sets `product_id` as the primary key + table.string("product_sku"); + table.string("product_name"); + table.text("product_description"); + table.integer("product_quantity_in_stock"); + table.decimal("product_weight_in_lbs"); + table.integer("supplier_id").unsigned().notNullable(); + // Links the product to the supoplier table + table + .foreign("supplier_id") + .references("supplier_id") + .inTable("suppliers") + .onDelete("cascade"); // If the supplier is deleted, also deletes all products that are associated with the supplier + table.timestamps(true, true); + }); +}; + +exports.down = function (knex) { + return knex.schema.dropTable("products"); +}; diff --git a/src/db/migrations/20211204214404_createCategoriesTable.js b/src/db/migrations/20211204214404_createCategoriesTable.js new file mode 100644 index 00000000..81345255 --- /dev/null +++ b/src/db/migrations/20211204214404_createCategoriesTable.js @@ -0,0 +1,12 @@ +exports.up = function (knex) { + return knex.schema.createTable("categories", (table) => { + table.increments("category_id").primary(); + table.string("category_name"); + table.text("category_description"); + table.timestamps(true, true); + }); +}; + +exports.down = function (knex) { + return knex.schema.dropTable("categories"); +}; diff --git a/src/db/migrations/20211204214624_createProductsCategoriesTable.js b/src/db/migrations/20211204214624_createProductsCategoriesTable.js new file mode 100644 index 00000000..6f561de2 --- /dev/null +++ b/src/db/migrations/20211204214624_createProductsCategoriesTable.js @@ -0,0 +1,22 @@ +exports.up = function (knex) { + return knex.schema.createTable("products_categories", (table) => { + table.integer("product_id").unsigned().notNullable(); + table + .foreign("product_id") + .references("product_id") + .inTable("products") + .onDelete("CASCADE"); + table.integer("category_id").unsigned().notNullable(); + table + .foreign("category_id") + .references("category_id") + .inTable("categories") + .onDelete("CASCADE"); + + table.timestamps(true, true); + }); +}; + +exports.down = function (knex) { + return knex.schema.dropTable("products_categories"); +}; diff --git a/src/db/migrations/20211204220401_productsAddPriceAndChangeProductNameToProductTitle.js b/src/db/migrations/20211204220401_productsAddPriceAndChangeProductNameToProductTitle.js new file mode 100644 index 00000000..0cac92af --- /dev/null +++ b/src/db/migrations/20211204220401_productsAddPriceAndChangeProductNameToProductTitle.js @@ -0,0 +1,13 @@ +exports.up = function (knex) { + return knex.schema.table("products", (table) => { + table.renameColumn("product_name", "product_title"); + table.decimal("product_price"); // Add a new column + }); +}; + +exports.down = function (knex) { + return knex.schema.table("products", (table) => { + table.renameColumn("product_title", "product_name"); + table.dropColumn("product_price"); + }); +}; diff --git a/src/db/seeds/00-suppliers.js b/src/db/seeds/00-suppliers.js new file mode 100644 index 00000000..53259253 --- /dev/null +++ b/src/db/seeds/00-suppliers.js @@ -0,0 +1,14 @@ +const suppliers = require("../fixtures/suppliers"); + +exports.seed = (knex) => { + // Deletes ALL existing entries + // resets primary keys values + // deletes all associated products to the supplier + // then + // insert key/value pairs from fixtures into the table + return knex + .raw("TRUNCATE TABLE suppliers RESTART IDENTITY CASCADE") + .then(() => { + return knex("suppliers").insert(suppliers); + }); +}; diff --git a/src/db/seeds/01-products.js b/src/db/seeds/01-products.js new file mode 100644 index 00000000..6eb58a08 --- /dev/null +++ b/src/db/seeds/01-products.js @@ -0,0 +1,10 @@ +const products = require("../fixtures/products"); + +exports.seed = (knex) => { + // populates products table with products key/value pairs + return knex + .raw("TRUNCATE TABLE products RESTART IDENTITY CASCADE") + .then(() => { + return knex("products").insert(products); + }); +}; diff --git a/src/db/seeds/02-categories.js b/src/db/seeds/02-categories.js new file mode 100644 index 00000000..c1467cd1 --- /dev/null +++ b/src/db/seeds/02-categories.js @@ -0,0 +1,9 @@ +const categories = require("../fixtures/categories"); + +exports.seed = (knex) => { + return knex + .raw("TRUNCATE TABLE categories RESTART IDENTITY CASCADE") + .then(() => { + return knex("categories").insert(categories); + }); +}; diff --git a/src/db/seeds/03-products_categories.js b/src/db/seeds/03-products_categories.js new file mode 100644 index 00000000..75068900 --- /dev/null +++ b/src/db/seeds/03-products_categories.js @@ -0,0 +1,9 @@ +const productsCategories = require("../fixtures/productsCategories"); + +exports.seed = (knex) => { + return knex + .raw("TRUNCATE TABLE products_categories RESTART IDENTITY CASCADE") + .then(() => { + return knex("products_categories").insert(productsCategories); + }); +}; diff --git a/src/errors/asyncErrorBoundary.js b/src/errors/asyncErrorBoundary.js new file mode 100644 index 00000000..89a89342 --- /dev/null +++ b/src/errors/asyncErrorBoundary.js @@ -0,0 +1,15 @@ +const asyncErrorBoundary = (delegate, defaultStatus) => { + return (request, response, next) => { + Promise.resolve() + .then(() => delegate(request, response, next)) + .catch((error = {}) => { + const { status = defaultStatus, message = error } = error; + next({ + status, + message, + }); + }); + }; +}; + +module.exports = asyncErrorBoundary; diff --git a/src/errors/hasProperties.js b/src/errors/hasProperties.js new file mode 100644 index 00000000..740b5098 --- /dev/null +++ b/src/errors/hasProperties.js @@ -0,0 +1,20 @@ +const hasProperties = (...properties) => { + return (req, res, next) => { + const { data = {} } = req.body; + + try { + properties.forEach((property) => { + if (!data[property]) { + const error = new Error(`A '${property}' property is required.`); + error.status = 400; + throw error; + } + }); + next(); + } catch (error) { + next(error); + } + }; +}; + +module.exports = hasProperties; diff --git a/src/products/products.controller.js b/src/products/products.controller.js index 961703ab..459a7d80 100644 --- a/src/products/products.controller.js +++ b/src/products/products.controller.js @@ -1,14 +1,48 @@ +const productsService = require("./products.service"); +const asyncErrorBoundary = require("../errors/asyncErrorBoundary"); + +// Middlewares ============================================================================================================= + +const productExists = async (req, res, next) => { + const product = await productsService.read(req.params.productId); + + if (product) { + res.locals.product = product; + return next(); + } + + next({ status: 404, message: `Product cannot be found.` }); +}; + +// Resource Queries ==================================================================================== + function read(req, res, next) { - res.json({ data: { product_title: "some product title" } }); + const { product: data } = res.locals; + res.json({ data }); } -function list(req, res, next) { - res.json({ - data: [{ product_title: "product 1" }, { product_title: "product 2" }], - }); -} +const list = async (req, res, next) => { + const data = await productsService.list(); + + res.json({ data }); +}; + +const listOutOfStockCount = async (req, res, next) => { + res.json({ data: await productsService.listOutOfStockCount() }); +}; + +const listPriceSummary = async (req, res, next) => { + res.json({ data: await productsService.listPriceSummary() }); +}; + +const listTotalWeightByProduct = async (req, res, next) => { + res.json({ data: await productsService.listTotalWeightByProduct() }); +}; module.exports = { - read: [read], - list: [list], + list, + read: [asyncErrorBoundary(productExists), read], + listOutOfStockCount: asyncErrorBoundary(listOutOfStockCount), + listPriceSummary: asyncErrorBoundary(listPriceSummary), + listTotalWeightByProduct: asyncErrorBoundary(listTotalWeightByProduct), }; diff --git a/src/products/products.router.js b/src/products/products.router.js index da9e571c..2c20f04b 100644 --- a/src/products/products.router.js +++ b/src/products/products.router.js @@ -3,6 +3,22 @@ const controller = require("./products.controller"); const methodNotAllowed = require("../errors/methodNotAllowed"); router.route("/").get(controller.list).all(methodNotAllowed); -router.route("/:productId").get(controller.read).all(methodNotAllowed); + +router + .route("/out-of-stock-count") + .get(controller.listOutOfStockCount) + .all(methodNotAllowed); + +router + .route("/price-summary") + .get(controller.listPriceSummary) + .all(methodNotAllowed); + +router + .route("/total-weight-by-product") + .get(controller.listTotalWeightByProduct) + .all(methodNotAllowed); + +router.route("/:productId([0-9]+)").get(controller.read).all(methodNotAllowed); module.exports = router; diff --git a/src/products/products.service.js b/src/products/products.service.js new file mode 100644 index 00000000..37b8a41c --- /dev/null +++ b/src/products/products.service.js @@ -0,0 +1,48 @@ +const knex = require("../db/connection"); + +// PRODUCTS - list queries =============================================================== + +const list = () => { + return knex("products").select("*"); +}; + +const listOutOfStockCount = () => { + return knex("products") + .select("product_quantity_in_stock as out_of_stock") + .count("product_id") + .where({ product_quantity_in_stock: 0 }) + .groupBy("out_of_stock"); +}; + +const listPriceSummary = () => { + return knex("products") + .select("supplier_id") + .min("product_price") + .max("product_price") + .avg("product_price") + .groupBy("supplier_id"); +}; + +const listTotalWeightByProduct = () => { + return knex("products") + .select( + "product_sku", + "product_title", + knex.raw( + "sum(product_weight_in_lbs * product_quantity_in_stock) as total_weight_in_lbs" + ) + ) + .groupBy("product_title", "product_sku"); +}; + +const read = (product_id) => { + return knex("products").select("*").where({ product_id }).first(); +}; + +module.exports = { + list, + read, + listOutOfStockCount, + listPriceSummary, + listTotalWeightByProduct, +}; diff --git a/src/suppliers/suppliers.controller.js b/src/suppliers/suppliers.controller.js index 51448dbc..b2b4b9a4 100644 --- a/src/suppliers/suppliers.controller.js +++ b/src/suppliers/suppliers.controller.js @@ -1,17 +1,91 @@ -async function create(req, res, next) { - res.status(201).json({ data: { supplier_name: "new supplier" } }); -} +const suppliersService = require("./suppliers.service.js"); +const hasProperties = require("../errors/hasProperties"); +const asyncErrorBoundary = require("../errors/asyncErrorBoundary"); -async function update(req, res, next) { - res.json({ data: { supplier_name: "updated supplier" } }); -} +// Middlewares ============================================================================================================= + +const VALID_PROPERTIES = [ + "supplier_name", + "supplier_address_line_1", + "supplier_address_line_2", + "supplier_city", + "supplier_state", + "supplier_zip", + "supplier_phone", + "supplier_email", + "supplier_notes", + "supplier_type_of_goods", +]; + +const hasRequiredProperties = hasProperties("supplier_name", "supplier_email"); + +// Check for valid field properties +const hasValidProperties = (req, res, next) => { + const { data = {} } = req.body; + + const invalidFields = Object.keys(data).filter( + (property) => !VALID_PROPERTIES.includes(property) + ); + + if (invalidFields.length) { + return next({ + status: 400, + message: `Invalid field(s): ${invalidFields.join(", ")}`, + }); + } + next(); +}; + +const supplierExists = async (req, res, next) => { + const supplier = await suppliersService.read(req.params.supplierId); + + if (supplier) { + res.locals.supplier = supplier; + return next(); + } + next({ status: 404, message: `Supplier cannot be found.` }); +}; + +// Resource Queries ==================================================================================== + +const create = async (req, res, next) => { + const data = await suppliersService.create(req.body.data); + + res.status(201).json({ data }); +}; + +const update = async (req, res, next) => { + const updatedSupplier = { + ...req.body.data, + supplier_id: res.locals.supplier.supplier_id, + }; + + console.log("updatedSupplier:", updatedSupplier); + + const data = await suppliersService.update(updatedSupplier); + + res.json({ data }); +}; + +const destroy = async (req, res, next) => { + const { supplier } = res.locals; + + await suppliersService.destroy(supplier.supplier_id); -async function destroy(req, res, next) { res.sendStatus(204); -} +}; module.exports = { - create, - update, - delete: destroy, + create: [ + hasValidProperties, + hasRequiredProperties, + asyncErrorBoundary(create), + ], + update: [ + asyncErrorBoundary(supplierExists), + hasValidProperties, + hasRequiredProperties, + asyncErrorBoundary(update), + ], + delete: [asyncErrorBoundary(supplierExists), asyncErrorBoundary(destroy)], }; diff --git a/src/suppliers/suppliers.service.js b/src/suppliers/suppliers.service.js new file mode 100644 index 00000000..5ade9e7b --- /dev/null +++ b/src/suppliers/suppliers.service.js @@ -0,0 +1,30 @@ +const knex = require("../db/connection"); + +const create = (supplier) => { + return knex("suppliers") + .insert(supplier) + .returning("*") + .then((createdRecords) => createdRecords[0]); +}; + +const read = (supplier_id) => { + return knex("suppliers").select("*").where({ supplier_id }).first(); +}; + +const update = (updatedSupplier) => { + return knex("suppliers") + .select("*") + .where({ supplier_id: updatedSupplier.supplier_id }) + .update(updatedSupplier, "*"); +}; + +const destroy = (supplier_id) => { + return knex("suppliers").select("*").where({ supplier_id }).del(); +}; + +module.exports = { + create, + read, + update, + destroy, +};