From 6d992ca88534c22847712b4be52332885ea26f54 Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Sun, 29 Nov 2020 17:00:35 -1000
Subject: [PATCH 01/35] create database configuration

---
 knexfile.js       |    6 +
 package-lock.json | 1401 ++++++++++++++++++++++++++++++++++++++++++++-
 package.json      |    4 +-
 3 files changed, 1405 insertions(+), 6 deletions(-)
 create mode 100644 knexfile.js

diff --git a/knexfile.js b/knexfile.js
new file mode 100644
index 00000000..3682fb66
--- /dev/null
+++ b/knexfile.js
@@ -0,0 +1,6 @@
+module.exports = {
+  development: {
+    client: "postgresql",
+    connection: "",
+  },
+};
diff --git a/package-lock.json b/package-lock.json
index 1e3d92c1..5d4c9c50 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -81,17 +81,107 @@
         "picomatch": "^2.0.4"
       }
     },
+    "arr-diff": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+      "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA="
+    },
+    "arr-flatten": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+      "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg=="
+    },
+    "arr-union": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+      "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ="
+    },
+    "array-each": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
+      "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8="
+    },
     "array-flatten": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
       "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
     },
+    "array-slice": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
+      "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w=="
+    },
+    "array-unique": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+      "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg="
+    },
+    "assign-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+      "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c="
+    },
+    "atob": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+      "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
+    },
     "balanced-match": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
       "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
       "dev": true
     },
+    "base": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+      "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+      "requires": {
+        "cache-base": "^1.0.1",
+        "class-utils": "^0.3.5",
+        "component-emitter": "^1.2.1",
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.1",
+        "mixin-deep": "^1.2.0",
+        "pascalcase": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
     "binary-extensions": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
@@ -141,11 +231,59 @@
         "concat-map": "0.0.1"
       }
     },
+    "braces": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+      "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+      "requires": {
+        "arr-flatten": "^1.1.0",
+        "array-unique": "^0.3.2",
+        "extend-shallow": "^2.0.1",
+        "fill-range": "^4.0.0",
+        "isobject": "^3.0.1",
+        "repeat-element": "^1.1.2",
+        "snapdragon": "^0.8.1",
+        "snapdragon-node": "^2.0.1",
+        "split-string": "^3.0.2",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "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",
       "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
     },
+    "cache-base": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+      "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+      "requires": {
+        "collection-visit": "^1.0.0",
+        "component-emitter": "^1.2.1",
+        "get-value": "^2.0.6",
+        "has-value": "^1.0.0",
+        "isobject": "^3.0.1",
+        "set-value": "^2.0.0",
+        "to-object-path": "^0.3.0",
+        "union-value": "^1.0.0",
+        "unset-value": "^1.0.0"
+      }
+    },
     "cacheable-request": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
@@ -268,6 +406,27 @@
       "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
       "dev": true
     },
+    "class-utils": {
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+      "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+      "requires": {
+        "arr-union": "^3.1.0",
+        "define-property": "^0.2.5",
+        "isobject": "^3.0.0",
+        "static-extend": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
     "cli-boxes": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
@@ -283,6 +442,15 @@
         "mimic-response": "^1.0.0"
       }
     },
+    "collection-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+      "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+      "requires": {
+        "map-visit": "^1.0.0",
+        "object-visit": "^1.0.0"
+      }
+    },
     "color-convert": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -298,6 +466,21 @@
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
       "dev": true
     },
+    "colorette": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
+      "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw=="
+    },
+    "commander": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
+      "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="
+    },
+    "component-emitter": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
+      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
+    },
     "concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -341,6 +524,11 @@
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
       "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
     },
+    "copy-descriptor": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+      "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
+    },
     "crypto-random-string": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -355,6 +543,11 @@
         "ms": "2.0.0"
       }
     },
+    "decode-uri-component": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+      "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
+    },
     "decompress-response": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
@@ -376,6 +569,43 @@
       "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
       "dev": true
     },
+    "define-property": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+      "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+      "requires": {
+        "is-descriptor": "^1.0.2",
+        "isobject": "^3.0.1"
+      },
+      "dependencies": {
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
     "depd": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -386,6 +616,11 @@
       "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
       "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
     },
+    "detect-file": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+      "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc="
+    },
     "dot-prop": {
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
@@ -437,11 +672,56 @@
       "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",
       "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
     },
+    "expand-brackets": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+      "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+      "requires": {
+        "debug": "^2.3.3",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "posix-character-classes": "^0.1.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "expand-tilde": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+      "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=",
+      "requires": {
+        "homedir-polyfill": "^1.0.1"
+      }
+    },
     "express": {
       "version": "4.17.1",
       "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
@@ -479,6 +759,110 @@
         "vary": "~1.1.2"
       }
     },
+    "extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
+    },
+    "extend-shallow": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+      "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+      "requires": {
+        "assign-symbols": "^1.0.0",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "extglob": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+      "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+      "requires": {
+        "array-unique": "^0.3.2",
+        "define-property": "^1.0.0",
+        "expand-brackets": "^2.1.4",
+        "extend-shallow": "^2.0.1",
+        "fragment-cache": "^0.2.1",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "fill-range": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+      "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1",
+        "to-regex-range": "^2.1.0"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
     "finalhandler": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
@@ -493,11 +877,60 @@
         "unpipe": "~1.0.0"
       }
     },
+    "findup-sync": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz",
+      "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==",
+      "requires": {
+        "detect-file": "^1.0.0",
+        "is-glob": "^4.0.0",
+        "micromatch": "^3.0.4",
+        "resolve-dir": "^1.0.1"
+      }
+    },
+    "fined": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz",
+      "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==",
+      "requires": {
+        "expand-tilde": "^2.0.2",
+        "is-plain-object": "^2.0.3",
+        "object.defaults": "^1.1.0",
+        "object.pick": "^1.2.0",
+        "parse-filepath": "^1.0.1"
+      }
+    },
+    "flagged-respawn": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
+      "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q=="
+    },
+    "for-in": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+      "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA="
+    },
+    "for-own": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+      "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
+      "requires": {
+        "for-in": "^1.0.1"
+      }
+    },
     "forwarded": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
       "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
     },
+    "fragment-cache": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+      "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+      "requires": {
+        "map-cache": "^0.2.2"
+      }
+    },
     "fresh": {
       "version": "0.5.2",
       "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -510,6 +943,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 +957,16 @@
         "pump": "^3.0.0"
       }
     },
+    "get-value": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+      "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg="
+    },
+    "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",
@@ -537,6 +985,28 @@
         "ini": "^1.3.5"
       }
     },
+    "global-modules": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+      "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+      "requires": {
+        "global-prefix": "^1.0.1",
+        "is-windows": "^1.0.1",
+        "resolve-dir": "^1.0.0"
+      }
+    },
+    "global-prefix": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+      "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+      "requires": {
+        "expand-tilde": "^2.0.2",
+        "homedir-polyfill": "^1.0.1",
+        "ini": "^1.3.4",
+        "is-windows": "^1.0.1",
+        "which": "^1.2.14"
+      }
+    },
     "got": {
       "version": "9.6.0",
       "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
@@ -562,18 +1032,63 @@
       "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",
       "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
       "dev": true
     },
+    "has-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+      "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+      "requires": {
+        "get-value": "^2.0.6",
+        "has-values": "^1.0.0",
+        "isobject": "^3.0.0"
+      }
+    },
+    "has-values": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+      "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+      "requires": {
+        "is-number": "^3.0.0",
+        "kind-of": "^4.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+          "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
     "has-yarn": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
       "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
       "dev": true
     },
+    "homedir-polyfill": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+      "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+      "requires": {
+        "parse-passwd": "^1.0.0"
+      }
+    },
     "http-cache-semantics": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
@@ -626,14 +1141,45 @@
     "ini": {
       "version": "1.3.5",
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
-      "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
-      "dev": true
+      "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
+    },
+    "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",
       "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
     },
+    "is-absolute": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
+      "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
+      "requires": {
+        "is-relative": "^1.0.0",
+        "is-windows": "^1.0.1"
+      }
+    },
+    "is-accessor-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+      "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
     "is-binary-path": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -643,6 +1189,11 @@
         "binary-extensions": "^2.0.0"
       }
     },
+    "is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+    },
     "is-ci": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
@@ -652,11 +1203,58 @@
         "ci-info": "^2.0.0"
       }
     },
+    "is-core-module": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz",
+      "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==",
+      "requires": {
+        "has": "^1.0.3"
+      }
+    },
+    "is-data-descriptor": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+      "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "requires": {
+        "is-accessor-descriptor": "^0.1.6",
+        "is-data-descriptor": "^0.1.4",
+        "kind-of": "^5.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+          "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw=="
+        }
+      }
+    },
+    "is-extendable": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik="
+    },
     "is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-      "dev": true
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
     },
     "is-fullwidth-code-point": {
       "version": "2.0.0",
@@ -668,7 +1266,6 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
       "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
-      "dev": true,
       "requires": {
         "is-extglob": "^2.1.1"
       }
@@ -689,6 +1286,24 @@
       "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==",
       "dev": true
     },
+    "is-number": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+      "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
     "is-obj": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
@@ -701,18 +1316,62 @@
       "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==",
       "dev": true
     },
+    "is-plain-object": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+      "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
+    "is-relative": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
+      "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
+      "requires": {
+        "is-unc-path": "^1.0.0"
+      }
+    },
     "is-typedarray": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
       "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
       "dev": true
     },
+    "is-unc-path": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
+      "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
+      "requires": {
+        "unc-path-regex": "^0.1.2"
+      }
+    },
+    "is-windows": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+      "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="
+    },
     "is-yarn-global": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
       "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==",
       "dev": true
     },
+    "isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
+    },
+    "isobject": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+      "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+    },
     "json-buffer": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
@@ -728,6 +1387,45 @@
         "json-buffer": "3.0.0"
       }
     },
+    "kind-of": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
+    },
+    "knex": {
+      "version": "0.21.12",
+      "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.12.tgz",
+      "integrity": "sha512-AEyyiTM9p/x/Pb38TPZkvphKPmn8UWxP7MdIphzjAOielOfFFeU6pjP6y3M7UJ7rxrQsCrAYHwdonLQ3l1JCDw==",
+      "requires": {
+        "colorette": "1.2.1",
+        "commander": "^5.1.0",
+        "debug": "4.1.1",
+        "esm": "^3.2.25",
+        "getopts": "2.2.5",
+        "interpret": "^2.2.0",
+        "liftoff": "3.1.0",
+        "lodash": "^4.17.20",
+        "pg-connection-string": "2.3.0",
+        "tarn": "^3.0.1",
+        "tildify": "2.0.0",
+        "v8flags": "^3.2.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "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 +1435,26 @@
         "package-json": "^6.3.0"
       }
     },
+    "liftoff": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz",
+      "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==",
+      "requires": {
+        "extend": "^3.0.0",
+        "findup-sync": "^3.0.0",
+        "fined": "^1.0.1",
+        "flagged-respawn": "^1.0.0",
+        "is-plain-object": "^2.0.4",
+        "object.map": "^1.0.0",
+        "rechoir": "^0.6.2",
+        "resolve": "^1.1.7"
+      }
+    },
+    "lodash": {
+      "version": "4.17.20",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
+      "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
+    },
     "lowercase-keys": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
@@ -760,6 +1478,27 @@
         }
       }
     },
+    "make-iterator": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
+      "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==",
+      "requires": {
+        "kind-of": "^6.0.2"
+      }
+    },
+    "map-cache": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+      "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8="
+    },
+    "map-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+      "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+      "requires": {
+        "object-visit": "^1.0.0"
+      }
+    },
     "media-typer": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -775,6 +1514,26 @@
       "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
       "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
     },
+    "micromatch": {
+      "version": "3.1.10",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+      "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "braces": "^2.3.1",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "extglob": "^2.0.4",
+        "fragment-cache": "^0.2.1",
+        "kind-of": "^6.0.2",
+        "nanomatch": "^1.2.9",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.2"
+      }
+    },
     "mime": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
@@ -814,11 +1573,48 @@
       "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
       "dev": true
     },
+    "mixin-deep": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+      "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
+      "requires": {
+        "for-in": "^1.0.2",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
     "ms": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
       "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
     },
+    "nanomatch": {
+      "version": "1.2.13",
+      "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+      "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "fragment-cache": "^0.2.1",
+        "is-windows": "^1.0.2",
+        "kind-of": "^6.0.2",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      }
+    },
     "negotiator": {
       "version": "0.6.2",
       "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
@@ -880,6 +1676,70 @@
       "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==",
       "dev": true
     },
+    "object-copy": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+      "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+      "requires": {
+        "copy-descriptor": "^0.1.0",
+        "define-property": "^0.2.5",
+        "kind-of": "^3.0.3"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "object-visit": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+      "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+      "requires": {
+        "isobject": "^3.0.0"
+      }
+    },
+    "object.defaults": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz",
+      "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=",
+      "requires": {
+        "array-each": "^1.0.1",
+        "array-slice": "^1.0.0",
+        "for-own": "^1.0.0",
+        "isobject": "^3.0.0"
+      }
+    },
+    "object.map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
+      "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=",
+      "requires": {
+        "for-own": "^1.0.0",
+        "make-iterator": "^1.0.0"
+      }
+    },
+    "object.pick": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+      "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
     "on-finished": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@@ -923,22 +1783,154 @@
         }
       }
     },
+    "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=="
+    },
+    "parse-filepath": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
+      "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=",
+      "requires": {
+        "is-absolute": "^1.0.0",
+        "map-cache": "^0.2.0",
+        "path-root": "^0.1.1"
+      }
+    },
+    "parse-passwd": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+      "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY="
+    },
     "parseurl": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
       "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
     },
+    "pascalcase": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+      "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ="
+    },
+    "path-parse": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
+    },
+    "path-root": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz",
+      "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=",
+      "requires": {
+        "path-root-regex": "^0.1.0"
+      }
+    },
+    "path-root-regex": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz",
+      "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0="
+    },
     "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.5.1",
+      "resolved": "https://registry.npmjs.org/pg/-/pg-8.5.1.tgz",
+      "integrity": "sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw==",
+      "requires": {
+        "buffer-writer": "2.0.0",
+        "packet-reader": "1.0.0",
+        "pg-connection-string": "^2.4.0",
+        "pg-pool": "^3.2.2",
+        "pg-protocol": "^1.4.0",
+        "pg-types": "^2.1.0",
+        "pgpass": "1.x"
+      },
+      "dependencies": {
+        "pg-connection-string": {
+          "version": "2.4.0",
+          "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz",
+          "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ=="
+        }
+      }
+    },
+    "pg-connection-string": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.3.0.tgz",
+      "integrity": "sha512-ukMTJXLI7/hZIwTW7hGMZJ0Lj0S2XQBCJ4Shv4y1zgQ/vqVea+FLhzywvPj0ujSuofu+yA4MYHGZPTsgjBgJ+w=="
+    },
+    "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.2.2",
+      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.2.tgz",
+      "integrity": "sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA=="
+    },
+    "pg-protocol": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.4.0.tgz",
+      "integrity": "sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA=="
+    },
+    "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
     },
+    "posix-character-classes": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+      "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs="
+    },
+    "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 +2004,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 +2023,23 @@
         "picomatch": "^2.2.1"
       }
     },
+    "rechoir": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
+      "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
+      "requires": {
+        "resolve": "^1.1.6"
+      }
+    },
+    "regex-not": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+      "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+      "requires": {
+        "extend-shallow": "^3.0.2",
+        "safe-regex": "^1.1.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 +2058,39 @@
         "rc": "^1.2.8"
       }
     },
+    "repeat-element": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
+      "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g=="
+    },
+    "repeat-string": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
+    },
+    "resolve": {
+      "version": "1.19.0",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
+      "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
+      "requires": {
+        "is-core-module": "^2.1.0",
+        "path-parse": "^1.0.6"
+      }
+    },
+    "resolve-dir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+      "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=",
+      "requires": {
+        "expand-tilde": "^2.0.0",
+        "global-modules": "^1.0.0"
+      }
+    },
+    "resolve-url": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+      "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo="
+    },
     "responselike": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
@@ -1048,11 +2100,24 @@
         "lowercase-keys": "^1.0.0"
       }
     },
+    "ret": {
+      "version": "0.1.15",
+      "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+      "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
+    },
     "safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
       "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
     },
+    "safe-regex": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+      "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+      "requires": {
+        "ret": "~0.1.10"
+      }
+    },
     "safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -1119,6 +2184,27 @@
         "send": "0.17.1"
       }
     },
+    "set-value": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+      "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-extendable": "^0.1.1",
+        "is-plain-object": "^2.0.3",
+        "split-string": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
     "setprototypeof": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
@@ -1130,6 +2216,160 @@
       "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
       "dev": true
     },
+    "snapdragon": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+      "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+      "requires": {
+        "base": "^0.11.1",
+        "debug": "^2.2.0",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "map-cache": "^0.2.2",
+        "source-map": "^0.5.6",
+        "source-map-resolve": "^0.5.0",
+        "use": "^3.1.0"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "snapdragon-node": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+      "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+      "requires": {
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.0",
+        "snapdragon-util": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "snapdragon-util": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+      "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+      "requires": {
+        "kind-of": "^3.2.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
+    },
+    "source-map-resolve": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
+      "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
+      "requires": {
+        "atob": "^2.1.2",
+        "decode-uri-component": "^0.2.0",
+        "resolve-url": "^0.2.1",
+        "source-map-url": "^0.4.0",
+        "urix": "^0.1.0"
+      }
+    },
+    "source-map-url": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
+      "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM="
+    },
+    "split-string": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+      "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+      "requires": {
+        "extend-shallow": "^3.0.0"
+      }
+    },
+    "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"
+      }
+    },
+    "static-extend": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+      "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+      "requires": {
+        "define-property": "^0.2.5",
+        "object-copy": "^0.1.0"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
     "statuses": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@@ -1175,6 +2415,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,18 +2454,66 @@
         "has-flag": "^3.0.0"
       }
     },
+    "tarn": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz",
+      "integrity": "sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw=="
+    },
     "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-object-path": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+      "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
     "to-readable-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
       "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==",
       "dev": true
     },
+    "to-regex": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+      "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+      "requires": {
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "regex-not": "^1.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "to-regex-range": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+      "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+      "requires": {
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1"
+      }
+    },
     "toidentifier": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
@@ -1249,6 +2552,11 @@
         "is-typedarray": "^1.0.0"
       }
     },
+    "unc-path-regex": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
+      "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo="
+    },
     "undefsafe": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz",
@@ -1258,6 +2566,17 @@
         "debug": "^2.2.0"
       }
     },
+    "union-value": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+      "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
+      "requires": {
+        "arr-union": "^3.1.0",
+        "get-value": "^2.0.6",
+        "is-extendable": "^0.1.1",
+        "set-value": "^2.0.1"
+      }
+    },
     "unique-string": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
@@ -1272,6 +2591,42 @@
       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
       "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
     },
+    "unset-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+      "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+      "requires": {
+        "has-value": "^0.3.1",
+        "isobject": "^3.0.0"
+      },
+      "dependencies": {
+        "has-value": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+          "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+          "requires": {
+            "get-value": "^2.0.3",
+            "has-values": "^0.1.4",
+            "isobject": "^2.0.0"
+          },
+          "dependencies": {
+            "isobject": {
+              "version": "2.1.0",
+              "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+              "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+              "requires": {
+                "isarray": "1.0.0"
+              }
+            }
+          }
+        },
+        "has-values": {
+          "version": "0.1.4",
+          "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+          "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E="
+        }
+      }
+    },
     "update-notifier": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz",
@@ -1293,6 +2648,11 @@
         "xdg-basedir": "^4.0.0"
       }
     },
+    "urix": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+      "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
+    },
     "url-parse-lax": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
@@ -1302,16 +2662,42 @@
         "prepend-http": "^2.0.0"
       }
     },
+    "use": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+      "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
+    },
+    "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",
       "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
     },
+    "v8flags": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz",
+      "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==",
+      "requires": {
+        "homedir-polyfill": "^1.0.1"
+      }
+    },
     "vary": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
       "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
     },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
     "widest-line": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
@@ -1344,6 +2730,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..28b17976 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,9 @@
   "author": "",
   "license": "ISC",
   "dependencies": {
-    "express": "^4.17.1"
+    "express": "^4.17.1",
+    "knex": "^0.21.12",
+    "pg": "^8.5.1"
   },
   "devDependencies": {
     "nodemon": "^2.0.6"

From 82f5e069e47ca0640a0fcb99892a58e75774fa6a Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Sun, 29 Nov 2020 19:38:36 -1000
Subject: [PATCH 02/35] initialize a Knex instance

---
 knexfile.js          | 5 ++++-
 package-lock.json    | 5 +++++
 package.json         | 1 +
 src/db/connection.js | 5 +++++
 4 files changed, 15 insertions(+), 1 deletion(-)
 create mode 100644 src/db/connection.js

diff --git a/knexfile.js b/knexfile.js
index 3682fb66..19feb34f 100644
--- a/knexfile.js
+++ b/knexfile.js
@@ -1,6 +1,9 @@
+require("dotenv").config();
+const { DATABASE_URL } = process.env;
+
 module.exports = {
   development: {
     client: "postgresql",
-    connection: "",
+    connection: DATABASE_URL,
   },
 };
diff --git a/package-lock.json b/package-lock.json
index 5d4c9c50..e5d459b1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -630,6 +630,11 @@
         "is-obj": "^2.0.0"
       }
     },
+    "dotenv": {
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
+      "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
+    },
     "duplexer3": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
diff --git a/package.json b/package.json
index 28b17976..53803528 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
   "author": "",
   "license": "ISC",
   "dependencies": {
+    "dotenv": "^8.2.0",
     "express": "^4.17.1",
     "knex": "^0.21.12",
     "pg": "^8.5.1"
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;

From 1e74382c1321c79af85145733f89717cf9834350 Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Mon, 30 Nov 2020 09:28:06 -1000
Subject: [PATCH 03/35] perform database migrations

---
 knexfile.js                                   |  4 ++++
 .../20201130084659_createSuppliersTable.js    | 20 ++++++++++++++++
 .../20201130084710_createProductsTable.js     | 23 +++++++++++++++++++
 .../20201130084715_createCategoriesTable.js   | 12 ++++++++++
 ...130084720_createProductsCategoriesTable.js | 18 +++++++++++++++
 ...1_renameProductNameColumnToProductTitle.js | 11 +++++++++
 6 files changed, 88 insertions(+)
 create mode 100644 src/db/migrations/20201130084659_createSuppliersTable.js
 create mode 100644 src/db/migrations/20201130084710_createProductsTable.js
 create mode 100644 src/db/migrations/20201130084715_createCategoriesTable.js
 create mode 100644 src/db/migrations/20201130084720_createProductsCategoriesTable.js
 create mode 100644 src/db/migrations/20201130091021_renameProductNameColumnToProductTitle.js

diff --git a/knexfile.js b/knexfile.js
index 19feb34f..f070fbbc 100644
--- a/knexfile.js
+++ b/knexfile.js
@@ -1,3 +1,4 @@
+const path = require("path");
 require("dotenv").config();
 const { DATABASE_URL } = process.env;
 
@@ -5,5 +6,8 @@ module.exports = {
   development: {
     client: "postgresql",
     connection: DATABASE_URL,
+    migrations: {
+      directory: path.join(__dirname, "src", "db", "migrations"),
+    },
   },
 };
diff --git a/src/db/migrations/20201130084659_createSuppliersTable.js b/src/db/migrations/20201130084659_createSuppliersTable.js
new file mode 100644
index 00000000..fdaa88d5
--- /dev/null
+++ b/src/db/migrations/20201130084659_createSuppliersTable.js
@@ -0,0 +1,20 @@
+exports.up = function (knex) {
+  return knex.schema.createTable("suppliers", table => {
+    table.increments("supplier_id").primary();
+    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);
+  });
+};
+
+exports.down = function (knex) {
+  return knex.schema.dropTable("suppliers");
+};
diff --git a/src/db/migrations/20201130084710_createProductsTable.js b/src/db/migrations/20201130084710_createProductsTable.js
new file mode 100644
index 00000000..74c11c15
--- /dev/null
+++ b/src/db/migrations/20201130084710_createProductsTable.js
@@ -0,0 +1,23 @@
+exports.up = function (knex) {
+  return knex.schema.createTable("products", table => {
+    table.increments("product_id").primary();
+    table.string("product_sku");
+    table.string("product_name");
+    table.text("product_description");
+    table.decimal("product_price");
+    table.integer("product_quantity_in_stock");
+    table.decimal("product_weight_in_lbs");
+
+    table.integer("supplier_id").unsigned().notNullable();
+    table
+      .foreign("supplier_id")
+      .references("supplier_id")
+      .inTable("suppliers")
+      .onDelete("cascade");
+    table.timestamps(true, true);
+  });
+};
+
+exports.down = function (knex) {
+  return knex.schema.dropTable("products");
+};
diff --git a/src/db/migrations/20201130084715_createCategoriesTable.js b/src/db/migrations/20201130084715_createCategoriesTable.js
new file mode 100644
index 00000000..5b4880dc
--- /dev/null
+++ b/src/db/migrations/20201130084715_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/20201130084720_createProductsCategoriesTable.js b/src/db/migrations/20201130084720_createProductsCategoriesTable.js
new file mode 100644
index 00000000..c11cd276
--- /dev/null
+++ b/src/db/migrations/20201130084720_createProductsCategoriesTable.js
@@ -0,0 +1,18 @@
+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");
+
+    table.integer("category_id").unsigned().notNullable();
+    table
+      .foreign("category_id")
+      .references("category_id")
+      .inTable("categories");
+
+    table.timestamps(true, true);
+  });
+};
+
+exports.down = function (knex) {
+  return knex.schema.dropTable("products_categories");
+};
diff --git a/src/db/migrations/20201130091021_renameProductNameColumnToProductTitle.js b/src/db/migrations/20201130091021_renameProductNameColumnToProductTitle.js
new file mode 100644
index 00000000..4df481c9
--- /dev/null
+++ b/src/db/migrations/20201130091021_renameProductNameColumnToProductTitle.js
@@ -0,0 +1,11 @@
+exports.up = function (knex) {
+  return knex.schema.table("products", table => {
+    table.renameColumn("product_name", "product_title");
+  });
+};
+
+exports.down = function (knex) {
+  return knex.schema.table("products", table => {
+    table.renameColumn("product_title", "product_name");
+  });
+};

From 00d3dab0c8f08c0123a700b0d948022d7fae8909 Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Tue, 1 Dec 2020 10:40:13 -1000
Subject: [PATCH 04/35] cascade deletes in migrations

---
 .../20201130084720_createProductsCategoriesTable.js      | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/src/db/migrations/20201130084720_createProductsCategoriesTable.js b/src/db/migrations/20201130084720_createProductsCategoriesTable.js
index c11cd276..e59da4fe 100644
--- a/src/db/migrations/20201130084720_createProductsCategoriesTable.js
+++ b/src/db/migrations/20201130084720_createProductsCategoriesTable.js
@@ -1,13 +1,18 @@
 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");
+    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");
+      .inTable("categories")
+      .onDelete("CASCADE");
 
     table.timestamps(true, true);
   });

From 4b482748193e3bde9b8798c3a0977831411eedd9 Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Tue, 2 Mar 2021 22:50:54 -0700
Subject: [PATCH 05/35] feat(server.js): add automatic migration

---
 src/server.js | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/src/server.js b/src/server.js
index dd5a5679..962b90e8 100644
--- a/src/server.js
+++ b/src/server.js
@@ -1,5 +1,17 @@
 const { PORT = 5000 } = process.env;
 
 const app = require("./app");
+const knex = require("./db/connection");
+
 const listener = () => console.log(`Listening on Port ${PORT}!`);
-app.listen(PORT, listener);
+
+knex.migrate
+  .latest()
+  .then((migrations) => {
+    console.log("migrations", migrations);
+    app.listen(PORT, listener);
+  })
+  .catch((error) => {
+    console.error(error);
+    knex.destroy();
+  });

From 6df3b1b32f797a410dbdf9b83da37b3d83fb4a98 Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Sun, 7 Mar 2021 22:02:19 -0700
Subject: [PATCH 06/35] feat: include dropColumn

---
 src/db/migrations/20201130084710_createProductsTable.js         | 2 --
 ...91021_productsAddPriceAndChangeProductNameToProductTitle.js} | 2 ++
 2 files changed, 2 insertions(+), 2 deletions(-)
 rename src/db/migrations/{20201130091021_renameProductNameColumnToProductTitle.js => 20201130091021_productsAddPriceAndChangeProductNameToProductTitle.js} (75%)

diff --git a/src/db/migrations/20201130084710_createProductsTable.js b/src/db/migrations/20201130084710_createProductsTable.js
index 74c11c15..a5522894 100644
--- a/src/db/migrations/20201130084710_createProductsTable.js
+++ b/src/db/migrations/20201130084710_createProductsTable.js
@@ -4,10 +4,8 @@ exports.up = function (knex) {
     table.string("product_sku");
     table.string("product_name");
     table.text("product_description");
-    table.decimal("product_price");
     table.integer("product_quantity_in_stock");
     table.decimal("product_weight_in_lbs");
-
     table.integer("supplier_id").unsigned().notNullable();
     table
       .foreign("supplier_id")
diff --git a/src/db/migrations/20201130091021_renameProductNameColumnToProductTitle.js b/src/db/migrations/20201130091021_productsAddPriceAndChangeProductNameToProductTitle.js
similarity index 75%
rename from src/db/migrations/20201130091021_renameProductNameColumnToProductTitle.js
rename to src/db/migrations/20201130091021_productsAddPriceAndChangeProductNameToProductTitle.js
index 4df481c9..92e1f079 100644
--- a/src/db/migrations/20201130091021_renameProductNameColumnToProductTitle.js
+++ b/src/db/migrations/20201130091021_productsAddPriceAndChangeProductNameToProductTitle.js
@@ -1,11 +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");
   });
 };

From 6f0d104030916e9d27698a724027d0ea2ac453d1 Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Wed, 14 Apr 2021 11:10:12 -0500
Subject: [PATCH 07/35] style: run prettier on migrations

---
 src/db/migrations/20201130084659_createSuppliersTable.js    | 2 +-
 src/db/migrations/20201130084710_createProductsTable.js     | 2 +-
 src/db/migrations/20201130084715_createCategoriesTable.js   | 2 +-
 .../20201130084720_createProductsCategoriesTable.js         | 2 +-
 ...21_productsAddPriceAndChangeProductNameToProductTitle.js | 6 +++---
 5 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/db/migrations/20201130084659_createSuppliersTable.js b/src/db/migrations/20201130084659_createSuppliersTable.js
index fdaa88d5..0180b60a 100644
--- a/src/db/migrations/20201130084659_createSuppliersTable.js
+++ b/src/db/migrations/20201130084659_createSuppliersTable.js
@@ -1,5 +1,5 @@
 exports.up = function (knex) {
-  return knex.schema.createTable("suppliers", table => {
+  return knex.schema.createTable("suppliers", (table) => {
     table.increments("supplier_id").primary();
     table.string("supplier_name");
     table.string("supplier_address_line_1");
diff --git a/src/db/migrations/20201130084710_createProductsTable.js b/src/db/migrations/20201130084710_createProductsTable.js
index a5522894..b16c2977 100644
--- a/src/db/migrations/20201130084710_createProductsTable.js
+++ b/src/db/migrations/20201130084710_createProductsTable.js
@@ -1,5 +1,5 @@
 exports.up = function (knex) {
-  return knex.schema.createTable("products", table => {
+  return knex.schema.createTable("products", (table) => {
     table.increments("product_id").primary();
     table.string("product_sku");
     table.string("product_name");
diff --git a/src/db/migrations/20201130084715_createCategoriesTable.js b/src/db/migrations/20201130084715_createCategoriesTable.js
index 5b4880dc..81345255 100644
--- a/src/db/migrations/20201130084715_createCategoriesTable.js
+++ b/src/db/migrations/20201130084715_createCategoriesTable.js
@@ -1,5 +1,5 @@
 exports.up = function (knex) {
-  return knex.schema.createTable("categories", table => {
+  return knex.schema.createTable("categories", (table) => {
     table.increments("category_id").primary();
     table.string("category_name");
     table.text("category_description");
diff --git a/src/db/migrations/20201130084720_createProductsCategoriesTable.js b/src/db/migrations/20201130084720_createProductsCategoriesTable.js
index e59da4fe..77954598 100644
--- a/src/db/migrations/20201130084720_createProductsCategoriesTable.js
+++ b/src/db/migrations/20201130084720_createProductsCategoriesTable.js
@@ -1,5 +1,5 @@
 exports.up = function (knex) {
-  return knex.schema.createTable("products_categories", table => {
+  return knex.schema.createTable("products_categories", (table) => {
     table.integer("product_id").unsigned().notNullable();
     table
       .foreign("product_id")
diff --git a/src/db/migrations/20201130091021_productsAddPriceAndChangeProductNameToProductTitle.js b/src/db/migrations/20201130091021_productsAddPriceAndChangeProductNameToProductTitle.js
index 92e1f079..0cac92af 100644
--- a/src/db/migrations/20201130091021_productsAddPriceAndChangeProductNameToProductTitle.js
+++ b/src/db/migrations/20201130091021_productsAddPriceAndChangeProductNameToProductTitle.js
@@ -1,12 +1,12 @@
 exports.up = function (knex) {
-  return knex.schema.table("products", table => {
+  return knex.schema.table("products", (table) => {
     table.renameColumn("product_name", "product_title");
-    table.decimal("product_price");  // Add a new column
+    table.decimal("product_price"); // Add a new column
   });
 };
 
 exports.down = function (knex) {
-  return knex.schema.table("products", table => {
+  return knex.schema.table("products", (table) => {
     table.renameColumn("product_title", "product_name");
     table.dropColumn("product_price");
   });

From bd0db3e68a75b0ac545977d242e1d0a379330c7d Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Mon, 30 Nov 2020 11:14:20 -1000
Subject: [PATCH 08/35] add seed scripts

---
 knexfile.js                            |  3 +++
 src/db/seeds/00-suppliers.js           | 11 +++++++++++
 src/db/seeds/01-products.js            | 11 +++++++++++
 src/db/seeds/03-categories.js          | 11 +++++++++++
 src/db/seeds/04-products_categories.js | 11 +++++++++++
 5 files changed, 47 insertions(+)
 create mode 100644 src/db/seeds/00-suppliers.js
 create mode 100644 src/db/seeds/01-products.js
 create mode 100644 src/db/seeds/03-categories.js
 create mode 100644 src/db/seeds/04-products_categories.js

diff --git a/knexfile.js b/knexfile.js
index f070fbbc..bc97b719 100644
--- a/knexfile.js
+++ b/knexfile.js
@@ -9,5 +9,8 @@ module.exports = {
     migrations: {
       directory: path.join(__dirname, "src", "db", "migrations"),
     },
+    seeds: {
+      directory: path.join(__dirname, "src", "db", "seeds"),
+    },
   },
 };
diff --git a/src/db/seeds/00-suppliers.js b/src/db/seeds/00-suppliers.js
new file mode 100644
index 00000000..25df32c9
--- /dev/null
+++ b/src/db/seeds/00-suppliers.js
@@ -0,0 +1,11 @@
+const suppliers = require("../fixtures/suppliers");
+
+exports.seed = function (knex) {
+  // Deletes ALL existing entries
+  return knex
+    .raw("TRUNCATE TABLE suppliers RESTART IDENTITY CASCADE")
+    .then(function () {
+      // Inserts seed entries
+      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..29759c7f
--- /dev/null
+++ b/src/db/seeds/01-products.js
@@ -0,0 +1,11 @@
+const products = require("../fixtures/products");
+
+exports.seed = function (knex) {
+  // Deletes ALL existing entries
+  return knex
+    .raw("TRUNCATE TABLE products RESTART IDENTITY CASCADE")
+    .then(function () {
+      // Inserts seed entries
+      return knex("products").insert(products);
+    });
+};
diff --git a/src/db/seeds/03-categories.js b/src/db/seeds/03-categories.js
new file mode 100644
index 00000000..400e9254
--- /dev/null
+++ b/src/db/seeds/03-categories.js
@@ -0,0 +1,11 @@
+const categories = require("../fixtures/categories");
+
+exports.seed = function (knex) {
+  // Deletes ALL existing entries
+  return knex
+    .raw("TRUNCATE TABLE categories RESTART IDENTITY CASCADE")
+    .then(function () {
+      // Inserts seed entries
+      return knex("categories").insert(categories);
+    });
+};
diff --git a/src/db/seeds/04-products_categories.js b/src/db/seeds/04-products_categories.js
new file mode 100644
index 00000000..4fb4500c
--- /dev/null
+++ b/src/db/seeds/04-products_categories.js
@@ -0,0 +1,11 @@
+const productsCategories = require("../fixtures/productsCategories");
+
+exports.seed = function (knex) {
+  // Deletes ALL existing entries
+  return knex
+    .raw("TRUNCATE TABLE products_categories RESTART IDENTITY CASCADE")
+    .then(function () {
+      // Inserts seed entries
+      return knex("products_categories").insert(productsCategories);
+    });
+};

From b970b7be2321ac8b3193cdd80584f11c62b2bb7f Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Mon, 30 Nov 2020 20:40:30 -1000
Subject: [PATCH 09/35] create CRUD functionality

---
 src/categories/categories.controller.js | 14 ++++----
 src/categories/categories.service.js    |  7 ++++
 src/products/products.controller.js     | 26 ++++++++++++---
 src/products/products.service.js        | 13 ++++++++
 src/suppliers/suppliers.controller.js   | 43 ++++++++++++++++++++-----
 src/suppliers/suppliers.service.js      | 21 ++++++++++++
 6 files changed, 103 insertions(+), 21 deletions(-)
 create mode 100644 src/categories/categories.service.js
 create mode 100644 src/products/products.service.js
 create mode 100644 src/suppliers/suppliers.service.js

diff --git a/src/categories/categories.controller.js b/src/categories/categories.controller.js
index 54354238..663a8dbe 100644
--- a/src/categories/categories.controller.js
+++ b/src/categories/categories.controller.js
@@ -1,11 +1,9 @@
-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");
+
+function list(req, res, next) {
+  CategoriesService.getAllCategories().then(categories =>
+    res.json({ data: categories })
+  );
 }
 
 module.exports = {
diff --git a/src/categories/categories.service.js b/src/categories/categories.service.js
new file mode 100644
index 00000000..01008ee0
--- /dev/null
+++ b/src/categories/categories.service.js
@@ -0,0 +1,7 @@
+const knex = require("../db/connection");
+
+const getAllCategories = () => knex("categories").select("*");
+
+module.exports = {
+  getAllCategories,
+};
diff --git a/src/products/products.controller.js b/src/products/products.controller.js
index 961703ab..7b82a576 100644
--- a/src/products/products.controller.js
+++ b/src/products/products.controller.js
@@ -1,14 +1,30 @@
+const ProductsService = require("./products.service");
+
+function productExists(req, res, next) {
+  const error = { status: 404, message: `Product cannot be found.` };
+
+  const { productId } = req.params;
+  if (!productId) return next(error);
+
+  ProductsService.getProductById(productId).then(product => {
+    if (!product) return next(error);
+    res.locals.product = product;
+    next();
+  });
+}
+
 function read(req, res, next) {
-  res.json({ data: { product_title: "some product title" } });
+  const { product } = res.locals;
+  res.json({ data: product });
 }
 
 function list(req, res, next) {
-  res.json({
-    data: [{ product_title: "product 1" }, { product_title: "product 2" }],
-  });
+  ProductsService.getAllProducts().then(products =>
+    res.json({ data: products })
+  );
 }
 
 module.exports = {
-  read: [read],
+  read: [productExists, read],
   list: [list],
 };
diff --git a/src/products/products.service.js b/src/products/products.service.js
new file mode 100644
index 00000000..3302f28c
--- /dev/null
+++ b/src/products/products.service.js
@@ -0,0 +1,13 @@
+const knex = require("../db/connection");
+
+const products = knex("products");
+
+const getAllProducts = () => products.select("*");
+
+const getProductById = productId =>
+  products.select("*").where({ product_id: productId }).first();
+
+module.exports = {
+  getAllProducts,
+  getProductById,
+};
diff --git a/src/suppliers/suppliers.controller.js b/src/suppliers/suppliers.controller.js
index 51448dbc..42f1bb64 100644
--- a/src/suppliers/suppliers.controller.js
+++ b/src/suppliers/suppliers.controller.js
@@ -1,17 +1,44 @@
-async function create(req, res, next) {
-  res.status(201).json({ data: { supplier_name: "new supplier" } });
+const SuppliersService = require("./suppliers.service.js");
+
+function supplierExists(req, res, next) {
+  const error = { status: 404, message: `Supplier cannot be found.` };
+  const { supplierId } = req.params;
+  if (!supplierId) return next(error);
+
+  SuppliersService.getSupplierById(supplierId).then(supplier => {
+    if (!supplier) return next(error);
+    res.locals.supplier = supplier;
+    next();
+  });
 }
 
-async function update(req, res, next) {
-  res.json({ data: { supplier_name: "updated supplier" } });
+function create(req, res, next) {
+  SuppliersService.createSupplier(req.body.data).then(newSupplier =>
+    res.status(201).json({ data: newSupplier })
+  );
+}
+
+function update(req, res, next) {
+  const {
+    supplier: { supplier_id: supplierId, ...supplier },
+  } = res.locals;
+  const updatedSupplier = { ...supplier, ...req.body.data };
+
+  SuppliersService.updateSupplierById(
+    supplierId,
+    updatedSupplier
+  ).then(updatedSupplier => res.json({ data: updatedSupplier }));
 }
 
-async function destroy(req, res, next) {
-  res.sendStatus(204);
+function destroy(req, res, next) {
+  const { supplier } = res.locals;
+  SuppliersService.deleteSupplierById(supplier.supplier_id).then(() =>
+    res.sendStatus(204)
+  );
 }
 
 module.exports = {
   create,
-  update,
-  delete: destroy,
+  update: [supplierExists, update],
+  delete: [supplierExists, destroy],
 };
diff --git a/src/suppliers/suppliers.service.js b/src/suppliers/suppliers.service.js
new file mode 100644
index 00000000..2d786163
--- /dev/null
+++ b/src/suppliers/suppliers.service.js
@@ -0,0 +1,21 @@
+const knex = require("../db/connection");
+
+const suppliers = knex("suppliers");
+
+const createSupplier = supplier => suppliers.insert(supplier).returning("*");
+
+const getSupplierById = supplierId =>
+  suppliers.select("*").where({ supplier_id: supplierId }).first();
+
+const updateSupplierById = (supplierId, updatedSupplier) =>
+  suppliers.where({ supplier_id: supplierId }).update(updatedSupplier, "*");
+
+const deleteSupplierById = supplierId =>
+  suppliers.where({ supplier_id: supplierId }).del();
+
+module.exports = {
+  createSupplier,
+  getSupplierById,
+  updateSupplierById,
+  deleteSupplierById,
+};

From e861f29e24b9a0c956e051ba98ad264443caecbd Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Tue, 1 Dec 2020 10:40:51 -1000
Subject: [PATCH 10/35] fix updateSupplierById query

---
 src/suppliers/suppliers.service.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/suppliers/suppliers.service.js b/src/suppliers/suppliers.service.js
index 2d786163..669a5f5f 100644
--- a/src/suppliers/suppliers.service.js
+++ b/src/suppliers/suppliers.service.js
@@ -8,7 +8,10 @@ const getSupplierById = supplierId =>
   suppliers.select("*").where({ supplier_id: supplierId }).first();
 
 const updateSupplierById = (supplierId, updatedSupplier) =>
-  suppliers.where({ supplier_id: supplierId }).update(updatedSupplier, "*");
+  suppliers
+    .select("*")
+    .where({ supplier_id: supplierId })
+    .update(updatedSupplier, "*");
 
 const deleteSupplierById = supplierId =>
   suppliers.where({ supplier_id: supplierId }).del();

From d8ceb80e31e2e91d199473f7cf07cc0306aaa556 Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Wed, 27 Jan 2021 16:15:02 -1000
Subject: [PATCH 11/35] feat: add create api validation

---
 src/suppliers/suppliers.controller.js | 59 +++++++++++++++++++++++++--
 1 file changed, 56 insertions(+), 3 deletions(-)

diff --git a/src/suppliers/suppliers.controller.js b/src/suppliers/suppliers.controller.js
index 42f1bb64..dba86a29 100644
--- a/src/suppliers/suppliers.controller.js
+++ b/src/suppliers/suppliers.controller.js
@@ -12,9 +12,62 @@ function supplierExists(req, res, next) {
   });
 }
 
+function hasValidFields(req, res, next) {
+  const { data = {} } = req.body;
+  const validFields = new Set([
+    "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 invalidFields = Object.keys(data).filter(
+    field => !validFields.has(field)
+  );
+
+  if (invalidFields.length)
+    return next({
+      status: 400,
+      message: `Invalid field(s): ${invalidFields.join(", ")}`,
+    });
+  next();
+}
+
+function bodyDataHas(propertyName) {
+  return function (req, res, next) {
+    const { data = {} } = req.body;
+    if (data[propertyName]) {
+      return next();
+    }
+    next({ status: 400, message: `Supplier must include a ${propertyName}` });
+  };
+}
+
+const hasSupplierName = bodyDataHas("supplier_name");
+const hasSupplierEmail = bodyDataHas("supplier_email");
+
 function create(req, res, next) {
-  SuppliersService.createSupplier(req.body.data).then(newSupplier =>
-    res.status(201).json({ data: newSupplier })
+  const newSupplier = ({
+    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,
+  } = req.body.data);
+
+  SuppliersService.createSupplier(newSupplier).then(createdSupplier =>
+    res.status(201).json({ data: createdSupplier })
   );
 }
 
@@ -38,7 +91,7 @@ function destroy(req, res, next) {
 }
 
 module.exports = {
-  create,
+  create: [hasValidFields, hasSupplierName, hasSupplierEmail, create],
   update: [supplierExists, update],
   delete: [supplierExists, destroy],
 };

From 53a67385b32c3a5da253c6556f7c9e5ee9bfa21e Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Wed, 27 Jan 2021 16:49:52 -1000
Subject: [PATCH 12/35] move supplierExists function down for consistency

---
 src/suppliers/suppliers.controller.js | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/src/suppliers/suppliers.controller.js b/src/suppliers/suppliers.controller.js
index dba86a29..c40246e9 100644
--- a/src/suppliers/suppliers.controller.js
+++ b/src/suppliers/suppliers.controller.js
@@ -1,17 +1,5 @@
 const SuppliersService = require("./suppliers.service.js");
 
-function supplierExists(req, res, next) {
-  const error = { status: 404, message: `Supplier cannot be found.` };
-  const { supplierId } = req.params;
-  if (!supplierId) return next(error);
-
-  SuppliersService.getSupplierById(supplierId).then(supplier => {
-    if (!supplier) return next(error);
-    res.locals.supplier = supplier;
-    next();
-  });
-}
-
 function hasValidFields(req, res, next) {
   const { data = {} } = req.body;
   const validFields = new Set([
@@ -71,6 +59,18 @@ function create(req, res, next) {
   );
 }
 
+function supplierExists(req, res, next) {
+  const error = { status: 404, message: `Supplier cannot be found.` };
+  const { supplierId } = req.params;
+  if (!supplierId) return next(error);
+
+  SuppliersService.getSupplierById(supplierId).then(supplier => {
+    if (!supplier) return next(error);
+    res.locals.supplier = supplier;
+    next();
+  });
+}
+
 function update(req, res, next) {
   const {
     supplier: { supplier_id: supplierId, ...supplier },

From cd89dea1440c634c02f44abb4e992609ab3c5dc4 Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Thu, 28 Jan 2021 23:48:35 -1000
Subject: [PATCH 13/35] validate :productId with regex

---
 src/products/products.router.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/products/products.router.js b/src/products/products.router.js
index da9e571c..4827291f 100644
--- a/src/products/products.router.js
+++ b/src/products/products.router.js
@@ -3,6 +3,6 @@ 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("/:productId([0-9]+)").get(controller.read).all(methodNotAllowed);
 
 module.exports = router;

From 889beaadccfa79225201b9f7a600a01069ddd54d Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Fri, 19 Mar 2021 22:56:54 -0700
Subject: [PATCH 14/35] fix: make update a full update, not patch

---
 src/categories/categories.controller.js |  10 +--
 src/categories/categories.router.js     |   4 +-
 src/categories/categories.service.js    |   6 +-
 src/errors/hasProperties.js             |  21 +++++
 src/products/products.controller.js     |  31 +++----
 src/products/products.router.js         |   2 +-
 src/products/products.service.js        |  15 ++--
 src/suppliers/suppliers.controller.js   | 113 +++++++++---------------
 src/suppliers/suppliers.router.js       |   2 +-
 src/suppliers/suppliers.service.js      |  31 ++++---
 10 files changed, 114 insertions(+), 121 deletions(-)
 create mode 100644 src/errors/hasProperties.js

diff --git a/src/categories/categories.controller.js b/src/categories/categories.controller.js
index 663a8dbe..813d9b14 100644
--- a/src/categories/categories.controller.js
+++ b/src/categories/categories.controller.js
@@ -1,11 +1,11 @@
-const CategoriesService = require("./categories.service");
+const categoriesService = require("./categories.service");
 
-function list(req, res, next) {
-  CategoriesService.getAllCategories().then(categories =>
-    res.json({ data: categories })
+function list(req, res) {
+  categoriesService.list().then((data) =>
+    res.json({ data })
   );
 }
 
 module.exports = {
-  list: [list],
+  list,
 };
diff --git a/src/categories/categories.router.js b/src/categories/categories.router.js
index e9ed0acd..0972ef44 100644
--- a/src/categories/categories.router.js
+++ b/src/categories/categories.router.js
@@ -1,8 +1,8 @@
-const router = require("express").Router({ mergeParams: true });
+const router = require("express").Router();
 const controller = require("./categories.controller");
 const methodNotAllowed = require("../errors/methodNotAllowed");
 
 router.route("/").get(controller.list).all(methodNotAllowed);
-router.route("/:categoryId").all(methodNotAllowed);
+router.route("/:categoryId([0-9]+)").all(methodNotAllowed);
 
 module.exports = router;
diff --git a/src/categories/categories.service.js b/src/categories/categories.service.js
index 01008ee0..8c265953 100644
--- a/src/categories/categories.service.js
+++ b/src/categories/categories.service.js
@@ -1,7 +1,9 @@
 const knex = require("../db/connection");
 
-const getAllCategories = () => knex("categories").select("*");
+function list () {
+  return knex("categories").select("*");
+}
 
 module.exports = {
-  getAllCategories,
+  list,
 };
diff --git a/src/errors/hasProperties.js b/src/errors/hasProperties.js
new file mode 100644
index 00000000..4d64afb9
--- /dev/null
+++ b/src/errors/hasProperties.js
@@ -0,0 +1,21 @@
+
+function hasProperties(...properties) {
+  return function (res, req, next) {
+    const { data = {} } = res.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 7b82a576..27a3c8d1 100644
--- a/src/products/products.controller.js
+++ b/src/products/products.controller.js
@@ -1,30 +1,25 @@
-const ProductsService = require("./products.service");
+const productsService = require("./products.service");
 
 function productExists(req, res, next) {
-  const error = { status: 404, message: `Product cannot be found.` };
-
-  const { productId } = req.params;
-  if (!productId) return next(error);
-
-  ProductsService.getProductById(productId).then(product => {
-    if (!product) return next(error);
-    res.locals.product = product;
-    next();
+  productsService.read(req.params.productId).then(product => {
+    if (product) {
+      res.locals.product = product;
+      return next();
+    }
+    next({ status: 404, message: `Product cannot be found.` });
   });
 }
 
-function read(req, res, next) {
-  const { product } = res.locals;
-  res.json({ data: product });
+function read (req, res) {
+  const { product: data } = res.locals;
+  res.json({ data });
 }
 
-function list(req, res, next) {
-  ProductsService.getAllProducts().then(products =>
-    res.json({ data: products })
-  );
+function list (req, res) {
+  productsService.list().then((data) => res.json({ data }));
 }
 
 module.exports = {
   read: [productExists, read],
-  list: [list],
+  list,
 };
diff --git a/src/products/products.router.js b/src/products/products.router.js
index 4827291f..dad38178 100644
--- a/src/products/products.router.js
+++ b/src/products/products.router.js
@@ -1,4 +1,4 @@
-const router = require("express").Router({ mergeParams: true });
+const router = require("express").Router();
 const controller = require("./products.controller");
 const methodNotAllowed = require("../errors/methodNotAllowed");
 
diff --git a/src/products/products.service.js b/src/products/products.service.js
index 3302f28c..48014595 100644
--- a/src/products/products.service.js
+++ b/src/products/products.service.js
@@ -1,13 +1,14 @@
 const knex = require("../db/connection");
 
-const products = knex("products");
+function list () {
+  return knex("products").select("*");
+}
 
-const getAllProducts = () => products.select("*");
-
-const getProductById = productId =>
-  products.select("*").where({ product_id: productId }).first();
+function read (product_id) {
+  return knex("products").select("*").where({ product_id }).first();
+}
 
 module.exports = {
-  getAllProducts,
-  getProductById,
+  list,
+  read,
 };
diff --git a/src/suppliers/suppliers.controller.js b/src/suppliers/suppliers.controller.js
index c40246e9..210e854d 100644
--- a/src/suppliers/suppliers.controller.js
+++ b/src/suppliers/suppliers.controller.js
@@ -1,22 +1,24 @@
-const SuppliersService = require("./suppliers.service.js");
+const suppliersService = require("./suppliers.service.js");
+const hasProperties = require("../errors/hasProperties");
 
-function hasValidFields(req, res, next) {
+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",
+];
+
+function hasOnlyValidProperties (req, res, next) {
   const { data = {} } = req.body;
-  const validFields = new Set([
-    "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 invalidFields = Object.keys(data).filter(
-    field => !validFields.has(field)
+    (field) => !VALID_PROPERTIES.includes(field)
   );
 
   if (invalidFields.length)
@@ -27,71 +29,40 @@ function hasValidFields(req, res, next) {
   next();
 }
 
-function bodyDataHas(propertyName) {
-  return function (req, res, next) {
-    const { data = {} } = req.body;
-    if (data[propertyName]) {
-      return next();
-    }
-    next({ status: 400, message: `Supplier must include a ${propertyName}` });
-  };
-}
-
-const hasSupplierName = bodyDataHas("supplier_name");
-const hasSupplierEmail = bodyDataHas("supplier_email");
-
-function create(req, res, next) {
-  const newSupplier = ({
-    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,
-  } = req.body.data);
+const hasRequiredProperties = hasProperties("supplier_name", "supplier_email");
 
-  SuppliersService.createSupplier(newSupplier).then(createdSupplier =>
-    res.status(201).json({ data: createdSupplier })
-  );
+function create (req, res) {
+  suppliersService
+    .create(req.body.data)
+    .then((data) => res.status(201).json({ data }));
 }
 
-function supplierExists(req, res, next) {
-  const error = { status: 404, message: `Supplier cannot be found.` };
-  const { supplierId } = req.params;
-  if (!supplierId) return next(error);
-
-  SuppliersService.getSupplierById(supplierId).then(supplier => {
-    if (!supplier) return next(error);
-    res.locals.supplier = supplier;
-    next();
+function supplierExists (req, res, next) {
+  suppliersService.read(req.params.supplierId).then((supplier) => {
+    if (supplier) {
+      res.locals.supplier = supplier;
+      return next();
+    }
+    next({ status: 404, message: `Supplier cannot be found.` });
   });
 }
 
-function update(req, res, next) {
-  const {
-    supplier: { supplier_id: supplierId, ...supplier },
-  } = res.locals;
-  const updatedSupplier = { ...supplier, ...req.body.data };
-
-  SuppliersService.updateSupplierById(
-    supplierId,
-    updatedSupplier
-  ).then(updatedSupplier => res.json({ data: updatedSupplier }));
+function update (req, res) {
+  const updatedSupplier = {
+    ...req.body.data,
+    supplier_id: res.locals.supplier.supplier_id,
+  };
+  suppliersService.update(updatedSupplier).then((data) => res.json({ data }));
 }
 
-function destroy(req, res, next) {
-  const { supplier } = res.locals;
-  SuppliersService.deleteSupplierById(supplier.supplier_id).then(() =>
-    res.sendStatus(204)
-  );
+function destroy (req, res) {
+  suppliersService
+    .delete(res.locals.supplier.supplier_id)
+    .then(() => res.sendStatus(204));
 }
 
 module.exports = {
-  create: [hasValidFields, hasSupplierName, hasSupplierEmail, create],
-  update: [supplierExists, update],
-  delete: [supplierExists, destroy],
+  create: [hasOnlyValidProperties, hasRequiredProperties, create],
+  update: [supplierExists, hasOnlyValidProperties, hasRequiredProperties, update],
+  destroy: [supplierExists, destroy],
 };
diff --git a/src/suppliers/suppliers.router.js b/src/suppliers/suppliers.router.js
index b9be48a1..80c90603 100644
--- a/src/suppliers/suppliers.router.js
+++ b/src/suppliers/suppliers.router.js
@@ -5,7 +5,7 @@ const methodNotAllowed = require("../errors/methodNotAllowed");
 router.route("/").post(controller.create).all(methodNotAllowed);
 
 router
-  .route("/:supplierId")
+  .route("/:supplierId([0-9]+)")
   .put(controller.update)
   .delete(controller.delete)
   .all(methodNotAllowed);
diff --git a/src/suppliers/suppliers.service.js b/src/suppliers/suppliers.service.js
index 669a5f5f..27124976 100644
--- a/src/suppliers/suppliers.service.js
+++ b/src/suppliers/suppliers.service.js
@@ -1,24 +1,27 @@
 const knex = require("../db/connection");
 
-const suppliers = knex("suppliers");
+function create(supplier) {
+  return knex("suppliers").insert(supplier).returning("*");
+}
 
-const createSupplier = supplier => suppliers.insert(supplier).returning("*");
+function read(supplier_id) {
+  return knex("suppliers").select("*").where({ supplier_id }).first();
+}
 
-const getSupplierById = supplierId =>
-  suppliers.select("*").where({ supplier_id: supplierId }).first();
-
-const updateSupplierById = (supplierId, updatedSupplier) =>
-  suppliers
+function update(updatedSupplier) {
+  return knex("suppliers")
     .select("*")
-    .where({ supplier_id: supplierId })
+    .where({ supplier_id: updatedSupplier.supplier_id })
     .update(updatedSupplier, "*");
+}
 
-const deleteSupplierById = supplierId =>
-  suppliers.where({ supplier_id: supplierId }).del();
+function destroy(supplierId) {
+  return knex("suppliers").where({ supplier_id: supplierId }).del();
+}
 
 module.exports = {
-  createSupplier,
-  getSupplierById,
-  updateSupplierById,
-  deleteSupplierById,
+  create,
+  read,
+  update,
+  delete: destroy,
 };

From e5368e6a0db9a1fd667af3a064ab545326f67154 Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Wed, 14 Apr 2021 10:58:01 -0500
Subject: [PATCH 15/35] fix: add .catch(next) to each knex promise

catch methods were missing from earlier implementations

fix TFENG-450 and fix #6238
---
 src/categories/categories.controller.js |  9 ++---
 src/products/products.controller.js     | 26 ++++++++------
 src/suppliers/suppliers.controller.js   | 48 ++++++++++++++++---------
 3 files changed, 52 insertions(+), 31 deletions(-)

diff --git a/src/categories/categories.controller.js b/src/categories/categories.controller.js
index 813d9b14..979f8cb4 100644
--- a/src/categories/categories.controller.js
+++ b/src/categories/categories.controller.js
@@ -1,9 +1,10 @@
 const categoriesService = require("./categories.service");
 
-function list(req, res) {
-  categoriesService.list().then((data) =>
-    res.json({ data })
-  );
+function list(req, res, next) {
+  categoriesService
+    .list()
+    .then((data) => res.json({ data }))
+    .catch(next);
 }
 
 module.exports = {
diff --git a/src/products/products.controller.js b/src/products/products.controller.js
index 27a3c8d1..9774cdd5 100644
--- a/src/products/products.controller.js
+++ b/src/products/products.controller.js
@@ -1,22 +1,28 @@
 const productsService = require("./products.service");
 
 function productExists(req, res, next) {
-  productsService.read(req.params.productId).then(product => {
-    if (product) {
-      res.locals.product = product;
-      return next();
-    }
-    next({ status: 404, message: `Product cannot be found.` });
-  });
+  productsService
+    .read(req.params.productId)
+    .then((product) => {
+      if (product) {
+        res.locals.product = product;
+        return next();
+      }
+      next({ status: 404, message: `Product cannot be found.` });
+    })
+    .catch(next);
 }
 
-function read (req, res) {
+function read(req, res) {
   const { product: data } = res.locals;
   res.json({ data });
 }
 
-function list (req, res) {
-  productsService.list().then((data) => res.json({ data }));
+function list(req, res, next) {
+  productsService
+    .list()
+    .then((data) => res.json({ data }))
+    .catch(next);
 }
 
 module.exports = {
diff --git a/src/suppliers/suppliers.controller.js b/src/suppliers/suppliers.controller.js
index 210e854d..6e2780cc 100644
--- a/src/suppliers/suppliers.controller.js
+++ b/src/suppliers/suppliers.controller.js
@@ -14,55 +14,69 @@ const VALID_PROPERTIES = [
   "supplier_type_of_goods",
 ];
 
-function hasOnlyValidProperties (req, res, next) {
+function hasOnlyValidProperties(req, res, next) {
   const { data = {} } = req.body;
 
   const invalidFields = Object.keys(data).filter(
     (field) => !VALID_PROPERTIES.includes(field)
   );
 
-  if (invalidFields.length)
+  if (invalidFields.length) {
     return next({
       status: 400,
       message: `Invalid field(s): ${invalidFields.join(", ")}`,
     });
+  }
   next();
 }
 
 const hasRequiredProperties = hasProperties("supplier_name", "supplier_email");
 
-function create (req, res) {
+function create(req, res, next) {
   suppliersService
     .create(req.body.data)
-    .then((data) => res.status(201).json({ data }));
+    .then((data) => res.status(201).json({ data }))
+    .catch(next);
 }
 
-function supplierExists (req, res, next) {
-  suppliersService.read(req.params.supplierId).then((supplier) => {
-    if (supplier) {
-      res.locals.supplier = supplier;
-      return next();
-    }
-    next({ status: 404, message: `Supplier cannot be found.` });
-  });
+function supplierExists(req, res, next) {
+  suppliersService
+    .read(req.params.supplierId)
+    .then((supplier) => {
+      if (supplier) {
+        res.locals.supplier = supplier;
+        return next();
+      }
+      next({ status: 404, message: `Supplier cannot be found.` });
+    })
+    .catch(next);
 }
 
-function update (req, res) {
+function update(req, res, next) {
   const updatedSupplier = {
     ...req.body.data,
     supplier_id: res.locals.supplier.supplier_id,
   };
-  suppliersService.update(updatedSupplier).then((data) => res.json({ data }));
+  suppliersService
+    .update(updatedSupplier)
+    .then((data) => res.json({ data }))
+    .catch(next);
 }
 
-function destroy (req, res) {
+function destroy(req, res, next) {
   suppliersService
     .delete(res.locals.supplier.supplier_id)
-    .then(() => res.sendStatus(204));
+    .then(() => res.sendStatus(204))
+    .catch(next);
 }
 
 module.exports = {
   create: [hasOnlyValidProperties, hasRequiredProperties, create],
-  update: [supplierExists, hasOnlyValidProperties, hasRequiredProperties, update],
+  update: [
+    supplierExists,
+    hasOnlyValidProperties,
+    hasRequiredProperties,
+    update,
+  ],
   destroy: [supplierExists, destroy],
 };

From e5a1ac9e39e8edab85890b9df2d20d8865ccf25f Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Wed, 14 Apr 2021 11:18:38 -0500
Subject: [PATCH 16/35] style: run prettier on code

---
 src/categories/categories.service.js | 2 +-
 src/errors/hasProperties.js          | 1 -
 src/products/products.service.js     | 4 ++--
 3 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/categories/categories.service.js b/src/categories/categories.service.js
index 8c265953..ecb36610 100644
--- a/src/categories/categories.service.js
+++ b/src/categories/categories.service.js
@@ -1,6 +1,6 @@
 const knex = require("../db/connection");
 
-function list () {
+function list() {
   return knex("categories").select("*");
 }
 
diff --git a/src/errors/hasProperties.js b/src/errors/hasProperties.js
index 4d64afb9..605dae3d 100644
--- a/src/errors/hasProperties.js
+++ b/src/errors/hasProperties.js
@@ -1,4 +1,3 @@
-
 function hasProperties(...properties) {
   return function (res, req, next) {
     const { data = {} } = res.body;
diff --git a/src/products/products.service.js b/src/products/products.service.js
index 48014595..c0dea6c3 100644
--- a/src/products/products.service.js
+++ b/src/products/products.service.js
@@ -1,10 +1,10 @@
 const knex = require("../db/connection");
 
-function list () {
+function list() {
   return knex("products").select("*");
 }
 
-function read (product_id) {
+function read(product_id) {
   return knex("products").select("*").where({ product_id }).first();
 }
 

From 0485d4cbf6c890cd9234a6209584296ff3c2553b Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Wed, 14 Apr 2021 11:31:28 -0500
Subject: [PATCH 17/35] fix(suppliers): export destroy as delete

---
 src/suppliers/suppliers.controller.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/suppliers/suppliers.controller.js b/src/suppliers/suppliers.controller.js
index 6e2780cc..9795620c 100644
--- a/src/suppliers/suppliers.controller.js
+++ b/src/suppliers/suppliers.controller.js
@@ -78,5 +78,5 @@ module.exports = {
     hasRequiredProperties,
     update,
   ],
-  destroy: [supplierExists, destroy],
+  delete: [supplierExists, destroy],
 };

From be04b1ce67576ddc58c3b0a28e3eecfd958af583 Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Fri, 16 Apr 2021 09:24:44 -0500
Subject: [PATCH 18/35] refactor(suppliers): simplify delete implementation

---
 src/suppliers/suppliers.service.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/suppliers/suppliers.service.js b/src/suppliers/suppliers.service.js
index 27124976..8ae82505 100644
--- a/src/suppliers/suppliers.service.js
+++ b/src/suppliers/suppliers.service.js
@@ -15,8 +15,8 @@ function update(updatedSupplier) {
     .update(updatedSupplier, "*");
 }
 
-function destroy(supplierId) {
-  return knex("suppliers").where({ supplier_id: supplierId }).del();
+function destroy(supplier_id) {
+  return knex("suppliers").where({ supplier_id }).del();
 }
 
 module.exports = {

From ae709d84fb13e4e7f32ac7a02541da146efa9300 Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Tue, 1 Dec 2020 10:39:23 -1000
Subject: [PATCH 19/35] refactor controllers to use async await

---
 src/categories/categories.controller.js |  9 ++--
 src/products/products.controller.js     | 28 +++++-------
 src/suppliers/suppliers.controller.js   | 57 +++++++++----------------
 3 files changed, 36 insertions(+), 58 deletions(-)

diff --git a/src/categories/categories.controller.js b/src/categories/categories.controller.js
index 979f8cb4..7fe026de 100644
--- a/src/categories/categories.controller.js
+++ b/src/categories/categories.controller.js
@@ -1,10 +1,9 @@
 const categoriesService = require("./categories.service");
 
-function list(req, res, next) {
-  categoriesService
-    .list()
-    .then((data) => res.json({ data }))
-    .catch(next);
+
+async function list(req, res) {
+  const data = await categoriesService.list();
+  res.json({ data });
 }
 
 module.exports = {
diff --git a/src/products/products.controller.js b/src/products/products.controller.js
index 9774cdd5..b4799560 100644
--- a/src/products/products.controller.js
+++ b/src/products/products.controller.js
@@ -1,28 +1,22 @@
 const productsService = require("./products.service");
 
-function productExists(req, res, next) {
-  productsService
-    .read(req.params.productId)
-    .then((product) => {
-      if (product) {
-        res.locals.product = product;
-        return next();
-      }
-      next({ status: 404, message: `Product cannot be found.` });
-    })
-    .catch(next);
+async function productExists(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.` });
 }
 
-function read(req, res) {
+function read (req, res) {
   const { product: data } = res.locals;
   res.json({ data });
 }
 
-function list(req, res, next) {
-  productsService
-    .list()
-    .then((data) => res.json({ data }))
-    .catch(next);
+async function list(req, res) {
+  const data = await productsService.list();
+  res.json({ data });
 }
 
 module.exports = {
diff --git a/src/suppliers/suppliers.controller.js b/src/suppliers/suppliers.controller.js
index 9795620c..a16c7744 100644
--- a/src/suppliers/suppliers.controller.js
+++ b/src/suppliers/suppliers.controller.js
@@ -14,69 +14,54 @@ const VALID_PROPERTIES = [
   "supplier_type_of_goods",
 ];
 
-function hasOnlyValidProperties(req, res, next) {
+function hasOnlyValidProperties (req, res, next) {
   const { data = {} } = req.body;
 
   const invalidFields = Object.keys(data).filter(
     (field) => !VALID_PROPERTIES.includes(field)
   );
 
-  if (invalidFields.length) {
+  if (invalidFields.length)
     return next({
       status: 400,
       message: `Invalid field(s): ${invalidFields.join(", ")}`,
     });
-  }
   next();
 }
 
 const hasRequiredProperties = hasProperties("supplier_name", "supplier_email");
 
-function create(req, res, next) {
-  suppliersService
-    .create(req.body.data)
-    .then((data) => res.status(201).json({ data }))
-    .catch(next);
+async function supplierExists (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.` });
 }
 
-function supplierExists(req, res, next) {
-  suppliersService
-    .read(req.params.supplierId)
-    .then((supplier) => {
-      if (supplier) {
-        res.locals.supplier = supplier;
-        return next();
-      }
-      next({ status: 404, message: `Supplier cannot be found.` });
-    })
-    .catch(next);
+async function create (req, res) {
+  const data = await suppliersService.create(req.body.data)
+  res.status(201).json({ data });
 }
 
-function update(req, res, next) {
+function update (req, res) {
   const updatedSupplier = {
     ...req.body.data,
     supplier_id: res.locals.supplier.supplier_id,
   };
-  suppliersService
-    .update(updatedSupplier)
-    .then((data) => res.json({ data }))
-    .catch(next);
+  const data = suppliersService.update(updatedSupplier);
+  res.json({ data });
 }
 
-function destroy(req, res, next) {
-  suppliersService
-    .delete(res.locals.supplier.supplier_id)
-    .then(() => res.sendStatus(204))
-    .catch(next);
+async function destroy(req, res, next) {
+  const { supplier } = res.locals;
+  await suppliersService.delete(supplier.supplier_id);
+  res.sendStatus(204);
 }
 
 module.exports = {
   create: [hasOnlyValidProperties, hasRequiredProperties, create],
-  update: [
-    supplierExists,
-    hasOnlyValidProperties,
-    hasRequiredProperties,
-    update,
-  ],
-  delete: [supplierExists, destroy],
+  update: [supplierExists, hasOnlyValidProperties, hasRequiredProperties, update],
+  destroy: [supplierExists, destroy],
 };

From 025a39bdab05ff7bc357dac8e84d00e1b489eb73 Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Wed, 27 Jan 2021 16:15:02 -1000
Subject: [PATCH 20/35] feat: add create api validation

---
 src/suppliers/suppliers.controller.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/suppliers/suppliers.controller.js b/src/suppliers/suppliers.controller.js
index a16c7744..fbad2202 100644
--- a/src/suppliers/suppliers.controller.js
+++ b/src/suppliers/suppliers.controller.js
@@ -45,12 +45,12 @@ async function create (req, res) {
   res.status(201).json({ data });
 }
 
-function update (req, res) {
+async function update (req, res) {
   const updatedSupplier = {
     ...req.body.data,
     supplier_id: res.locals.supplier.supplier_id,
   };
-  const data = suppliersService.update(updatedSupplier);
+  const data = await suppliersService.update(updatedSupplier);
   res.json({ data });
 }
 

From 985088af0b6234220656bb021057cfb5dddf6dcf Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Sun, 28 Feb 2021 13:23:41 -0700
Subject: [PATCH 21/35] feat: add asyncErrorBoundary

---
 src/categories/categories.controller.js |  4 ++--
 src/errors/asyncErrorBoundary.js        | 15 +++++++++++++
 src/products/products.controller.js     |  5 +++--
 src/suppliers/suppliers.controller.js   | 28 +++++++++++++++++--------
 4 files changed, 39 insertions(+), 13 deletions(-)
 create mode 100644 src/errors/asyncErrorBoundary.js

diff --git a/src/categories/categories.controller.js b/src/categories/categories.controller.js
index 7fe026de..5012cf43 100644
--- a/src/categories/categories.controller.js
+++ b/src/categories/categories.controller.js
@@ -1,5 +1,5 @@
 const categoriesService = require("./categories.service");
-
+const asyncErrorBoundary = require("../errors/asyncErrorBoundary");
 
 async function list(req, res) {
   const data = await categoriesService.list();
@@ -7,5 +7,5 @@ async function list(req, res) {
 }
 
 module.exports = {
-  list,
+  list: asyncErrorBoundary(list),
 };
diff --git a/src/errors/asyncErrorBoundary.js b/src/errors/asyncErrorBoundary.js
new file mode 100644
index 00000000..ab4ea140
--- /dev/null
+++ b/src/errors/asyncErrorBoundary.js
@@ -0,0 +1,15 @@
+function 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/products/products.controller.js b/src/products/products.controller.js
index b4799560..22f104fd 100644
--- a/src/products/products.controller.js
+++ b/src/products/products.controller.js
@@ -1,4 +1,5 @@
 const productsService = require("./products.service");
+const asyncErrorBoundary = require("../errors/asyncErrorBoundary");
 
 async function productExists(req, res, next) {
   const product = await productsService.read(req.params.productId);
@@ -20,6 +21,6 @@ async function list(req, res) {
 }
 
 module.exports = {
-  read: [productExists, read],
-  list,
+  read: [asyncErrorBoundary(productExists), read],
+  list: asyncErrorBoundary(list),
 };
diff --git a/src/suppliers/suppliers.controller.js b/src/suppliers/suppliers.controller.js
index fbad2202..90071f5a 100644
--- a/src/suppliers/suppliers.controller.js
+++ b/src/suppliers/suppliers.controller.js
@@ -1,5 +1,6 @@
 const suppliersService = require("./suppliers.service.js");
 const hasProperties = require("../errors/hasProperties");
+const asyncErrorBoundary = require("../errors/asyncErrorBoundary");
 
 const VALID_PROPERTIES = [
   "supplier_name",
@@ -14,7 +15,7 @@ const VALID_PROPERTIES = [
   "supplier_type_of_goods",
 ];
 
-function hasOnlyValidProperties (req, res, next) {
+function hasOnlyValidProperties(req, res, next) {
   const { data = {} } = req.body;
 
   const invalidFields = Object.keys(data).filter(
@@ -31,7 +32,7 @@ function hasOnlyValidProperties (req, res, next) {
 
 const hasRequiredProperties = hasProperties("supplier_name", "supplier_email");
 
-async function supplierExists (req, res, next) {
+async function supplierExists(req, res, next) {
   const supplier = await suppliersService.read(req.params.supplierId);
   if (supplier) {
     res.locals.supplier = supplier;
@@ -40,12 +41,12 @@ async function supplierExists (req, res, next) {
   next({ status: 404, message: `Supplier cannot be found.` });
 }
 
-async function create (req, res) {
-  const data = await suppliersService.create(req.body.data)
+async function create(req, res) {
+  const data = await suppliersService.create(req.body.data);
   res.status(201).json({ data });
 }
 
-async function update (req, res) {
+async function update(req, res) {
   const updatedSupplier = {
     ...req.body.data,
     supplier_id: res.locals.supplier.supplier_id,
@@ -54,14 +55,23 @@ async function update (req, res) {
   res.json({ data });
 }
 
-async function destroy(req, res, next) {
+async function destroy(req, res) {
   const { supplier } = res.locals;
   await suppliersService.delete(supplier.supplier_id);
   res.sendStatus(204);
 }
 
 module.exports = {
-  create: [hasOnlyValidProperties, hasRequiredProperties, create],
-  update: [supplierExists, hasOnlyValidProperties, hasRequiredProperties, update],
-  destroy: [supplierExists, destroy],
+  create: [
+    hasOnlyValidProperties,
+    hasRequiredProperties,
+    asyncErrorBoundary(create),
+  ],
+  update: [
+    asyncErrorBoundary(supplierExists),
+    hasOnlyValidProperties,
+    hasRequiredProperties,
+    asyncErrorBoundary(update),
+  ],
+  destroy: [asyncErrorBoundary(supplierExists), asyncErrorBoundary(destroy)],
 };

From ba7f521a2ab49cb0b9c4a2d1a58a4b793c7dd999 Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Sun, 28 Feb 2021 13:56:30 -0700
Subject: [PATCH 22/35] feat: use const where appropriate

---
 src/products/products.controller.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/products/products.controller.js b/src/products/products.controller.js
index 22f104fd..a60a6ef1 100644
--- a/src/products/products.controller.js
+++ b/src/products/products.controller.js
@@ -21,6 +21,6 @@ async function list(req, res) {
 }
 
 module.exports = {
-  read: [asyncErrorBoundary(productExists), read],
+  read: [asyncErrorBoundary(productExists), asyncErrorBoundary(read)],
   list: asyncErrorBoundary(list),
 };

From 4bd71f0ad4290b6a4c13dc697ad972e4939f8ecb Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Wed, 14 Apr 2021 11:39:48 -0500
Subject: [PATCH 23/35] fix: export suppliers controller destroy as delete

---
 src/suppliers/suppliers.controller.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/suppliers/suppliers.controller.js b/src/suppliers/suppliers.controller.js
index 90071f5a..d9471059 100644
--- a/src/suppliers/suppliers.controller.js
+++ b/src/suppliers/suppliers.controller.js
@@ -73,5 +73,5 @@ module.exports = {
     hasRequiredProperties,
     asyncErrorBoundary(update),
   ],
-  destroy: [asyncErrorBoundary(supplierExists), asyncErrorBoundary(destroy)],
+  delete: [asyncErrorBoundary(supplierExists), asyncErrorBoundary(destroy)],
 };

From 19c3d60bc788e05d1eb74ec9652d0c3b506f4f05 Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Wed, 14 Apr 2021 11:41:04 -0500
Subject: [PATCH 24/35] style: run prettier on code

---
 src/products/products.controller.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/products/products.controller.js b/src/products/products.controller.js
index a60a6ef1..df40ba50 100644
--- a/src/products/products.controller.js
+++ b/src/products/products.controller.js
@@ -10,7 +10,7 @@ async function productExists(req, res, next) {
   next({ status: 404, message: `Product cannot be found.` });
 }
 
-function read (req, res) {
+function read(req, res) {
   const { product: data } = res.locals;
   res.json({ data });
 }

From f154f66ae6a4baa650c4c32a13f330721151a107 Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Wed, 27 Jan 2021 23:07:04 -1000
Subject: [PATCH 25/35] update aggregate controllers

---
 src/products/products.controller.js | 17 ++++++++++++++-
 src/products/products.router.js     | 14 ++++++++++++-
 src/products/products.service.js    | 32 +++++++++++++++++++++++++++++
 3 files changed, 61 insertions(+), 2 deletions(-)

diff --git a/src/products/products.controller.js b/src/products/products.controller.js
index df40ba50..c38a40e3 100644
--- a/src/products/products.controller.js
+++ b/src/products/products.controller.js
@@ -20,7 +20,22 @@ async function list(req, res) {
   res.json({ data });
 }
 
+async function listOutOfStockCount(req, res) {
+  res.json({ data: await productsService.listOutOfStockCount() });
+}
+
+async function listPriceSummary(req, res) {
+  res.json({ data: await productsService.listPriceSummary() });
+}
+
+async function listTotalWeightByProduct(req, res) {
+  res.json({ data: await productsService.listTotalWeightByProduct() });
+}
+
 module.exports = {
-  read: [asyncErrorBoundary(productExists), asyncErrorBoundary(read)],
+  read: [asyncErrorBoundary(productExists), read],
   list: asyncErrorBoundary(list),
+  listOutOfStockCount: asyncErrorBoundary(listOutOfStockCount),
+  listPriceSummary: asyncErrorBoundary(listPriceSummary),
+  listTotalWeightByProduct: asyncErrorBoundary(listTotalWeightByProduct),
 };
diff --git a/src/products/products.router.js b/src/products/products.router.js
index dad38178..08548f57 100644
--- a/src/products/products.router.js
+++ b/src/products/products.router.js
@@ -3,6 +3,18 @@ const controller = require("./products.controller");
 const methodNotAllowed = require("../errors/methodNotAllowed");
 
 router.route("/").get(controller.list).all(methodNotAllowed);
-router.route("/:productId([0-9]+)").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").get(controller.read).all(methodNotAllowed);
 
 module.exports = router;
diff --git a/src/products/products.service.js b/src/products/products.service.js
index c0dea6c3..db828789 100644
--- a/src/products/products.service.js
+++ b/src/products/products.service.js
@@ -8,7 +8,39 @@ function read(product_id) {
   return knex("products").select("*").where({ product_id }).first();
 }
 
+function 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");
+}
+
+function listPriceSummary () {
+  return knex("products")
+    .select("supplier_id")
+    .min("product_price")
+    .max("product_price")
+    .avg("product_price")
+    .groupBy("supplier_id");
+}
+
+function 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");
+}
+
 module.exports = {
   list,
   read,
+  listOutOfStockCount,
+  listPriceSummary,
+  listTotalWeightByProduct,
 };

From 0e89a735e755475fff7eb220a2aa60493774bfee Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Thu, 28 Jan 2021 23:48:35 -1000
Subject: [PATCH 26/35] validate :productId with regex

---
 src/products/products.router.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/products/products.router.js b/src/products/products.router.js
index 08548f57..6260b429 100644
--- a/src/products/products.router.js
+++ b/src/products/products.router.js
@@ -15,6 +15,6 @@ router
   .route("/total_weight_by_product")
   .get(controller.listTotalWeightByProduct)
   .all(methodNotAllowed);
-router.route("/:productId").get(controller.read).all(methodNotAllowed);
+router.route("/:productId([0-9]+)").get(controller.read).all(methodNotAllowed);
 
 module.exports = router;

From 0178e11d76da324bda1c2362cf91937c6323e310 Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Mon, 1 Feb 2021 09:41:48 -1000
Subject: [PATCH 27/35] use dashes instead of underscores in url path

---
 src/products/products.router.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/products/products.router.js b/src/products/products.router.js
index 6260b429..adac04a2 100644
--- a/src/products/products.router.js
+++ b/src/products/products.router.js
@@ -4,15 +4,15 @@ const methodNotAllowed = require("../errors/methodNotAllowed");
 
 router.route("/").get(controller.list).all(methodNotAllowed);
 router
-  .route("/out_of_stock_count")
+  .route("/out-of-stock-count")
   .get(controller.listOutOfStockCount)
   .all(methodNotAllowed);
 router
-  .route("/price_summary")
+  .route("/price-summary")
   .get(controller.listPriceSummary)
   .all(methodNotAllowed);
 router
-  .route("/total_weight_by_product")
+  .route("/total-weight-by-product")
   .get(controller.listTotalWeightByProduct)
   .all(methodNotAllowed);
 router.route("/:productId([0-9]+)").get(controller.read).all(methodNotAllowed);

From 33dd956abd5215d12cb23798680b2ae62dea2d2e Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Wed, 14 Apr 2021 11:48:43 -0500
Subject: [PATCH 28/35] style: run prettier on code

---
 src/products/products.service.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/products/products.service.js b/src/products/products.service.js
index db828789..2e3d5e80 100644
--- a/src/products/products.service.js
+++ b/src/products/products.service.js
@@ -8,7 +8,7 @@ function read(product_id) {
   return knex("products").select("*").where({ product_id }).first();
 }
 
-function listOutOfStockCount () {
+function listOutOfStockCount() {
   return knex("products")
     .select("product_quantity_in_stock as out_of_stock")
     .count("product_id")
@@ -16,7 +16,7 @@ function listOutOfStockCount () {
     .groupBy("out_of_stock");
 }
 
-function listPriceSummary () {
+function listPriceSummary() {
   return knex("products")
     .select("supplier_id")
     .min("product_price")
@@ -25,7 +25,7 @@ function listPriceSummary () {
     .groupBy("supplier_id");
 }
 
-function listTotalWeightByProduct () {
+function listTotalWeightByProduct() {
   return knex("products")
     .select(
       "product_sku",

From ee90b36d9f7187c64a96e3ad73e8e0c7dc03097c Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Wed, 2 Dec 2020 13:11:24 -1000
Subject: [PATCH 29/35] add join base queries and query builder functions

---
 src/products/products.service.js | 49 ++++++++++++++++++++++++++++++++
 1 file changed, 49 insertions(+)

diff --git a/src/products/products.service.js b/src/products/products.service.js
index 2e3d5e80..7701d0d3 100644
--- a/src/products/products.service.js
+++ b/src/products/products.service.js
@@ -37,10 +37,59 @@ function listTotalWeightByProduct() {
     .groupBy("product_title", "product_sku");
 }
 
+const productsSuppliersJoin = knex("products as p").join(
+  "suppliers as s",
+  "p.supplier_id",
+  "s.supplier_id"
+);
+
+const productsCategoriesJoin = knex("products as p")
+  .join("products_categories as pc", "p.product_id", "pc.product_id")
+  .join("categories as c", "pc.category_id", "c.category_id");
+
+const productsCategoriesSuppliersJoin = knex("products as p")
+  .join("products_categories as pc", "p.product_id", "pc.product_id")
+  .join("categories as c", "pc.category_id", "c.category_id")
+  .join("suppliers as s", "p.supplier_id", "s.supplier_id");
+
+
+const getProductByIdWithCategories = productId =>
+  productsCategoriesJoin
+    .select("p.*", "c.*")
+    .where({ "p.product_id": productId })
+    .first();
+
+const getProductByIdWithSuppliers = productId =>
+  productsSuppliersJoin
+    .select("p.*", "s.*")
+    .where({ "p.product_id": productId })
+    .first();
+
+const getProductByIdWithCategoriesAndSuppliers = productId =>
+  productsCategoriesSuppliersJoin
+    .select("p.*", "c.*", "s.*")
+    .where({ "p.product_id": productId })
+    .first();
+
+const getTotalWeightOfProductsByCategory = () =>
+  productsCategoriesJoin
+    .select(
+      "c.category_name",
+      knex.raw(
+        "sum(p.product_weight_in_lbs * p.product_quantity_in_stock) as total_weight_by_category"
+      )
+    )
+    .groupBy("c.category_name")
+    .orderBy("total_weight_by_category");
+
 module.exports = {
   list,
   read,
   listOutOfStockCount,
   listPriceSummary,
   listTotalWeightByProduct,
+  getProductByIdWithCategories,
+  getProductByIdWithSuppliers,
+  getProductByIdWithCategoriesAndSuppliers,
+  getTotalWeightOfProductsByCategory,
 };

From 87b6c050bdead142ac6c1ce2edc45fc895b6a75d Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Sat, 20 Mar 2021 12:09:59 -0700
Subject: [PATCH 30/35] feat: add mapProperties

---
 package-lock.json                |  6 +--
 package.json                     |  1 +
 src/products/products.service.js | 65 ++++++++++++++++++--------------
 src/utils/map-properties.js      | 14 +++++++
 4 files changed, 55 insertions(+), 31 deletions(-)
 create mode 100644 src/utils/map-properties.js

diff --git a/package-lock.json b/package-lock.json
index e5d459b1..713678c9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1456,9 +1456,9 @@
       }
     },
     "lodash": {
-      "version": "4.17.20",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-      "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
+      "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",
diff --git a/package.json b/package.json
index 53803528..777fd867 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
     "dotenv": "^8.2.0",
     "express": "^4.17.1",
     "knex": "^0.21.12",
+    "lodash": "^4.17.19",
     "pg": "^8.5.1"
   },
   "devDependencies": {
diff --git a/src/products/products.service.js b/src/products/products.service.js
index 7701d0d3..518e6bea 100644
--- a/src/products/products.service.js
+++ b/src/products/products.service.js
@@ -1,11 +1,24 @@
 const knex = require("../db/connection");
+const mapProperties = require("../utils/properties-to-object");
+
+const addCategory = mapProperties({
+  category_id: "category.id",
+  category_name: "category.name",
+  category_description: "category.description",
+});
 
 function list() {
   return knex("products").select("*");
 }
 
 function read(product_id) {
-  return knex("products").select("*").where({ product_id }).first();
+  return knex("products as p")
+    .join("products_categories as pc", "p.product_id", "pc.product_id")
+    .join("categories as c", "pc.category_id", "c.category_id")
+    .select("p.*", "c.*")
+    .where({ "p.product_id": product_id })
+    .first()
+    .then(addCategory);
 }
 
 function listOutOfStockCount() {
@@ -37,42 +50,37 @@ function listTotalWeightByProduct() {
     .groupBy("product_title", "product_sku");
 }
 
-const productsSuppliersJoin = knex("products as p").join(
-  "suppliers as s",
-  "p.supplier_id",
-  "s.supplier_id"
-);
-
-const productsCategoriesJoin = knex("products as p")
-  .join("products_categories as pc", "p.product_id", "pc.product_id")
-  .join("categories as c", "pc.category_id", "c.category_id");
-
-const productsCategoriesSuppliersJoin = knex("products as p")
-  .join("products_categories as pc", "p.product_id", "pc.product_id")
-  .join("categories as c", "pc.category_id", "c.category_id")
-  .join("suppliers as s", "p.supplier_id", "s.supplier_id");
-
-
-const getProductByIdWithCategories = productId =>
-  productsCategoriesJoin
+function getProductByIdWithCategories(product_id) {
+  return knex("products as p")
+    .join("products_categories as pc", "p.product_id", "pc.product_id")
+    .join("categories as c", "pc.category_id", "c.category_id")
     .select("p.*", "c.*")
-    .where({ "p.product_id": productId })
+    .where({ "p.product_id": product_id })
     .first();
+}
 
-const getProductByIdWithSuppliers = productId =>
-  productsSuppliersJoin
+function getProductByIdWithSuppliers(product_id) {
+  return knex("products as p")
+    .join("suppliers as s", "p.supplier_id", "s.supplier_id")
     .select("p.*", "s.*")
-    .where({ "p.product_id": productId })
+    .where({ "p.product_id": product_id })
     .first();
+}
 
-const getProductByIdWithCategoriesAndSuppliers = productId =>
-  productsCategoriesSuppliersJoin
+function getProductByIdWithCategoriesAndSuppliers(product_id) {
+  return knex("products as p")
+    .join("products_categories as pc", "p.product_id", "pc.product_id")
+    .join("categories as c", "pc.category_id", "c.category_id")
+    .join("suppliers as s", "p.supplier_id", "s.supplier_id")
     .select("p.*", "c.*", "s.*")
-    .where({ "p.product_id": productId })
+    .where({ "p.product_id": product_id })
     .first();
+}
 
-const getTotalWeightOfProductsByCategory = () =>
-  productsCategoriesJoin
+function getTotalWeightOfProductsByCategory() {
+  return knex("products as p")
+    .join("products_categories as pc", "p.product_id", "pc.product_id")
+    .join("categories as c", "pc.category_id", "c.category_id")
     .select(
       "c.category_name",
       knex.raw(
@@ -81,6 +89,7 @@ const getTotalWeightOfProductsByCategory = () =>
     )
     .groupBy("c.category_name")
     .orderBy("total_weight_by_category");
+}
 
 module.exports = {
   list,
diff --git a/src/utils/map-properties.js b/src/utils/map-properties.js
new file mode 100644
index 00000000..bd49f339
--- /dev/null
+++ b/src/utils/map-properties.js
@@ -0,0 +1,14 @@
+const lodash = require("lodash");
+
+function mapProperties(configuration) {
+  return (data) => {
+    if (data) {
+      return Object.entries(data).reduce((accumulator, [key, value]) => {
+        return lodash.set(accumulator, configuration[key] || key, value);
+      }, {});
+    }
+    return data;
+  };
+}
+
+module.exports = mapProperties;

From 08324481950acff6e2242bb3b903161dce0fcf65 Mon Sep 17 00:00:00 2001
From: Gabriel Sanchez <gabrielsz87@gmail.com>
Date: Tue, 13 Apr 2021 17:56:48 -0500
Subject: [PATCH 31/35] Fix map-properties import.

---
 src/products/products.service.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/products/products.service.js b/src/products/products.service.js
index 518e6bea..6609c9d4 100644
--- a/src/products/products.service.js
+++ b/src/products/products.service.js
@@ -1,5 +1,5 @@
 const knex = require("../db/connection");
-const mapProperties = require("../utils/properties-to-object");
+const mapProperties = require("../utils/map-properties");
 
 const addCategory = mapProperties({
   category_id: "category.id",

From 4640a05216e53f968c95da84cd54a75e1730635a Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Wed, 14 Apr 2021 11:56:59 -0500
Subject: [PATCH 32/35] fix: remove dead code from products service

---
 src/products/products.service.js | 45 --------------------------------
 1 file changed, 45 deletions(-)

diff --git a/src/products/products.service.js b/src/products/products.service.js
index 6609c9d4..98f2101b 100644
--- a/src/products/products.service.js
+++ b/src/products/products.service.js
@@ -50,55 +50,10 @@ function listTotalWeightByProduct() {
     .groupBy("product_title", "product_sku");
 }
 
-function getProductByIdWithCategories(product_id) {
-  return knex("products as p")
-    .join("products_categories as pc", "p.product_id", "pc.product_id")
-    .join("categories as c", "pc.category_id", "c.category_id")
-    .select("p.*", "c.*")
-    .where({ "p.product_id": product_id })
-    .first();
-}
-
-function getProductByIdWithSuppliers(product_id) {
-  return knex("products as p")
-    .join("suppliers as s", "p.supplier_id", "s.supplier_id")
-    .select("p.*", "s.*")
-    .where({ "p.product_id": product_id })
-    .first();
-}
-
-function getProductByIdWithCategoriesAndSuppliers(product_id) {
-  return knex("products as p")
-    .join("products_categories as pc", "p.product_id", "pc.product_id")
-    .join("categories as c", "pc.category_id", "c.category_id")
-    .join("suppliers as s", "p.supplier_id", "s.supplier_id")
-    .select("p.*", "c.*", "s.*")
-    .where({ "p.product_id": product_id })
-    .first();
-}
-
-function getTotalWeightOfProductsByCategory() {
-  return knex("products as p")
-    .join("products_categories as pc", "p.product_id", "pc.product_id")
-    .join("categories as c", "pc.category_id", "c.category_id")
-    .select(
-      "c.category_name",
-      knex.raw(
-        "sum(p.product_weight_in_lbs * p.product_quantity_in_stock) as total_weight_by_category"
-      )
-    )
-    .groupBy("c.category_name")
-    .orderBy("total_weight_by_category");
-}
-
 module.exports = {
   list,
   read,
   listOutOfStockCount,
   listPriceSummary,
   listTotalWeightByProduct,
-  getProductByIdWithCategories,
-  getProductByIdWithSuppliers,
-  getProductByIdWithCategoriesAndSuppliers,
-  getTotalWeightOfProductsByCategory,
 };

From 0257902fcd3cf265a3996fdbdc10ac7da11a6b28 Mon Sep 17 00:00:00 2001
From: Dale 'Ducky' Lotts <dlotts@knightrider.com>
Date: Thu, 15 Apr 2021 09:07:59 -0500
Subject: [PATCH 33/35] fix(products): change product.category mapping to
 include category_ prefix

most fields have a talbe name prefix so this is being consistent

fix TFENG-452
---
 src/products/products.service.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/products/products.service.js b/src/products/products.service.js
index 98f2101b..0b327fcd 100644
--- a/src/products/products.service.js
+++ b/src/products/products.service.js
@@ -2,9 +2,9 @@ const knex = require("../db/connection");
 const mapProperties = require("../utils/map-properties");
 
 const addCategory = mapProperties({
-  category_id: "category.id",
-  category_name: "category.name",
-  category_description: "category.description",
+  category_id: "category.category_id",
+  category_name: "category.category_name",
+  category_description: "category.category_description",
 });
 
 function list() {

From 735e64d790d07689b06e2c7b9b2dbff7dac973fc Mon Sep 17 00:00:00 2001
From: kchia <kchia87@gmail.com>
Date: Mon, 21 Jun 2021 15:03:38 -0400
Subject: [PATCH 34/35] fix typo

---
 src/suppliers/suppliers.service.js | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/suppliers/suppliers.service.js b/src/suppliers/suppliers.service.js
index 8ae82505..87d4d5d8 100644
--- a/src/suppliers/suppliers.service.js
+++ b/src/suppliers/suppliers.service.js
@@ -1,7 +1,10 @@
 const knex = require("../db/connection");
 
 function create(supplier) {
-  return knex("suppliers").insert(supplier).returning("*");
+  return knex("suppliers")
+    .insert(supplier)
+    .returning("*")
+    .then((createdRecords) => createdRecords[0]);
 }
 
 function read(supplier_id) {
@@ -12,7 +15,8 @@ function update(updatedSupplier) {
   return knex("suppliers")
     .select("*")
     .where({ supplier_id: updatedSupplier.supplier_id })
-    .update(updatedSupplier, "*");
+    .update(updatedSupplier, "*")
+    .then((updatedRecords) => updatedRecords[0]);
 }
 
 function destroy(supplier_id) {

From 178a61a5ecabcb6f06123ae972684544cc85f061 Mon Sep 17 00:00:00 2001
From: Collin <collindewalt@gmail.com>
Date: Thu, 23 Nov 2023 22:38:45 -0500
Subject: [PATCH 35/35] Update knexfile.js

---
 knexfile.js | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/knexfile.js b/knexfile.js
index bc97b719..cd1252ff 100644
--- a/knexfile.js
+++ b/knexfile.js
@@ -13,4 +13,14 @@ module.exports = {
       directory: path.join(__dirname, "src", "db", "seeds"),
     },
   },
+  production: {
+    client: "postgresql",
+    connection: DATABASE_URL,
+    migrations: {
+      directory: path.join(__dirname, "src", "db", "migrations"),
+    },
+    seeds: {
+      directory: path.join(__dirname, "src", "db", "seeds"),
+    },
+  },
 };