From 57338355a7ba0e35927fe07bfe7a36c5e8259ab6 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 12 Oct 2019 23:32:56 +0200 Subject: [PATCH 1/4] Added Sequelize - added sequelize for database management - added sequelize models under dataaccess/datamodels - removed postgres-only stuff --- CHANGELOG.md | 3 +- package-lock.json | 624 +++++++++++++++++------- package.json | 6 +- src/app.ts | 23 +- src/default-config.yaml | 6 +- src/graphql/resolvers.ts | 18 +- src/graphql/schema.graphql | 3 + src/lib/QueryHelper.ts | 212 -------- src/lib/Route.ts | 1 + src/lib/dataaccess/ChatMessage.ts | 37 +- src/lib/dataaccess/Chatroom.ts | 60 +-- src/lib/dataaccess/DataObject.ts | 40 -- src/lib/dataaccess/Post.ts | 134 ++--- src/lib/dataaccess/Profile.ts | 168 ++++--- src/lib/dataaccess/Request.ts | 24 - src/lib/dataaccess/User.ts | 123 +---- src/lib/dataaccess/datamodels/index.ts | 12 + src/lib/dataaccess/datamodels/models.ts | 279 +++++++++++ src/lib/dataaccess/index.ts | 179 ++----- src/lib/dataaccess/wrappers.ts | 5 + src/lib/errors/graphqlErrors.ts | 1 - src/lib/globals.ts | 2 +- src/routes/home.ts | 22 +- tsconfig.json | 3 +- tslint.json | 3 +- 25 files changed, 1022 insertions(+), 966 deletions(-) delete mode 100644 src/lib/QueryHelper.ts delete mode 100644 src/lib/dataaccess/DataObject.ts delete mode 100644 src/lib/dataaccess/Request.ts create mode 100644 src/lib/dataaccess/datamodels/index.ts create mode 100644 src/lib/dataaccess/datamodels/models.ts create mode 100644 src/lib/dataaccess/wrappers.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a9eb2..a76df00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Connection to Postgres Database - Graphql Schema - default-config file and generation of config file on startup - DTOs - Home Route -- database caching - session management +- Sequelize modules and integration diff --git a/package-lock.json b/package-lock.json index 44c83f2..2249f82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,12 @@ "@types/babel-types": "*" } }, + "@types/bluebird": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.27.tgz", + "integrity": "sha512-6BmYWSBea18+tSjjSC3QIyV93ZKAeNWGM7R6aYt1ryTZXrlHF+QLV0G2yV0viEGVyRkyQsWfMoJ0k/YghBX5sQ==", + "dev": true + }, "@types/body-parser": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz", @@ -84,6 +90,15 @@ "@types/pg": "*" } }, + "@types/continuation-local-storage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/continuation-local-storage/-/continuation-local-storage-3.2.2.tgz", + "integrity": "sha512-aItm+aYPJ4rT1cHmAxO+OdWjSviQ9iB5UKb5f0Uvgln0N4hS2mcDodHtPiqicYBXViUYhqyBjhA5uyOcT+S34Q==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/cookie-parser": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.2.tgz", @@ -186,6 +201,12 @@ "integrity": "sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==", "dev": true }, + "@types/lodash": { + "version": "4.14.144", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.144.tgz", + "integrity": "sha512-ogI4g9W5qIQQUhXAclq6zhqgqNUr7UlFaqDHbch7WLSLeeM/7d3CRaw7GLajxvyFvhJqw4Rpcz5bhoaYtIx6Tg==", + "dev": true + }, "@types/markdown-it": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-0.0.9.tgz", @@ -204,8 +225,7 @@ "@types/node": { "version": "12.7.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.8.tgz", - "integrity": "sha512-FMdVn84tJJdV+xe+53sYiZS4R5yn1mAIxfj+DVoNiQjTYz1+OYmjwEZr1ev9nU0axXwda0QDbYl06QHanRVH3A==", - "dev": true + "integrity": "sha512-FMdVn84tJJdV+xe+53sYiZS4R5yn1mAIxfj+DVoNiQjTYz1+OYmjwEZr1ev9nU0axXwda0QDbYl06QHanRVH3A==" }, "@types/pg": { "version": "7.11.0", @@ -232,6 +252,18 @@ "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", "dev": true }, + "@types/sequelize": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@types/sequelize/-/sequelize-4.28.5.tgz", + "integrity": "sha512-1n2GJuTnUPzXzO4pv/m531y1lYkWjxgtv+FGIa3DDF30XZMPzXBWfDt/XpXaR64OQ/6CWwf5ZMwJ2CucO0VFvw==", + "dev": true, + "requires": { + "@types/bluebird": "*", + "@types/continuation-local-storage": "*", + "@types/lodash": "*", + "@types/validator": "*" + } + }, "@types/serve-static": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", @@ -251,6 +283,12 @@ "@types/node": "*" } }, + "@types/validator": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-10.11.3.tgz", + "integrity": "sha512-GKF2VnEkMmEeEGvoo03ocrP9ySMuX1ypKazIYMlsjfslfBMhOAtC5dmEWKdJioW4lJN7MZRS88kalTsVClyQ9w==", + "dev": true + }, "@types/winston": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.4.4.tgz", @@ -263,8 +301,7 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "accepts": { "version": "1.3.7", @@ -304,7 +341,6 @@ "version": "6.10.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -374,8 +410,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "3.2.1", @@ -392,6 +427,11 @@ "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", "dev": true }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -414,8 +454,7 @@ "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "archy": { "version": "1.0.0", @@ -427,7 +466,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -437,7 +475,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -451,14 +488,12 @@ "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==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -612,7 +647,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, "requires": { "safer-buffer": "~2.1.0" } @@ -620,8 +654,7 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "assign-symbols": { "version": "1.0.0", @@ -684,8 +717,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.2", @@ -696,14 +728,12 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "babel-code-frame": { "version": "6.26.0", @@ -802,8 +832,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -885,7 +914,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, "requires": { "tweetnacl": "^0.14.3" } @@ -918,6 +946,11 @@ "inherits": "~2.0.0" } }, + "bluebird": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.0.tgz", + "integrity": "sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg==" + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -946,7 +979,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1048,8 +1080,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "center-align": { "version": "0.1.3", @@ -1116,6 +1147,11 @@ } } }, + "chownr": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", + "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==" + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -1237,11 +1273,19 @@ } } }, + "cls-bluebird": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz", + "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=", + "requires": { + "is-bluebird": "^1.0.2", + "shimmer": "^1.1.0" + } + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "collection-map": { "version": "1.0.0", @@ -1324,7 +1368,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -1388,8 +1431,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -1443,11 +1485,34 @@ "pg": "^7.4.3" } }, + "connect-session-sequelize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/connect-session-sequelize/-/connect-session-sequelize-6.0.0.tgz", + "integrity": "sha512-XC71xJd5rqObdL7700S/qFD+gSRA4o6WVJAyFY0Vjah73id5bBElM0SHQR1ME5Bxrt4JL8alvggseNDVTlKyxA==", + "requires": { + "debug": "^3.1.0", + "deep-equal": "^1.0.1" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "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==" + } + } + }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "constantinople": { "version": "3.1.2", @@ -1599,7 +1664,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -1623,6 +1687,24 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.0.tgz", + "integrity": "sha512-ZbfWJq/wN1Z273o7mUSjILYqehAktR2NVoSrOukDkU9kg2v/Uv89yU4Cvz8seJeAmtN5oqiefKq8FPuXOboqLw==", + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "default-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", @@ -1650,7 +1732,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -1705,14 +1786,12 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "delete": { "version": "1.1.0", @@ -1742,6 +1821,11 @@ "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", "dev": true }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, "diagnostics": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", @@ -1763,6 +1847,11 @@ "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" }, + "dottie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.1.tgz", + "integrity": "sha512-ch5OQgvGDK2u8pSZeSYAQaV/lczImd7pMJ7BcEPXmnFVjy4yJIzP6CsODJUTH8mg1tyH1Z2abOiuJO3DjZ/GBw==" + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -1821,7 +1910,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -2197,8 +2285,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extend-shallow": { "version": "2.0.1", @@ -2274,8 +2361,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fancy-log": { "version": "1.3.3", @@ -2292,14 +2378,12 @@ "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "fast-safe-stringify": { "version": "2.0.6", @@ -2449,14 +2533,12 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -2492,6 +2574,14 @@ "universalify": "^0.1.0" } }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "requires": { + "minipass": "^2.6.0" + } + }, "fs-mkdirp-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", @@ -2547,8 +2637,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.2.9", @@ -2570,8 +2659,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -2592,14 +2680,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2614,20 +2700,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -2744,8 +2827,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -2757,7 +2839,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2772,7 +2853,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2780,14 +2860,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2806,7 +2884,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -2887,8 +2964,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -2900,7 +2976,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -2986,8 +3061,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -3023,7 +3097,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3043,7 +3116,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3087,14 +3159,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -3124,7 +3194,6 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -3140,7 +3209,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3178,7 +3246,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -3187,7 +3254,6 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3573,14 +3639,12 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" @@ -3647,8 +3711,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "has-value": { "version": "1.0.0", @@ -3720,7 +3783,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -3745,6 +3807,14 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "requires": { + "minimatch": "^3.0.4" + } + }, "in-publish": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", @@ -3765,11 +3835,15 @@ "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" }, + "inflection": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -3783,8 +3857,7 @@ "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": "1.2.0", @@ -3833,6 +3906,11 @@ } } }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3848,6 +3926,11 @@ "binary-extensions": "^1.0.0" } }, + "is-bluebird": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz", + "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" + }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -3873,6 +3956,11 @@ } } }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", @@ -3933,7 +4021,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4012,8 +4099,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "is-unc-path": { "version": "1.0.0", @@ -4062,8 +4148,7 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "iterall": { "version": "1.2.2", @@ -4099,20 +4184,17 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -4123,8 +4205,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "jsonfile": { "version": "4.0.0", @@ -4138,7 +4219,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -4544,7 +4624,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4552,8 +4631,31 @@ "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "requires": { + "minipass": "^2.9.0" + } }, "mixin-deep": { "version": "1.3.2", @@ -4580,7 +4682,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" }, @@ -4588,16 +4689,22 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, "moment": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==", - "dev": true + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "moment-timezone": { + "version": "0.5.26", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.26.tgz", + "integrity": "sha512-sFP4cgEKTCymBBKgoxZjYzlSovC20Y6J7y3nanDc5RoBIXKlZhoYwBoZGe3flwU6A372AcRwScH8KiwV6zjy1g==", + "requires": { + "moment": ">= 2.9.0" + } }, "ms": { "version": "2.0.0", @@ -4613,8 +4720,7 @@ "nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, "nanomatch": { "version": "1.2.13", @@ -4662,6 +4768,31 @@ } } }, + "needle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "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==" + } + } + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -4701,6 +4832,53 @@ } } }, + "node-pre-gyp": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, + "dependencies": { + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, "node-sass": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", @@ -4801,11 +4979,24 @@ "once": "^1.3.2" } }, + "npm-bundled": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" + }, + "npm-packlist": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.6.tgz", + "integrity": "sha512-u65uQdb+qwtGvEJh/DgQgW1Xg7sqeNbmxYyrvlNznaVTjV3E5P6F/EFjM+BVHXl7JJlsdG8A64M0XI8FI/IOlg==", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -4816,14 +5007,12 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "object-assign": { "version": "4.1.1", @@ -4866,11 +5055,15 @@ } } }, + "object-is": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", + "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=" + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -4959,7 +5152,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -5031,8 +5223,7 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-locale": { "version": "1.4.0", @@ -5046,14 +5237,12 @@ "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -5141,8 +5330,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-parse": { "version": "1.0.6", @@ -5183,8 +5371,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pg": { "version": "7.12.1", @@ -5371,8 +5558,7 @@ "psl": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz", - "integrity": "sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag==", - "dev": true + "integrity": "sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag==" }, "pug": { "version": "2.0.4", @@ -5512,14 +5698,12 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "random-bytes": { "version": "1.0.0", @@ -5542,6 +5726,17 @@ "unpipe": "1.0.0" } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -5671,6 +5866,14 @@ } } }, + "regexp.prototype.flags": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", + "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", + "requires": { + "define-properties": "^1.1.2" + } + }, "remove-bom-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", @@ -5791,7 +5994,6 @@ "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -5871,6 +6073,14 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, + "retry-as-promised": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-3.2.0.tgz", + "integrity": "sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg==", + "requires": { + "any-promise": "^1.3.0" + } + }, "right-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", @@ -5883,7 +6093,6 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, "requires": { "glob": "^7.1.3" } @@ -5919,6 +6128,11 @@ "yargs": "^7.0.0" } }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "scss-tokenizer": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", @@ -5943,8 +6157,7 @@ "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "semver-greatest-satisfied-range": { "version": "1.1.0", @@ -5982,6 +6195,53 @@ } } }, + "sequelize": { + "version": "5.19.6", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-5.19.6.tgz", + "integrity": "sha512-LPDD+v+iEmjwIw4HveoWR0OZXz0PFH74wmL+wLSSqQFYWRQCi6ml0n9XTRsVDCdVXA1VwUcwvRAaDkyQTvSmRA==", + "requires": { + "bluebird": "^3.5.0", + "cls-bluebird": "^2.1.0", + "debug": "^4.1.1", + "dottie": "^2.0.0", + "inflection": "1.12.0", + "lodash": "^4.17.15", + "moment": "^2.24.0", + "moment-timezone": "^0.5.21", + "retry-as-promised": "^3.2.0", + "semver": "^6.3.0", + "sequelize-pool": "^2.3.0", + "toposort-class": "^1.0.1", + "uuid": "^3.3.3", + "validator": "^10.11.0", + "wkx": "^0.4.8" + }, + "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==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "sequelize-pool": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-2.3.0.tgz", + "integrity": "sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA==" + }, "serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", @@ -5996,8 +6256,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-value": { "version": "2.0.1", @@ -6016,11 +6275,15 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "simple-swizzle": { "version": "0.2.2", @@ -6370,11 +6633,20 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "sqlite3": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.0.tgz", + "integrity": "sha512-RvqoKxq+8pDHsJo7aXxsFR18i+dU2Wp5o12qAJOV5LNcDt+fgJsc2QKKg3sIRfXrN9ZjzY1T7SNe/DFVqAXjaw==", + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.11.0", + "request": "^2.87.0" + } + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -6475,7 +6747,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6486,7 +6757,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6536,6 +6806,11 @@ "get-stdin": "^4.0.1" } }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -6806,11 +7081,15 @@ "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" }, + "toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" @@ -6819,8 +7098,7 @@ "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" } } }, @@ -6940,7 +7218,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, "requires": { "safe-buffer": "^5.0.1" } @@ -6948,8 +7225,7 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type": { "version": "1.2.0", @@ -7156,7 +7432,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, "requires": { "punycode": "^2.1.0" } @@ -7186,8 +7461,7 @@ "uuid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", - "dev": true + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" }, "v8flags": { "version": "3.1.3", @@ -7208,6 +7482,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", + "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" + }, "value-or-function": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", @@ -7223,7 +7502,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -7367,7 +7645,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, "requires": { "string-width": "^1.0.2 || 2" } @@ -7440,6 +7717,14 @@ "acorn-globals": "^3.0.0" } }, + "wkx": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.8.tgz", + "integrity": "sha512-ikPXMM9IR/gy/LwiOSqWlSL3X/J5uk9EO2hHNRXS41eTLXaUFEVw9fn/593jW/tE5tedNg8YjT5HkCa4FqQZyQ==", + "requires": { + "@types/node": "*" + } + }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", @@ -7470,8 +7755,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { "version": "6.1.4", diff --git a/package.json b/package.json index cd1a34f..c47a635 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@types/markdown-it": "0.0.9", "@types/node": "^12.7.8", "@types/pg": "^7.11.0", + "@types/sequelize": "^4.28.5", "@types/socket.io": "^2.1.2", "@types/winston": "^2.4.4", "delete": "^1.1.0", @@ -49,7 +50,7 @@ }, "dependencies": { "compression": "^1.7.4", - "connect-pg-simple": "^6.0.1", + "connect-session-sequelize": "^6.0.0", "cookie-parser": "^1.4.4", "cors": "^2.8.5", "express": "^4.17.1", @@ -57,7 +58,6 @@ "express-session": "^1.16.2", "express-socket.io-session": "^1.3.5", "fs-extra": "^8.1.0", - "g": "^2.0.1", "graphql": "^14.4.2", "graphql-import": "^0.7.1", "http-status": "^1.3.2", @@ -66,7 +66,9 @@ "markdown-it-emoji": "^1.4.0", "pg": "^7.12.1", "pug": "^2.0.4", + "sequelize": "^5.19.6", "socket.io": "^2.2.0", + "sqlite3": "^4.1.0", "winston": "^3.2.1" } } diff --git a/src/app.ts b/src/app.ts index fe01b63..dabc2ec 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,4 @@ import * as compression from "compression"; -import connectPgSimple = require("connect-pg-simple"); import * as cookieParser from "cookie-parser"; import * as cors from "cors"; import * as express from "express"; @@ -10,33 +9,35 @@ import {buildSchema} from "graphql"; import {importSchema} from "graphql-import"; import * as http from "http"; import * as path from "path"; +import {Sequelize} from "sequelize"; import * as socketIo from "socket.io"; import {resolver} from "./graphql/resolvers"; -import dataaccess, {queryHelper} from "./lib/dataaccess"; +import dataaccess from "./lib/dataaccess"; import globals from "./lib/globals"; import routes from "./routes"; +import * as fsx from "fs-extra"; +const SequelizeStore = require("connect-session-sequelize")(session.Store); const logger = globals.logger; -const PgSession = connectPgSimple(session); - class App { public app: express.Application; public io: socketIo.Server; public server: http.Server; + public readonly sequelize: Sequelize; constructor() { this.app = express(); this.server = new http.Server(this.app); this.io = socketIo(this.server); + this.sequelize = new Sequelize(globals.config.database.connectionUri); } /** * initializes everything that needs to be initialized asynchronous. */ public async init() { - await dataaccess.init(); - await routes.ioListeners(this.io); + await dataaccess.init(this.sequelize); const appSession = session({ cookie: { @@ -46,12 +47,14 @@ class App { resave: false, saveUninitialized: false, secret: globals.config.session.secret, - store: new PgSession({ - pool: dataaccess.pool, - tableName: "user_sessions", - }), + store: new SequelizeStore({db: this.sequelize}), }); + const force = fsx.existsSync("sqz-force"); + logger.info(`Sequelize Table force: ${force}`); + await this.sequelize.sync({force, logging: (msg) => logger.silly(msg)}); + await routes.ioListeners(this.io); + this.io.use(sharedsession(appSession, {autoSave: true})); this.app.set("views", path.join(__dirname, "views")); diff --git a/src/default-config.yaml b/src/default-config.yaml index 0996432..58a8ecc 100644 --- a/src/default-config.yaml +++ b/src/default-config.yaml @@ -1,10 +1,6 @@ # database connection info database: - host: - port: - user: - password: - database: + connectionUri: "sqlite://:memory:" # http server configuration server: diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 7b35d4b..23745e3 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -1,7 +1,9 @@ import {GraphQLError} from "graphql"; import * as status from "http-status"; +import {Sequelize} from "sequelize"; import dataaccess from "../lib/dataaccess"; import {Chatroom} from "../lib/dataaccess/Chatroom"; +import * as models from "../lib/dataaccess/datamodels"; import {Post} from "../lib/dataaccess/Post"; import {Profile} from "../lib/dataaccess/Profile"; import {User} from "../lib/dataaccess/User"; @@ -17,9 +19,10 @@ import {is} from "../lib/regex"; */ export function resolver(req: any, res: any): any { return { - getSelf() { + async getSelf() { if (req.session.userId) { - return new Profile(req.session.userId); + const user = await models.SqUser.findByPk(req.session.userId); + return user.profile; } else { res.status(status.UNAUTHORIZED); return new NotLoggedInGqlError(); @@ -29,7 +32,8 @@ export function resolver(req: any, res: any): any { if (handle) { return await dataaccess.getUserByHandle(handle); } else if (userId) { - return new User(userId); + const user = await models.SqUser.findByPk(userId); + return user.user; } else { res.status(status.BAD_REQUEST); return new GraphQLError("No userId or handle provided."); @@ -45,7 +49,8 @@ export function resolver(req: any, res: any): any { }, async getChat({chatId}: { chatId: number }) { if (chatId) { - return new Chatroom(chatId); + const chat = await models.SqChat.findByPk(chatId); + return new Chatroom(chat); } else { res.status(status.BAD_REQUEST); return new GraphQLError("No chatId given."); @@ -105,7 +110,8 @@ export function resolver(req: any, res: any): any { async vote({postId, type}: { postId: number, type: dataaccess.VoteType }) { if (postId && type) { if (req.session.userId) { - return await (new Post(postId)).vote(req.session.userId, type); + const post = await models.SqPost.findByPk(postId); + return await (post.post).vote(req.session.userId, type); } else { res.status(status.UNAUTHORIZED); return new NotLoggedInGqlError(); @@ -132,7 +138,7 @@ export function resolver(req: any, res: any): any { }, async deletePost({postId}: { postId: number }) { if (postId) { - const post = new Post(postId); + const post = (await models.SqPost.findByPk(postId)).post; if ((await post.author()).id === req.session.userId) { return await dataaccess.deletePost(post.id); } else { diff --git a/src/graphql/schema.graphql b/src/graphql/schema.graphql index a33be9a..681b0f4 100644 --- a/src/graphql/schema.graphql +++ b/src/graphql/schema.graphql @@ -206,6 +206,9 @@ type ChatRoom { } type ChatMessage { + "Id of the chat message" + id: ID! + "The author of the chat message." author: User! diff --git a/src/lib/QueryHelper.ts b/src/lib/QueryHelper.ts deleted file mode 100644 index 83b36fc..0000000 --- a/src/lib/QueryHelper.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * @author Trivernis - * @remarks - * - * Taken from {@link https://github.com/Trivernis/whooshy} - */ - -import * as fsx from "fs-extra"; -import {Pool, PoolClient, QueryConfig, QueryResult} from "pg"; -import globals from "./globals"; - -const logger = globals.logger; - -export interface IAdvancedQueryConfig extends QueryConfig { - cache?: boolean; -} - -/** - * Transaction class to wrap SQL transactions. - */ -export class SqlTransaction { - /** - * Constructor. - * @param client - */ - constructor(private client: PoolClient) { - } - - /** - * Begins the transaction. - */ - public async begin() { - return await this.client.query("BEGIN"); - } - - /** - * Commits the transaction - */ - public async commit() { - return await this.client.query("COMMIT"); - } - - /** - * Rolls back the transaction - */ - public async rollback() { - return await this.client.query("ROLLBACK"); - } - - /** - * Executes a query inside the transaction. - * @param query - */ - public async query(query: QueryConfig) { - return await this.client.query(query); - } - - /** - * Releases the client back to the pool. - */ - public release() { - this.client.release(); - } -} - -/** - * Query helper for easyer fetching of a specific row count. - */ -export class QueryHelper { - private pool: Pool; - - /** - * Constructor. - * @param pgPool - * @param [tableCreationFile] - * @param [tableUpdateFile] - */ - constructor(pgPool: Pool, private tableCreationFile?: string, private tableUpdateFile?: string) { - this.pool = pgPool; - } - - /** - * Async init function - */ - public async init() { - await this.pool.connect(); - await this.createTables(); - await this.updateTableDefinitions(); - } - - /** - * creates all tables needed if a filepath was given with the constructor - */ - public async createTables() { - if (this.tableCreationFile) { - logger.info("Creating nonexistent tables..."); - const tableSql = await fsx.readFile(this.tableCreationFile, "utf-8"); - const trans = await this.createTransaction(); - await trans.begin(); - try { - await trans.query({text: tableSql}); - await trans.commit(); - } catch (err) { - globals.logger.error(`Error on table creation ${err.message}`); - globals.logger.debug(err.stack); - await trans.rollback(); - } finally { - trans.release(); - } - } - } - - /** - * Updates the definition of the tables if the table update file was passed in the constructor - */ - public async updateTableDefinitions() { - if (this.tableUpdateFile) { - logger.info("Updating table definitions..."); - const tableSql = await fsx.readFile(this.tableUpdateFile, "utf-8"); - const trans = await this.createTransaction(); - await trans.begin(); - try { - await trans.query({text: tableSql}); - await trans.commit(); - } catch (err) { - globals.logger.error(`Error on table update ${err.message}`); - globals.logger.debug(err.stack); - await trans.rollback(); - } finally { - trans.release(); - } - } - } - - /** - * executes the sql query with values and returns all results. - * @param query - */ - public async all(query: IAdvancedQueryConfig): Promise { - const result = await this.query(query); - return result.rows; - } - - /** - * executes the sql query with values and returns the first result. - * @param query - */ - public async first(query: IAdvancedQueryConfig): Promise { - const result = await this.query(query); - if (result.rows && result.rows.length > 0) { - return result.rows[0]; - } - } - - /** - * Creates a new Transaction to be uses with error handling. - */ - public async createTransaction() { - const client: PoolClient = await this.pool.connect(); - return new SqlTransaction(client); - } - - /** - * Queries the database with error handling. - * @param query - the sql and values to execute - */ - private async query(query: IAdvancedQueryConfig): Promise { - try { - query.text = query.text.replace(/[\r\n]/g, " "); - globals.logger.silly(`Executing sql '${JSON.stringify(query)}'`); - - if (query.cache) { - const key = globals.cache.hashKey(JSON.stringify(query)); - const cacheResult = globals.cache.get(key); - if (cacheResult) { - return cacheResult; - } else { - const result = await this.pool.query(query); - globals.cache.set(key, result); - return result; - } - } else { - return await this.pool.query(query); - } - } catch (err) { - logger.debug(`Error on query "${JSON.stringify(query)}".`); - logger.error(`Sql query failed: ${err}`); - logger.verbose(err.stack); - return { - rows: null, - }; - } - } -} - -/** - * Returns the parameterized value sql for inserting - * @param columnCount - * @param rowCount - * @param [offset] - */ -export function buildSqlParameters(columnCount: number, rowCount: number, offset?: number): string { - let sql = ""; - for (let i = 0; i < rowCount; i++) { - sql += "("; - for (let j = 0; j < columnCount; j++) { - sql += `$${(i * columnCount) + j + 1 + offset},`; - } - sql = sql.replace(/,$/, "") + "),"; - } - return sql.replace(/,$/, ""); -} diff --git a/src/lib/Route.ts b/src/lib/Route.ts index 37bed62..be4988e 100644 --- a/src/lib/Route.ts +++ b/src/lib/Route.ts @@ -20,6 +20,7 @@ abstract class Route { protected ions?: Namespace; public abstract async init(...params: any): Promise; + public abstract async destroy(...params: any): Promise; } diff --git a/src/lib/dataaccess/ChatMessage.ts b/src/lib/dataaccess/ChatMessage.ts index 4c91a16..1e42a05 100644 --- a/src/lib/dataaccess/ChatMessage.ts +++ b/src/lib/dataaccess/ChatMessage.ts @@ -1,31 +1,38 @@ import markdown from "../markdown"; import {Chatroom} from "./Chatroom"; +import * as models from "./datamodels/models"; import {User} from "./User"; export class ChatMessage { - constructor( - public readonly author: User, - public readonly chat: Chatroom, - public readonly createdAt: number, - public readonly content: string) {} + + public id: number; + public content: string; + public createdAt: Date; + + constructor(private message: models.ChatMessage) { + this.id = message.id; + this.content = message.content; + this.createdAt = message.createdAt; + } + + /** + * returns the author of the chat message. + */ + public async author(): Promise { + return new User(await this.message.getAuthor()); + } /** - * The content rendered by markdown-it. + * Returns the rendered html content of the chat message. */ public htmlContent(): string { return markdown.renderInline(this.content); } /** - * Returns resolved and rendered content of the chat message. + * returns the chatroom for the chatmessage. */ - public resolvedContent() { - return { - author: this.author.id, - chat: this.chat.id, - content: this.content, - createdAt: this.createdAt, - htmlContent: this.htmlContent(), - }; + public async chat(): Promise { + return (await this.message.getChat()).chatroom; } } diff --git a/src/lib/dataaccess/Chatroom.ts b/src/lib/dataaccess/Chatroom.ts index e541f06..3e08389 100644 --- a/src/lib/dataaccess/Chatroom.ts +++ b/src/lib/dataaccess/Chatroom.ts @@ -1,44 +1,22 @@ -import globals from "../globals"; -import {ChatMessage} from "./ChatMessage"; -import {queryHelper} from "./index"; +import {SqChat} from "./datamodels"; import {User} from "./User"; export class Chatroom { + public readonly id: number; public namespace: string; - constructor(public readonly id: number) { - this.id = Number(id); - this.namespace = `/chat/${id}`; - } - /** - * Returns if the chat exists. - */ - public async exists(): Promise { - const result = await queryHelper.first({ - text: "SELECT id FROM chats WHERE id = $1", - values: [this.id], - }); - return !!result.id; + constructor(private chat: SqChat) { + this.id = chat.id; + this.namespace = `/chat/${chat.id}`; } /** * Returns all members of a chatroom. */ public async members(): Promise { - const result = await queryHelper.all({ - cache: true, - text: `SELECT * FROM chat_members - JOIN users ON (chat_members.member = users.id) - WHERE chat_members.chat = $1;`, - values: [this.id], - }); - const chatMembers = []; - for (const row of result) { - const user = new User(row.id, row); - chatMembers.push(user); - } - return chatMembers; + const members = await this.chat.getMembers(); + return members.map((m) => new User(m)); } /** @@ -47,30 +25,14 @@ export class Chatroom { * @param offset - the offset of messages to return * @param containing - filter by containing */ - public async messages({first, offset, containing}: {first?: number, offset?: number, containing?: string}) { + public async messages({first, offset, containing}: { first?: number, offset?: number, containing?: string }) { const lim = first || 16; const offs = offset || 0; - - const result = await queryHelper.all({ - cache: true, - text: "SELECT * FROM chat_messages WHERE chat = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", - values: [this.id, lim, offs], - }); - - const messages = []; - const users: any = {}; - for (const row of result) { - if (!users[row.author]) { - const user = new User(row.author); - await user.exists(); - users[row.author] = user; - } - messages.push(new ChatMessage(users[row.author], this, row.created_at, row.content)); - } + const messages = await this.chat.getMessages({limit: lim, offset: offs}); if (containing) { - return messages.filter((x) => x.content.includes(containing)); + return messages.filter((x) => x.content.includes(containing)).map((m) => m.message); } else { - return messages; + return messages.map((m) => m.message); } } } diff --git a/src/lib/dataaccess/DataObject.ts b/src/lib/dataaccess/DataObject.ts deleted file mode 100644 index 26fc809..0000000 --- a/src/lib/dataaccess/DataObject.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * abstact DataObject class - */ -import {EventEmitter} from "events"; - -export abstract class DataObject extends EventEmitter { - protected dataLoaded: boolean = false; - private loadingData: boolean = false; - - constructor(public id: number, protected row?: any) { - super(); - this.id = Number(id); - } - - /** - * Returns if the object extists by trying to load data. - */ - public async exists() { - await this.loadDataIfNotExists(); - return this.dataLoaded; - } - - protected abstract loadData(): Promise; - - /** - * Loads data from the database if data has not been loaded - */ - protected async loadDataIfNotExists() { - if (!this.dataLoaded && !this.loadingData) { - this.loadingData = true; - await this.loadData(); - this.loadingData = false; - this.emit("loaded"); - } else if (this.loadingData) { - return new Promise((res) => { - this.on("loaded", () => res()); - }); - } - } -} diff --git a/src/lib/dataaccess/Post.ts b/src/lib/dataaccess/Post.ts index e4912c7..1105499 100644 --- a/src/lib/dataaccess/Post.ts +++ b/src/lib/dataaccess/Post.ts @@ -1,106 +1,68 @@ import markdown from "../markdown"; -import {DataObject} from "./DataObject"; -import {queryHelper} from "./index"; +import {SqPost, SqPostVotes} from "./datamodels"; +import {PostVotes} from "./datamodels/models"; import dataaccess from "./index"; import {User} from "./User"; -export class Post extends DataObject { +export class Post { public readonly id: number; - private $createdAt: string; - private $content: string; - private $author: number; - private $type: string; + public createdAt: Date; + public content: string; + public type: string; - /** - * Returns the resolved data of the post. - */ - public async resolvedData() { - await this.loadDataIfNotExists(); - return { - authorId: this.$author, - content: this.$content, - createdAt: this.$createdAt, - id: this.id, - type: this.$type, - }; + private post: SqPost; + + constructor(post: SqPost) { + this.id = post.id; + this.createdAt = post.createdAt; + this.post = post; + this.type = ""; + this.content = post.content; } + /** * Returns the upvotes of a post. */ public async upvotes(): Promise { - const result = await queryHelper.first({ - cache: true, - text: "SELECT COUNT(*) count FROM votes WHERE item_id = $1 AND vote_type = 'UPVOTE'", - values: [this.id], - }); - return result.count; + return PostVotes.count({where: {voteType: dataaccess.VoteType.UPVOTE, post_id: this.id}}); } /** * Returns the downvotes of the post */ public async downvotes(): Promise { - const result = await queryHelper.first({ - cache: true, - text: "SELECT COUNT(*) count FROM votes WHERE item_id = $1 AND vote_type = 'DOWNVOTE'", - values: [this.id], - }); - return result.count; - } - - /** - * The content of the post (markdown) - */ - public async content(): Promise { - await this.loadDataIfNotExists(); - return this.$content; + return PostVotes.count({where: {voteType: dataaccess.VoteType.DOWNVOTE, post_id: this.id}}); } /** * the content rendered by markdown-it. */ public async htmlContent(): Promise { - await this.loadDataIfNotExists(); - return markdown.render(this.$content); - } - - /** - * The date the post was created at. - */ - public async createdAt(): Promise { - await this.loadDataIfNotExists(); - return this.$createdAt; + return markdown.render(this.content); } /** * The autor of the post. */ public async author(): Promise { - await this.loadDataIfNotExists(); - return new User(this.$author); + return new User(await this.post.getUser()); } /** * Deletes the post. */ public async delete(): Promise { - const query = await queryHelper.first({ - text: "DELETE FROM posts WHERE id = $1", - values: [this.id], - }); + await this.post.destroy(); } /** * The type of vote the user performed on the post. */ public async userVote(userId: number): Promise { - const result = await queryHelper.first({ - cache: true, - text: "SELECT vote_type FROM votes WHERE user_id = $1 AND item_id = $2", - values: [userId, this.id], - }); - if (result) { - return result.vote_type; + const votes = await this.post.getVotes({where: {userId}}); + + if (votes.length >= 1) { + return votes[0].voteType; } else { return null; } @@ -112,48 +74,10 @@ export class Post extends DataObject { * @param type */ public async vote(userId: number, type: dataaccess.VoteType): Promise { - const uVote = await this.userVote(userId); - if (uVote === type) { - await queryHelper.first({ - text: "DELETE FROM votes WHERE item_id = $1 AND user_id = $2", - values: [this.id, userId], - }); - } else { - if (uVote) { - await queryHelper.first({ - text: "UPDATE votes SET vote_type = $1 WHERE user_id = $2 AND item_id = $3", - values: [type, userId, this.id], - }); - } else { - await queryHelper.first({ - text: "INSERT INTO votes (user_id, item_id, vote_type) values ($1, $2, $3)", - values: [userId, this.id, type], - }); - } - return type; - } - } - - /** - * Loads the data from the database if needed. - */ - protected async loadData(): Promise { - let result: any; - if (this.row) { - result = this.row; - } else { - result = await queryHelper.first({ - cache: true, - text: "SELECT * FROM posts WHERE posts.id = $1", - values: [this.id], - }); - } - if (result) { - this.$author = result.author; - this.$content = result.content; - this.$createdAt = result.created_at; - this.$type = result.type; - this.dataLoaded = true; - } + const [vote, _] = await SqPostVotes + .findOrCreate({where: {userId}, defaults: {voteType: type, postId: this.post.id}}); + vote.voteType = type; + await vote.save(); + return vote.voteType; } } diff --git a/src/lib/dataaccess/Profile.ts b/src/lib/dataaccess/Profile.ts index 090e6da..a9eaba2 100644 --- a/src/lib/dataaccess/Profile.ts +++ b/src/lib/dataaccess/Profile.ts @@ -1,10 +1,61 @@ import {RequestNotFoundError} from "../errors/RequestNotFoundError"; import {Chatroom} from "./Chatroom"; -import dataaccess, {queryHelper} from "./index"; -import {User} from "./User"; -import {Request} from "./Request"; +import {SqUser} from "./datamodels"; +import dataaccess from "./index"; +import * as wrappers from "./wrappers"; -export class Profile extends User { +export class Profile { + + public id: number; + public name: string; + public handle: string; + public email: string; + public greenpoints: number; + public joinedAt: Date; + + protected user: SqUser; + + constructor(user: SqUser) { + this.name = user.username; + this.handle = user.handle; + this.email = user.email; + this.greenpoints = user.rankpoints; + this.joinedAt = user.joinedAt; + this.id = user.id; + this.user = user; + } + + /** + * Returns the number of posts the user created + */ + public async numberOfPosts(): Promise { + return this.user.countPosts(); + } + + /** + * Returns all friends of the user. + */ + public async friends(): Promise { + const result = await this.user.getFriends(); + const userFriends = []; + for (const friend of result) { + userFriends.push(new wrappers.User(friend)); + } + return userFriends; + } + + /** + * Returns all posts for a user. + */ + public async posts({first, offset}: { first: number, offset: number }): Promise { + const postRes = await this.user.getPosts(); + const posts = []; + + for (const post of postRes) { + posts.push(new wrappers.Post(post)); + } + return posts; + } /** * Returns all chatrooms (with pagination). @@ -12,19 +63,14 @@ export class Profile extends User { * @param first * @param offset */ - public async chats({first, offset}: {first: number, offset?: number}): Promise { - if (!(await this.exists())) { - return []; - } + public async chats({first, offset}: { first: number, offset?: number }): Promise { first = first || 10; offset = offset || 0; - const result = await queryHelper.all({ - text: "SELECT chat FROM chat_members WHERE member = $1 LIMIT $2 OFFSET $3", - values: [this.id, first, offset], - }); + const result = await this.user.getChats(); + if (result) { - return result.map((row) => new Chatroom(row.chat)); + return result.map((chat) => new Chatroom(chat)); } else { return []; } @@ -34,24 +80,14 @@ export class Profile extends User { * Returns all open requests the user has send. */ public async sentRequests() { - const result = await queryHelper.all({ - cache: true, - text: "SELECT * FROM requests WHERE sender = $1", - values: [this.id], - }); - return this.getRequests(result); + return this.user.getSentRequests(); } /** * Returns all received requests of the user. */ public async receivedRequests() { - const result = await queryHelper.all({ - cache: true, - text: "SELECT * FROM requests WHERE receiver = $1", - values: [this.id], - }); - return this.getRequests(result); + return this.user.getReceivedRequests(); } /** @@ -59,11 +95,9 @@ export class Profile extends User { * @param points */ public async setGreenpoints(points: number): Promise { - const result = await queryHelper.first({ - text: "UPDATE users SET greenpoints = $1 WHERE id = $2 RETURNING greenpoints", - values: [points, this.id], - }); - return result.greenpoints; + this.user.rankpoints = points; + await this.user.save(); + return this.user.rankpoints; } /** @@ -71,22 +105,18 @@ export class Profile extends User { * @param email */ public async setEmail(email: string): Promise { - const result = await queryHelper.first({ - text: "UPDATE users SET email = $1 WHERE users.id = $2 RETURNING email", - values: [email, this.id], - }); - return result.email; + this.user.email = email; + await this.user.save(); + return this.user.email; } /** * Updates the handle of the user */ public async setHandle(handle: string): Promise { - const result = await queryHelper.first({ - text: "UPDATE users SET handle = $1 WHERE id = $2", - values: [handle, this.id], - }); - return result.handle; + this.user.handle = handle; + await this.user.save(); + return this.user.handle; } /** @@ -94,11 +124,9 @@ export class Profile extends User { * @param name */ public async setName(name: string): Promise { - const result = await queryHelper.first({ - text: "UPDATE users SET name = $1 WHERE id = $2", - values: [name, this.id], - }); - return result.name; + this.user.username = name; + await this.user.save(); + return this.user.username; } /** @@ -107,10 +135,10 @@ export class Profile extends User { * @param type */ public async denyRequest(sender: number, type: dataaccess.RequestType) { - await queryHelper.first({ - text: "DELETE FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3", - values: [this.id, sender, type], - }); + const request = await this.user.getReceivedRequests({where: {senderId: sender, requestType: type}}); + if (request[0]) { + await request[0].destroy(); + } } /** @@ -119,45 +147,15 @@ export class Profile extends User { * @param type */ public async acceptRequest(sender: number, type: dataaccess.RequestType) { - const exists = await queryHelper.first({ - cache: true, - text: "SELECT 1 FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3", - values: [this.id, sender, type], - }); - if (exists) { - if (type === dataaccess.RequestType.FRIENDREQUEST) { - await queryHelper.first({ - text: "INSERT INTO user_friends (user_id, friend_id) VALUES ($1, $2)", - values: [this.id, sender], - }); + const requests = await this.user.getReceivedRequests({where: {senderId: sender, requestType: type}}); + if (requests.length > 0) { + const request = requests[0]; + if (request.requestType === dataaccess.RequestType.FRIENDREQUEST) { + await this.user.addFriend(sender); + await request.destroy(); } } else { throw new RequestNotFoundError(sender, this.id, type); } } - - /** - * Returns request wrapper for a row database request result. - * @param rows - */ - private getRequests(rows: any) { - const requests = []; - const requestUsers: any = {}; - - for (const row of rows) { - let sender = requestUsers[row.sender]; - - if (!sender) { - sender = new User(row.sender); - requestUsers[row.sender] = sender; - } - let receiver = requestUsers[row.receiver]; - if (!receiver) { - receiver = new User(row.receiver); - requestUsers[row.receiver] = receiver; - } - requests.push(new Request(sender, receiver, row.type)); - } - return requests; - } } diff --git a/src/lib/dataaccess/Request.ts b/src/lib/dataaccess/Request.ts deleted file mode 100644 index 7e8f1bc..0000000 --- a/src/lib/dataaccess/Request.ts +++ /dev/null @@ -1,24 +0,0 @@ -import dataaccess from "./index"; -import {User} from "./User"; - -/** - * Represents a request to a user. - */ -export class Request { - constructor( - public readonly sender: User, - public readonly receiver: User, - public readonly type: dataaccess.RequestType) { - } - - /** - * Returns the resolved request data. - */ - public resolvedData() { - return { - receiverId: this.receiver.id, - senderId: this.sender.id, - type: this.type, - }; - } -} diff --git a/src/lib/dataaccess/User.ts b/src/lib/dataaccess/User.ts index b3501cc..8d0b19e 100644 --- a/src/lib/dataaccess/User.ts +++ b/src/lib/dataaccess/User.ts @@ -1,84 +1,39 @@ -import globals from "../globals"; -import {DataObject} from "./DataObject"; -import {queryHelper} from "./index"; -import {Post} from "./Post"; +import {SqUser} from "./datamodels"; +import * as wrappers from "./wrappers"; -export class User extends DataObject { - private $name: string; - private $handle: string; - private $email: string; - private $greenpoints: number; - private $joinedAt: string; - private $exists: boolean; +export class User { + public id: number; + public name: string; + public handle: string; + public greenpoints: number; + public joinedAt: Date; - /** - * The name of the user - */ - public async name(): Promise { - await this.loadDataIfNotExists(); - return this.$name; - } - - /** - * The unique handle of the user. - */ - public async handle(): Promise { - await this.loadDataIfNotExists(); - return this.$handle; - } + protected user: SqUser; - /** - * The email of the user - */ - public async email(): Promise { - await this.loadDataIfNotExists(); - return this.$email; - } - - /** - * The number of greenpoints of the user - */ - public async greenpoints(): Promise { - await this.loadDataIfNotExists(); - return this.$greenpoints; + constructor(user: SqUser) { + this.id = user.id; + this.name = user.username; + this.handle = user.handle; + this.greenpoints = user.rankpoints; + this.joinedAt = user.joinedAt; + this.user = user; } /** * Returns the number of posts the user created */ public async numberOfPosts(): Promise { - const result = await queryHelper.first({ - cache: true, - text: "SELECT COUNT(*) count FROM posts WHERE author = $1", - values: [this.id], - }); - return result.count; - } - - /** - * The date the user joined the platform - */ - public async joinedAt(): Promise { - await this.loadDataIfNotExists(); - return new Date(this.$joinedAt); + return this.user.countPosts(); } /** * Returns all friends of the user. */ public async friends(): Promise { - const result = await queryHelper.all({ - cache: true, - text: "SELECT * FROM user_friends WHERE user_id = $1 OR friend_id = $1", - values: [this.id], - }); + const result = await this.user.getFriends(); const userFriends = []; - for (const row of result) { - if (row.user_id === this.id) { - userFriends.push(new User(row.friend_id)); - } else { - userFriends.push(new User(row.user_id)); - } + for (const friend of result) { + userFriends.push(new User(friend)); } return userFriends; } @@ -86,43 +41,13 @@ export class User extends DataObject { /** * Returns all posts for a user. */ - public async posts({first, offset}: {first: number, offset: number}): Promise { - first = first || 10; - offset = offset || 0; - const result = await queryHelper.all({ - cache: true, - text: "SELECT * FROM posts WHERE author = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", - values: [this.id, first, offset], - }); + public async posts({first, offset}: { first: number, offset: number }): Promise { + const postRes = await this.user.getPosts(); const posts = []; - for (const row of result) { - posts.push(new Post(row.id, row)); + for (const post of postRes) { + posts.push(new wrappers.Post(post)); } return posts; } - - /** - * Fetches the data for the user. - */ - protected async loadData(): Promise { - let result: any; - if (this.row) { - result = this.row; - } else { - result = await queryHelper.first({ - cache: true, - text: "SELECT * FROM users WHERE users.id = $1", - values: [this.id], - }); - } - if (result) { - this.$name = result.name; - this.$handle = result.handle; - this.$email = result.email; - this.$greenpoints = result.greenpoints; - this.$joinedAt = result.joined_at; - this.dataLoaded = true; - } - } } diff --git a/src/lib/dataaccess/datamodels/index.ts b/src/lib/dataaccess/datamodels/index.ts new file mode 100644 index 0000000..9d57bf6 --- /dev/null +++ b/src/lib/dataaccess/datamodels/index.ts @@ -0,0 +1,12 @@ +export { + init as datainit, + User as SqUser, + Post as SqPost, + Chat as SqChat, + Request as SqRequest, + PostVotes as SqPostVotes, + ChatMessage as SqChatMessage, + ChatMembers as SqChatMembers, + RequestType as SqRequestType, + UserFriends as SqUserFriends, +} from "./models"; diff --git a/src/lib/dataaccess/datamodels/models.ts b/src/lib/dataaccess/datamodels/models.ts new file mode 100644 index 0000000..1780124 --- /dev/null +++ b/src/lib/dataaccess/datamodels/models.ts @@ -0,0 +1,279 @@ +// tslint:disable:object-literal-sort-keys + +import * as sqz from "sequelize"; +import { + Association, + BelongsToGetAssociationMixin, + BelongsToManyAddAssociationMixin, + BelongsToManyCountAssociationsMixin, + BelongsToManyCreateAssociationMixin, + BelongsToManyGetAssociationsMixin, + BelongsToManyHasAssociationMixin, + DataTypes, + HasManyAddAssociationMixin, + HasManyCountAssociationsMixin, + HasManyCreateAssociationMixin, + HasManyGetAssociationsMixin, + HasManyHasAssociationMixin, + HasOneGetAssociationMixin, + Model, + Sequelize, +} from "sequelize"; +import * as wrappers from "../wrappers"; + +const underscored = true; + +enum VoteType { + UPVOTE = "UPVOTE", + DOWNVOTE = "DOWNVOTE", +} + +export enum RequestType { + FRIENDREQUEST = "FRIENDREQUEST", + GROUPINVITE = "GROUPINVITE", + EVENTINVITE = "EVENTINVITE", +} + +export class User extends Model { + + public static associations: { + friends: Association; + posts: Association; + votes: Association; + requests: Association; + }; + + public id!: number; + public username!: string; + public handle!: string; + public email!: string; + public password!: string; + public rankpoints!: number; + + public readonly createdAt!: Date; + public readonly updatedAt!: Date; + + public getFriends!: HasManyGetAssociationsMixin; + public addFriend!: HasManyAddAssociationMixin; + public hasFriend!: HasManyHasAssociationMixin; + public countFriends!: HasManyCountAssociationsMixin; + + public getPosts!: HasManyGetAssociationsMixin; + public addPost!: HasManyAddAssociationMixin; + public hasPost!: HasManyHasAssociationMixin; + public countPosts!: HasManyCountAssociationsMixin; + public createPost!: HasManyCreateAssociationMixin; + + public getReceivedRequests!: HasManyGetAssociationsMixin; + public addReceivedRequest!: HasManyAddAssociationMixin; + public hasReceivedRequest!: HasManyHasAssociationMixin; + public countReceivedRequests!: HasManyCountAssociationsMixin; + public createReceivedRequest!: HasManyCreateAssociationMixin; + + + public getSentRequests!: HasManyGetAssociationsMixin; + public addSentRequest!: HasManyAddAssociationMixin; + public hasSentRequest!: HasManyHasAssociationMixin; + public countSentRequests!: HasManyCountAssociationsMixin; + public createSentRequest!: HasManyCreateAssociationMixin; + + public getChats!: BelongsToManyGetAssociationsMixin; + public addChat!: BelongsToManyAddAssociationMixin; + public hasChat!: BelongsToManyHasAssociationMixin; + public countChats!: BelongsToManyCountAssociationsMixin; + public createChat!: BelongsToManyCreateAssociationMixin; + + /** + * Getter for joined at as the date the entry was created. + */ + public get joinedAt(): Date { + // @ts-ignore + return this.getDataValue("createdAt"); + } + + /** + * Wraps itself into a user + */ + public get user(): wrappers.User { + return new wrappers.User(this); + } + + /** + * returns the username. + */ + public get name(): string { + return this.getDataValue("username"); + } + + /** + * Wraps itself into a profile. + */ + public get profile(): wrappers.Profile { + return new wrappers.Profile(this); + } +} + +export class UserFriends extends Model { +} + +export class Post extends Model { + + public static associations: { + author: Association, + votes: Association, + }; + + public id!: number; + public content!: string; + + public readonly createdAt!: Date; + public readonly updatedAt!: Date; + + public getUser!: BelongsToGetAssociationMixin; + + public getVotes!: HasManyGetAssociationsMixin; + public addVote!: HasManyAddAssociationMixin; + public hasVote!: HasManyHasAssociationMixin; + public countVotes!: HasManyCountAssociationsMixin; + public createVote!: HasManyCreateAssociationMixin; + + /** + * Wraps itself into a Post instance. + */ + public get post(): wrappers.Post { + return new wrappers.Post(this); + } +} + +export class PostVotes extends Model { + public voteType: VoteType; +} + +export class Request extends Model { + public id!: number; + public requestType!: RequestType; + + public getSender!: HasOneGetAssociationMixin; + public getReceiver!: HasOneGetAssociationMixin; +} + +export class Chat extends Model { + public static associations: { + members: Association, + messages: Association, + }; + + public id!: number; + + public readonly createdAt!: Date; + public readonly updatedAt!: Date; + + public getMembers!: BelongsToManyGetAssociationsMixin; + public addMember!: BelongsToManyAddAssociationMixin; + public hasMember!: BelongsToManyHasAssociationMixin; + public countMembers!: BelongsToManyCountAssociationsMixin; + + public getMessages!: HasManyGetAssociationsMixin; + public addMessage!: HasManyAddAssociationMixin; + public hasMessage!: HasManyHasAssociationMixin; + public countMessages!: HasManyCountAssociationsMixin; + public createMessage!: HasManyCreateAssociationMixin; + + /** + * wraps itself into a chatroom. + */ + public get chatroom(): wrappers.Chatroom { + return new wrappers.Chatroom(this); + } +} + +export class ChatMembers extends Model { +} + +export class ChatMessage extends Model { + public id: number; + public content!: string; + public readonly createdAt!: Date; + public readonly updatedAt!: Date; + + public getAuthor!: BelongsToGetAssociationMixin; + public getChat!: BelongsToGetAssociationMixin; + + public get message(): wrappers.ChatMessage { + return new wrappers.ChatMessage(this); + } +} + +export function init(sequelize: Sequelize) { + User.init({ + username: { + allowNull: false, + type: sqz.STRING(128), + }, + handle: { + allowNull: false, + type: sqz.STRING(128), + unique: true, + }, + email: { + allowNull: false, + type: sqz.STRING(128), + unique: true, + }, + password: { + allowNull: false, + type: sqz.STRING(128), + }, + rankpoints: { + allowNull: false, + type: DataTypes.INTEGER, + defaultValue: 0, + }, + }, {sequelize, underscored}); + + UserFriends.init({}, {sequelize, underscored}); + + Post.init({ + content: DataTypes.TEXT, + }, {sequelize, underscored}); + + PostVotes.init({ + voteType: { + type: DataTypes.ENUM, + values: ["UPVOTE", "DOWNVOTE"], + }, + }, {sequelize, underscored}); + + Request.init({ + requestType: { + type: DataTypes.ENUM, + values: ["FRIENDREQUEST", "GROUPINVITE", "EVENTINVITE"], + }, + }, {sequelize, underscored}); + + Chat.init({}, {sequelize, underscored}); + + ChatMembers.init({}, {sequelize, underscored}); + + ChatMessage.init({ + content: { + type: DataTypes.TEXT, + allowNull: false, + }, + }, {sequelize, underscored}); + + User.belongsToMany(User, {through: UserFriends, as: "friends"}); + Post.belongsTo(User, {foreignKey: "userId"}); + User.hasMany(Post, {as: "posts", foreignKey: "userId"}); + Post.belongsToMany(User, {through: PostVotes, as: "votes"}); + User.belongsToMany(Post, {through: PostVotes, as: "votes"}); + User.hasMany(Request, {as: "sentRequests"}); + User.hasMany(Request, {as: "receivedRequests"}); + User.belongsToMany(Chat, {through: ChatMembers}); + Chat.belongsToMany(User, {through: ChatMembers, as: "members"}); + Chat.hasMany(ChatMessage, {as: "messages"}); + ChatMessage.belongsTo(Chat); + ChatMessage.belongsTo(User, {as: "author", foreignKey: "userId"}); + User.hasMany(ChatMessage, {foreignKey: "userId"}); +} + diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 3e6628d..9fc6359 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -1,30 +1,19 @@ -import {Pool} from "pg"; +import {Sequelize} from "sequelize"; import {ChatNotFoundError} from "../errors/ChatNotFoundError"; import {EmailAlreadyRegisteredError} from "../errors/EmailAlreadyRegisteredError"; import {UserNotFoundError} from "../errors/UserNotFoundError"; import globals from "../globals"; import {InternalEvents} from "../InternalEvents"; -import {QueryHelper} from "../QueryHelper"; -import {ChatMessage} from "./ChatMessage"; import {Chatroom} from "./Chatroom"; +import * as models from "./datamodels"; import {Post} from "./Post"; import {Profile} from "./Profile"; -import {Request} from "./Request"; import {User} from "./User"; const config = globals.config; const tableCreationFile = __dirname + "/../../sql/create-tables.sql"; const tableUpdateFile = __dirname + "/../../sql/update-tables.sql"; -const dbClient: Pool = new Pool({ - database: config.database.database, - host: config.database.host, - password: config.database.password, - port: config.database.port, - user: config.database.user, -}); -export const queryHelper = new QueryHelper(dbClient, tableCreationFile, tableUpdateFile); - /** * Generates a new handle from the username and a base64 string of the current time. * @param username @@ -38,14 +27,15 @@ function generateHandle(username: string) { */ namespace dataaccess { - export const pool: Pool = dbClient; + let sequelize: Sequelize; /** * Initializes everything that needs to be initialized asynchronous. */ - export async function init() { + export async function init(seq: Sequelize) { + sequelize = seq; try { - await queryHelper.init(); + await models.datainit(sequelize); } catch (err) { globals.logger.error(err.message); globals.logger.debug(err.stack); @@ -57,12 +47,9 @@ namespace dataaccess { * @param userHandle */ export async function getUserByHandle(userHandle: string): Promise { - const result = await queryHelper.first({ - text: "SELECT * FROM users WHERE users.handle = $1", - values: [userHandle], - }); - if (result) { - return new User(result.id, result); + const user = await models.SqUser.findOne({where: {handle: userHandle}}); + if (user) { + return new User(user); } else { throw new UserNotFoundError(userHandle); } @@ -74,12 +61,9 @@ namespace dataaccess { * @param password */ export async function getUserByLogin(email: string, password: string): Promise { - const result = await queryHelper.first({ - text: "SELECT * FROM users WHERE email = $1 AND password = $2", - values: [email, password], - }); - if (result) { - return new Profile(result.id, result); + const user = await models.SqUser.findOne({where: {email, password}}); + if (user) { + return new Profile(user); } else { throw new UserNotFoundError(email); } @@ -92,16 +76,11 @@ namespace dataaccess { * @param password */ export async function registerUser(username: string, email: string, password: string) { - const existResult = await queryHelper.first({ - text: "SELECT email FROM users WHERE email = $1;", - values: [email], - }); - if (!existResult || !existResult.email) { - const result = await queryHelper.first({ - text: "INSERT INTO users (name, handle, password, email) VALUES ($1, $2, $3, $4) RETURNING *", - values: [username, generateHandle(username), password, email], - }); - return new Profile(result.id, result); + const existResult = !!(await models.SqUser.findOne({where: {username, email, password}})); + const handle = generateHandle(username); + if (!existResult) { + const user = await models.SqUser.create({username, email, password, handle}); + return new Profile(user); } else { throw new EmailAlreadyRegisteredError(email); } @@ -112,12 +91,9 @@ namespace dataaccess { * @param postId */ export async function getPost(postId: number): Promise { - const result = await queryHelper.first({ - text: "SELECT * FROM posts WHERE id = $1", - values: [postId], - }); - if (result) { - return new Post(result.id, result); + const post = await models.SqPost.findByPk(postId); + if (post) { + return new Post(post); } else { return null; } @@ -131,33 +107,18 @@ namespace dataaccess { */ export async function getPosts(first: number, offset: number, sort: SortType) { if (sort === SortType.NEW) { - const results = await queryHelper.all({ - cache: true, - text: "SELECT * FROM posts ORDER BY created_at DESC LIMIT $1 OFFSET $2", - values: [first, offset], - }); - const posts = []; - for (const row of results) { - posts.push(new Post(row.id, row)); - } - return posts; + const posts = await models.SqPost.findAll({order: [["createdAt", "DESC"]], limit: first, offset}); + return posts.map((p) => new Post(p)); } else { - const results = await queryHelper.all({ - cache: true, - text: ` - SELECT * FROM ( - SELECT *, - (SELECT count(*) FROM votes WHERE vote_type = 'UPVOTE' AND item_id = posts.id) AS upvotes , - (SELECT count(*) FROM votes WHERE vote_type = 'DOWNVOTE' AND item_id = posts.id) AS downvotes - FROM posts) AS a ORDER BY (a.upvotes - a.downvotes) DESC LIMIT $1 OFFSET $2; - `, - values: [first, offset], - }); - const posts = []; - for (const row of results) { - posts.push(new Post(row.id, row)); - } - return posts; + const results: models.SqPost[] = await sequelize.query( + `SELECT id FROM ( + SELECT *, + (SELECT count(*) FROM votes WHERE vote_type = 'UPVOTE' AND item_id = posts.id) AS upvotes , + (SELECT count(*) FROM votes WHERE vote_type = 'DOWNVOTE' AND item_id = posts.id) AS downvotes + FROM posts) AS a ORDER BY (a.upvotes - a.downvotes) DESC LIMIT ? OFFSET ?`, + {replacements: [first, offset], mapToModel: true, model: models.SqPost}); + + return results.map((p) => new Post(p)); } } @@ -169,11 +130,8 @@ namespace dataaccess { */ export async function createPost(content: string, authorId: number, type?: string): Promise { type = type || "MISC"; - const result = await queryHelper.first({ - text: "INSERT INTO posts (content, author, type) VALUES ($1, $2, $3) RETURNING *", - values: [content, authorId, type], - }); - const post = new Post(result.id, result); + const sqPost = await models.SqPost.create({content, userId: authorId}); + const post = new Post(sqPost); globals.internalEmitter.emit(InternalEvents.POSTCREATE, post); return post; } @@ -183,10 +141,7 @@ namespace dataaccess { * @param postId */ export async function deletePost(postId: number): Promise { - const result = await queryHelper.first({ - text: "DELETE FROM posts WHERE posts.id = $1", - values: [postId], - }); + await (await models.SqPost.findByPk(postId)).destroy(); return true; } @@ -195,31 +150,15 @@ namespace dataaccess { * @param members */ export async function createChat(...members: number[]): Promise { - const idResult = await queryHelper.first({ - text: "INSERT INTO chats (id) values (default) RETURNING *;", - }); - const id = idResult.id; - const transaction = await queryHelper.createTransaction(); - try { - await transaction.begin(); + return sequelize.transaction(async (t) => { + const chat = await models.SqChat.create({}, {transaction: t}); for (const member of members) { - await transaction.query({ - name: "chat-member-insert", - text: "INSERT INTO chat_members (chat, member) VALUES ($1, $2);", - values: [id, member], - }); + await chat.addMember(Number(member), {transaction: t}); } - await transaction.commit(); - } catch (err) { - globals.logger.warn(`Failed to insert chatmember into database: ${err.message}`); - globals.logger.debug(err.stack); - await transaction.rollback(); - } finally { - transaction.release(); - } - const chat = new Chatroom(id); - globals.internalEmitter.emit(InternalEvents.CHATCREATE, chat); - return chat; + const chatroom = new Chatroom(chat); + globals.internalEmitter.emit(InternalEvents.CHATCREATE, chatroom); + return chatroom; + }); } /** @@ -229,15 +168,11 @@ namespace dataaccess { * @param content */ export async function sendChatMessage(authorId: number, chatId: number, content: string) { - const chat = new Chatroom(chatId); - if ((await chat.exists())) { - const result = await queryHelper.first({ - text: "INSERT INTO chat_messages (chat, author, content) values ($1, $2, $3) RETURNING *", - values: [chatId, authorId, content], - }); - const message = new ChatMessage(new User(result.author), chat, result.created_at, result.content); - globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message); - return message; + const chat = await models.SqChat.findByPk(chatId); + if (chat) { + const message = await chat.createMessage({content, userId: authorId}); + globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message.message); + return message.message; } else { throw new ChatNotFoundError(chatId); } @@ -247,30 +182,20 @@ namespace dataaccess { * Returns all chats. */ export async function getAllChats(): Promise { - const result = await queryHelper.all({ - text: "SELECT id FROM chats;", - }); - const chats = []; - for (const row of result) { - chats.push(new Chatroom(row.id)); - } - return chats; + const chats = await models.SqChat.findAll(); + return chats.map((c) => new Chatroom(c)); } /** * Sends a request to a user. * @param sender * @param receiver - * @param type + * @param requestType */ - export async function createRequest(sender: number, receiver: number, type?: RequestType) { - type = type || RequestType.FRIENDREQUEST; + export async function createRequest(sender: number, receiver: number, requestType?: RequestType) { + requestType = requestType || RequestType.FRIENDREQUEST; - const result = await queryHelper.first({ - text: "INSERT INTO requests (sender, receiver, type) VALUES ($1, $2, $3) RETURNING *", - values: [sender, receiver, type], - }); - const request = new Request(new User(result.sender), new User(result.receiver), result.type); + const request = await models.SqRequest.create({senderId: sender, receiverId: receiver, requestType}); globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, Request); return request; } diff --git a/src/lib/dataaccess/wrappers.ts b/src/lib/dataaccess/wrappers.ts new file mode 100644 index 0000000..052fb95 --- /dev/null +++ b/src/lib/dataaccess/wrappers.ts @@ -0,0 +1,5 @@ +export {User} from "./User"; +export {Chatroom} from "./Chatroom"; +export {Post} from "./Post"; +export {Profile} from "./Profile"; +export {ChatMessage} from "./ChatMessage"; diff --git a/src/lib/errors/graphqlErrors.ts b/src/lib/errors/graphqlErrors.ts index 9784712..9e0b1b5 100644 --- a/src/lib/errors/graphqlErrors.ts +++ b/src/lib/errors/graphqlErrors.ts @@ -1,5 +1,4 @@ import {GraphQLError} from "graphql"; -import {BaseError} from "./BaseError"; export class NotLoggedInGqlError extends GraphQLError { diff --git a/src/lib/globals.ts b/src/lib/globals.ts index cea8e3c..0a6669a 100644 --- a/src/lib/globals.ts +++ b/src/lib/globals.ts @@ -35,7 +35,7 @@ namespace globals { format: winston.format.combine( winston.format.timestamp(), winston.format.colorize(), - winston.format.printf(({ level, message, timestamp }) => { + winston.format.printf(({level, message, timestamp}) => { return `${timestamp} ${level}: ${message}`; }), ), diff --git a/src/routes/home.ts b/src/routes/home.ts index 726b890..4fb7f79 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -3,8 +3,8 @@ import {Namespace, Server} from "socket.io"; import dataaccess from "../lib/dataaccess"; import {ChatMessage} from "../lib/dataaccess/ChatMessage"; import {Chatroom} from "../lib/dataaccess/Chatroom"; +import {Request} from "../lib/dataaccess/datamodels/models"; import {Post} from "../lib/dataaccess/Post"; -import {Request} from "../lib/dataaccess/Request"; import globals from "../lib/globals"; import {InternalEvents} from "../lib/InternalEvents"; import Route from "../lib/Route"; @@ -37,18 +37,18 @@ class HomeRoute extends Route { socket.on("postCreate", async (content) => { if (socket.handshake.session.userId) { const post = await dataaccess.createPost(content, socket.handshake.session.userId); - io.emit("post", await post.resolvedData()); + io.emit("post", Object.assign(post, {htmlContent: post.htmlContent()})); } else { socket.emit("error", "Not logged in!"); } }); - globals.internalEmitter.on(InternalEvents.REQUESTCREATE, (request: Request) => { - if (request.receiver.id === socket.handshake.session.userId) { - socket.emit("request", request.resolvedData()); + globals.internalEmitter.on(InternalEvents.REQUESTCREATE, async (request: Request) => { + if ((await request.getSender()).id === socket.handshake.session.userId) { + socket.emit("request", request); } }); globals.internalEmitter.on(InternalEvents.GQLPOSTCREATE, async (post: Post) => { - socket.emit("post", await post.resolvedData()); + socket.emit("post", Object.assign(post, {htmlContent: post.htmlContent()})); }); }); @@ -82,15 +82,15 @@ class HomeRoute extends Route { if (socket.handshake.session.userId) { const userId = socket.handshake.session.userId; const message = await dataaccess.sendChatMessage(userId, chatId, content); - socket.broadcast.emit("chatMessage", message.resolvedContent()); - socket.emit("chatMessageSent", message.resolvedContent()); + socket.broadcast.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent()})); + socket.emit("chatMessageSent", Object.assign(message, {htmlContent: message.htmlContent()})); } else { socket.emit("error", "Not logged in!"); } }); - globals.internalEmitter.on(InternalEvents.GQLCHATMESSAGE, (message: ChatMessage) => { - if (message.chat.id === chatId) { - socket.emit("chatMessage", message.resolvedContent()); + globals.internalEmitter.on(InternalEvents.GQLCHATMESSAGE, async (message: ChatMessage) => { + if ((await message.chat()).id === chatId) { + socket.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent()})); } }); }); diff --git a/tsconfig.json b/tsconfig.json index 9ef65e8..6fedd9d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "noImplicitAny": true, "removeComments": true, "preserveConstEnums": true, + "allowSyntheticDefaultImports": true, "outDir": "./dist", "sourceMap": true, "target": "es2018", @@ -18,4 +19,4 @@ "node_modules", "**/*.spec.ts" ] -} \ No newline at end of file +} diff --git a/tslint.json b/tslint.json index 78800e0..642f353 100644 --- a/tslint.json +++ b/tslint.json @@ -21,7 +21,8 @@ }, "no-namespace": false, "no-internal-module": false, - "max-classes-per-file": false + "max-classes-per-file": false, + "no-var-requires": false }, "jsRules": { "max-line-length": { From 511a446c719b1abfdc0794e5f1aecf55dcd6d935 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 13 Oct 2019 00:20:46 +0200 Subject: [PATCH 2/4] Fixed user voting --- src/graphql/resolvers.ts | 2 +- src/lib/dataaccess/Post.ts | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 23745e3..fe4e76a 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -111,7 +111,7 @@ export function resolver(req: any, res: any): any { if (postId && type) { if (req.session.userId) { const post = await models.SqPost.findByPk(postId); - return await (post.post).vote(req.session.userId, type); + return await post.post.vote(req.session.userId, type); } else { res.status(status.UNAUTHORIZED); return new NotLoggedInGqlError(); diff --git a/src/lib/dataaccess/Post.ts b/src/lib/dataaccess/Post.ts index 1105499..db6690c 100644 --- a/src/lib/dataaccess/Post.ts +++ b/src/lib/dataaccess/Post.ts @@ -74,10 +74,22 @@ export class Post { * @param type */ public async vote(userId: number, type: dataaccess.VoteType): Promise { - const [vote, _] = await SqPostVotes - .findOrCreate({where: {userId}, defaults: {voteType: type, postId: this.post.id}}); - vote.voteType = type; - await vote.save(); + type = type || dataaccess.VoteType.UPVOTE; + let vote = await SqPostVotes.findOne({where: {user_id: userId, post_id: this.id}}); + if (!vote) { + await this.post.addVote(userId); + vote = await SqPostVotes.findOne({where: {user_id: userId, post_id: this.id}}); + } + if (vote) { + if (vote.voteType === type) { + await vote.destroy(); + return null; + } else { + vote.voteType = type; + await vote.save(); + } + } + return vote.voteType; } } From 298940cc321a29010c2e8ec152334103cbf70794 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 13 Oct 2019 17:10:47 +0200 Subject: [PATCH 3/4] Switched to typescript-sequelize - delted old models - deleted wrapper models - integrated api functions in sequelize-typescript models --- .gitignore | 1 + CHANGELOG.md | 3 +- package-lock.json | 47 ++-- package.json | 6 +- src/app.ts | 6 +- src/graphql/resolvers.ts | 32 +-- src/lib/dataaccess/ChatMessage.ts | 38 --- src/lib/dataaccess/Chatroom.ts | 38 --- src/lib/dataaccess/Post.ts | 95 -------- src/lib/dataaccess/Profile.ts | 161 ------------- src/lib/dataaccess/User.ts | 53 ----- src/lib/dataaccess/datamodels/index.ts | 12 - src/lib/dataaccess/datamodels/models.ts | 279 ----------------------- src/lib/dataaccess/index.ts | 105 ++++----- src/lib/dataaccess/models/ChatMember.ts | 14 ++ src/lib/dataaccess/models/ChatMessage.ts | 41 ++++ src/lib/dataaccess/models/ChatRoom.ts | 28 +++ src/lib/dataaccess/models/Friendship.ts | 14 ++ src/lib/dataaccess/models/Post.ts | 64 ++++++ src/lib/dataaccess/models/PostVote.ts | 23 ++ src/lib/dataaccess/models/Request.ts | 37 +++ src/lib/dataaccess/models/User.ts | 105 +++++++++ src/lib/dataaccess/models/index.ts | 8 + src/lib/dataaccess/wrappers.ts | 5 - src/routes/home.ts | 21 +- tsconfig.json | 4 +- 26 files changed, 453 insertions(+), 787 deletions(-) delete mode 100644 src/lib/dataaccess/ChatMessage.ts delete mode 100644 src/lib/dataaccess/Chatroom.ts delete mode 100644 src/lib/dataaccess/Post.ts delete mode 100644 src/lib/dataaccess/Profile.ts delete mode 100644 src/lib/dataaccess/User.ts delete mode 100644 src/lib/dataaccess/datamodels/index.ts delete mode 100644 src/lib/dataaccess/datamodels/models.ts create mode 100644 src/lib/dataaccess/models/ChatMember.ts create mode 100644 src/lib/dataaccess/models/ChatMessage.ts create mode 100644 src/lib/dataaccess/models/ChatRoom.ts create mode 100644 src/lib/dataaccess/models/Friendship.ts create mode 100644 src/lib/dataaccess/models/Post.ts create mode 100644 src/lib/dataaccess/models/PostVote.ts create mode 100644 src/lib/dataaccess/models/Request.ts create mode 100644 src/lib/dataaccess/models/User.ts create mode 100644 src/lib/dataaccess/models/index.ts delete mode 100644 src/lib/dataaccess/wrappers.ts diff --git a/.gitignore b/.gitignore index f043384..92b6a08 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ test/*.log dist .idea config.yaml +sqz-force diff --git a/CHANGELOG.md b/CHANGELOG.md index a76df00..3edaa34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,4 +13,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - DTOs - Home Route - session management -- Sequelize modules and integration +- Sequelize models and integration +- Sequelize-typescript integration diff --git a/package-lock.json b/package-lock.json index 2249f82..6ccf67c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -223,9 +223,9 @@ "dev": true }, "@types/node": { - "version": "12.7.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.8.tgz", - "integrity": "sha512-FMdVn84tJJdV+xe+53sYiZS4R5yn1mAIxfj+DVoNiQjTYz1+OYmjwEZr1ev9nU0axXwda0QDbYl06QHanRVH3A==" + "version": "12.7.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz", + "integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ==" }, "@types/pg": { "version": "7.11.0", @@ -1477,14 +1477,6 @@ } } }, - "connect-pg-simple": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-6.0.1.tgz", - "integrity": "sha512-zW5AOtRNOLcXxphSmQ+oYj0snlLs1Je3u5K2NWyF7WhMVoPvnQXraK2wzS8f7qLwhMcmYukah2ymu0Gdxf7Qsg==", - "requires": { - "pg": "^7.4.3" - } - }, "connect-session-sequelize": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/connect-session-sequelize/-/connect-session-sequelize-6.0.0.tgz", @@ -3185,11 +3177,6 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "g": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/g/-/g-2.0.1.tgz", - "integrity": "sha1-C1lj69DKcOO8jGdmk0oCGCHIuFc=" - }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -5830,6 +5817,11 @@ "strip-indent": "^1.0.1" } }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", @@ -6242,6 +6234,29 @@ "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-2.3.0.tgz", "integrity": "sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA==" }, + "sequelize-typescript": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sequelize-typescript/-/sequelize-typescript-1.0.0.tgz", + "integrity": "sha512-oXyvHRTOyI8sJettpISL5LO30GaMMrLqzxiLCy6MjUmBJdaQDpdjn7ofge4J87MSdw+YPzkjrJLogMc9ONY2Tg==", + "requires": { + "glob": "7.1.2" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, "serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", diff --git a/package.json b/package.json index c47a635..94d36ad 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "author": "SoftEngI", "license": "ISC", "devDependencies": { + "@types/bluebird": "^3.5.27", "@types/compression": "^1.0.1", "@types/connect-pg-simple": "^4.2.0", "@types/cookie-parser": "^1.4.2", @@ -33,10 +34,11 @@ "@types/http-status": "^0.2.30", "@types/js-yaml": "^3.12.1", "@types/markdown-it": "0.0.9", - "@types/node": "^12.7.8", + "@types/node": "^12.7.12", "@types/pg": "^7.11.0", "@types/sequelize": "^4.28.5", "@types/socket.io": "^2.1.2", + "@types/validator": "^10.11.3", "@types/winston": "^2.4.4", "delete": "^1.1.0", "gulp": "^4.0.2", @@ -66,7 +68,9 @@ "markdown-it-emoji": "^1.4.0", "pg": "^7.12.1", "pug": "^2.0.4", + "reflect-metadata": "^0.1.13", "sequelize": "^5.19.6", + "sequelize-typescript": "^1.0.0", "socket.io": "^2.2.0", "sqlite3": "^4.1.0", "winston": "^3.2.1" diff --git a/src/app.ts b/src/app.ts index dabc2ec..0a565a6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,17 +5,17 @@ import * as express from "express"; import * as graphqlHTTP from "express-graphql"; import * as session from "express-session"; import sharedsession = require("express-socket.io-session"); +import * as fsx from "fs-extra"; import {buildSchema} from "graphql"; import {importSchema} from "graphql-import"; import * as http from "http"; import * as path from "path"; -import {Sequelize} from "sequelize"; +import {Sequelize} from "sequelize-typescript"; import * as socketIo from "socket.io"; import {resolver} from "./graphql/resolvers"; import dataaccess from "./lib/dataaccess"; import globals from "./lib/globals"; import routes from "./routes"; -import * as fsx from "fs-extra"; const SequelizeStore = require("connect-session-sequelize")(session.Store); const logger = globals.logger; @@ -30,7 +30,7 @@ class App { this.app = express(); this.server = new http.Server(this.app); this.io = socketIo(this.server); - this.sequelize = new Sequelize(globals.config.database.connectionUri); + this.sequelize = new Sequelize(globals.config.database.connectionUri ); } /** diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index fe4e76a..8867c30 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -1,12 +1,7 @@ import {GraphQLError} from "graphql"; import * as status from "http-status"; -import {Sequelize} from "sequelize"; import dataaccess from "../lib/dataaccess"; -import {Chatroom} from "../lib/dataaccess/Chatroom"; -import * as models from "../lib/dataaccess/datamodels"; -import {Post} from "../lib/dataaccess/Post"; -import {Profile} from "../lib/dataaccess/Profile"; -import {User} from "../lib/dataaccess/User"; +import * as models from "../lib/dataaccess/models"; import {NotLoggedInGqlError} from "../lib/errors/graphqlErrors"; import globals from "../lib/globals"; import {InternalEvents} from "../lib/InternalEvents"; @@ -21,8 +16,7 @@ export function resolver(req: any, res: any): any { return { async getSelf() { if (req.session.userId) { - const user = await models.SqUser.findByPk(req.session.userId); - return user.profile; + return models.User.findByPk(req.session.userId); } else { res.status(status.UNAUTHORIZED); return new NotLoggedInGqlError(); @@ -32,8 +26,7 @@ export function resolver(req: any, res: any): any { if (handle) { return await dataaccess.getUserByHandle(handle); } else if (userId) { - const user = await models.SqUser.findByPk(userId); - return user.user; + return models.User.findByPk(userId); } else { res.status(status.BAD_REQUEST); return new GraphQLError("No userId or handle provided."); @@ -49,8 +42,7 @@ export function resolver(req: any, res: any): any { }, async getChat({chatId}: { chatId: number }) { if (chatId) { - const chat = await models.SqChat.findByPk(chatId); - return new Chatroom(chat); + return models.ChatRoom.findByPk(chatId); } else { res.status(status.BAD_REQUEST); return new GraphQLError("No chatId given."); @@ -110,8 +102,8 @@ export function resolver(req: any, res: any): any { async vote({postId, type}: { postId: number, type: dataaccess.VoteType }) { if (postId && type) { if (req.session.userId) { - const post = await models.SqPost.findByPk(postId); - return await post.post.vote(req.session.userId, type); + const post = await models.Post.findByPk(postId); + return await post.vote(req.session.userId, type); } else { res.status(status.UNAUTHORIZED); return new NotLoggedInGqlError(); @@ -138,8 +130,8 @@ export function resolver(req: any, res: any): any { }, async deletePost({postId}: { postId: number }) { if (postId) { - const post = (await models.SqPost.findByPk(postId)).post; - if ((await post.author()).id === req.session.userId) { + const post = await models.Post.findByPk(postId, {include: [models.User]}); + if (post.rAuthor.id === req.session.userId) { return await dataaccess.deletePost(post.id); } else { res.status(status.FORBIDDEN); @@ -200,8 +192,8 @@ export function resolver(req: any, res: any): any { return new NotLoggedInGqlError(); } if (sender && type) { - const profile = new Profile(req.session.userId); - await profile.denyRequest(sender, type); + const user = await models.User.findByPk(req.session.userId); + await user.denyRequest(sender, type); return true; } else { res.status(status.BAD_REQUEST); @@ -215,8 +207,8 @@ export function resolver(req: any, res: any): any { } if (sender && type) { try { - const profile = new Profile(req.session.userId); - await profile.acceptRequest(sender, type); + const user = await models.User.findByPk(req.session.userId); + await user.acceptRequest(sender, type); return true; } catch (err) { globals.logger.warn(err.message); diff --git a/src/lib/dataaccess/ChatMessage.ts b/src/lib/dataaccess/ChatMessage.ts deleted file mode 100644 index 1e42a05..0000000 --- a/src/lib/dataaccess/ChatMessage.ts +++ /dev/null @@ -1,38 +0,0 @@ -import markdown from "../markdown"; -import {Chatroom} from "./Chatroom"; -import * as models from "./datamodels/models"; -import {User} from "./User"; - -export class ChatMessage { - - public id: number; - public content: string; - public createdAt: Date; - - constructor(private message: models.ChatMessage) { - this.id = message.id; - this.content = message.content; - this.createdAt = message.createdAt; - } - - /** - * returns the author of the chat message. - */ - public async author(): Promise { - return new User(await this.message.getAuthor()); - } - - /** - * Returns the rendered html content of the chat message. - */ - public htmlContent(): string { - return markdown.renderInline(this.content); - } - - /** - * returns the chatroom for the chatmessage. - */ - public async chat(): Promise { - return (await this.message.getChat()).chatroom; - } -} diff --git a/src/lib/dataaccess/Chatroom.ts b/src/lib/dataaccess/Chatroom.ts deleted file mode 100644 index 3e08389..0000000 --- a/src/lib/dataaccess/Chatroom.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {SqChat} from "./datamodels"; -import {User} from "./User"; - -export class Chatroom { - - public readonly id: number; - public namespace: string; - - constructor(private chat: SqChat) { - this.id = chat.id; - this.namespace = `/chat/${chat.id}`; - } - - /** - * Returns all members of a chatroom. - */ - public async members(): Promise { - const members = await this.chat.getMembers(); - return members.map((m) => new User(m)); - } - - /** - * Returns messages of the chat - * @param limit - the limit of messages to return - * @param offset - the offset of messages to return - * @param containing - filter by containing - */ - public async messages({first, offset, containing}: { first?: number, offset?: number, containing?: string }) { - const lim = first || 16; - const offs = offset || 0; - const messages = await this.chat.getMessages({limit: lim, offset: offs}); - if (containing) { - return messages.filter((x) => x.content.includes(containing)).map((m) => m.message); - } else { - return messages.map((m) => m.message); - } - } -} diff --git a/src/lib/dataaccess/Post.ts b/src/lib/dataaccess/Post.ts deleted file mode 100644 index db6690c..0000000 --- a/src/lib/dataaccess/Post.ts +++ /dev/null @@ -1,95 +0,0 @@ -import markdown from "../markdown"; -import {SqPost, SqPostVotes} from "./datamodels"; -import {PostVotes} from "./datamodels/models"; -import dataaccess from "./index"; -import {User} from "./User"; - -export class Post { - public readonly id: number; - public createdAt: Date; - public content: string; - public type: string; - - private post: SqPost; - - constructor(post: SqPost) { - this.id = post.id; - this.createdAt = post.createdAt; - this.post = post; - this.type = ""; - this.content = post.content; - } - - /** - * Returns the upvotes of a post. - */ - public async upvotes(): Promise { - return PostVotes.count({where: {voteType: dataaccess.VoteType.UPVOTE, post_id: this.id}}); - } - - /** - * Returns the downvotes of the post - */ - public async downvotes(): Promise { - return PostVotes.count({where: {voteType: dataaccess.VoteType.DOWNVOTE, post_id: this.id}}); - } - - /** - * the content rendered by markdown-it. - */ - public async htmlContent(): Promise { - return markdown.render(this.content); - } - - /** - * The autor of the post. - */ - public async author(): Promise { - return new User(await this.post.getUser()); - } - - /** - * Deletes the post. - */ - public async delete(): Promise { - await this.post.destroy(); - } - - /** - * The type of vote the user performed on the post. - */ - public async userVote(userId: number): Promise { - const votes = await this.post.getVotes({where: {userId}}); - - if (votes.length >= 1) { - return votes[0].voteType; - } else { - return null; - } - } - - /** - * Performs a vote on a post. - * @param userId - * @param type - */ - public async vote(userId: number, type: dataaccess.VoteType): Promise { - type = type || dataaccess.VoteType.UPVOTE; - let vote = await SqPostVotes.findOne({where: {user_id: userId, post_id: this.id}}); - if (!vote) { - await this.post.addVote(userId); - vote = await SqPostVotes.findOne({where: {user_id: userId, post_id: this.id}}); - } - if (vote) { - if (vote.voteType === type) { - await vote.destroy(); - return null; - } else { - vote.voteType = type; - await vote.save(); - } - } - - return vote.voteType; - } -} diff --git a/src/lib/dataaccess/Profile.ts b/src/lib/dataaccess/Profile.ts deleted file mode 100644 index a9eaba2..0000000 --- a/src/lib/dataaccess/Profile.ts +++ /dev/null @@ -1,161 +0,0 @@ -import {RequestNotFoundError} from "../errors/RequestNotFoundError"; -import {Chatroom} from "./Chatroom"; -import {SqUser} from "./datamodels"; -import dataaccess from "./index"; -import * as wrappers from "./wrappers"; - -export class Profile { - - public id: number; - public name: string; - public handle: string; - public email: string; - public greenpoints: number; - public joinedAt: Date; - - protected user: SqUser; - - constructor(user: SqUser) { - this.name = user.username; - this.handle = user.handle; - this.email = user.email; - this.greenpoints = user.rankpoints; - this.joinedAt = user.joinedAt; - this.id = user.id; - this.user = user; - } - - /** - * Returns the number of posts the user created - */ - public async numberOfPosts(): Promise { - return this.user.countPosts(); - } - - /** - * Returns all friends of the user. - */ - public async friends(): Promise { - const result = await this.user.getFriends(); - const userFriends = []; - for (const friend of result) { - userFriends.push(new wrappers.User(friend)); - } - return userFriends; - } - - /** - * Returns all posts for a user. - */ - public async posts({first, offset}: { first: number, offset: number }): Promise { - const postRes = await this.user.getPosts(); - const posts = []; - - for (const post of postRes) { - posts.push(new wrappers.Post(post)); - } - return posts; - } - - /** - * Returns all chatrooms (with pagination). - * Skips the query if the user doesn't exist. - * @param first - * @param offset - */ - public async chats({first, offset}: { first: number, offset?: number }): Promise { - first = first || 10; - offset = offset || 0; - - const result = await this.user.getChats(); - - if (result) { - return result.map((chat) => new Chatroom(chat)); - } else { - return []; - } - } - - /** - * Returns all open requests the user has send. - */ - public async sentRequests() { - return this.user.getSentRequests(); - } - - /** - * Returns all received requests of the user. - */ - public async receivedRequests() { - return this.user.getReceivedRequests(); - } - - /** - * Sets the greenpoints of a user. - * @param points - */ - public async setGreenpoints(points: number): Promise { - this.user.rankpoints = points; - await this.user.save(); - return this.user.rankpoints; - } - - /** - * Sets the email of the user - * @param email - */ - public async setEmail(email: string): Promise { - this.user.email = email; - await this.user.save(); - return this.user.email; - } - - /** - * Updates the handle of the user - */ - public async setHandle(handle: string): Promise { - this.user.handle = handle; - await this.user.save(); - return this.user.handle; - } - - /** - * Sets the username of the user - * @param name - */ - public async setName(name: string): Promise { - this.user.username = name; - await this.user.save(); - return this.user.username; - } - - /** - * Denys a request. - * @param sender - * @param type - */ - public async denyRequest(sender: number, type: dataaccess.RequestType) { - const request = await this.user.getReceivedRequests({where: {senderId: sender, requestType: type}}); - if (request[0]) { - await request[0].destroy(); - } - } - - /** - * Accepts a request. - * @param sender - * @param type - */ - public async acceptRequest(sender: number, type: dataaccess.RequestType) { - const requests = await this.user.getReceivedRequests({where: {senderId: sender, requestType: type}}); - if (requests.length > 0) { - const request = requests[0]; - if (request.requestType === dataaccess.RequestType.FRIENDREQUEST) { - await this.user.addFriend(sender); - await request.destroy(); - } - } else { - throw new RequestNotFoundError(sender, this.id, type); - } - } -} diff --git a/src/lib/dataaccess/User.ts b/src/lib/dataaccess/User.ts deleted file mode 100644 index 8d0b19e..0000000 --- a/src/lib/dataaccess/User.ts +++ /dev/null @@ -1,53 +0,0 @@ -import {SqUser} from "./datamodels"; -import * as wrappers from "./wrappers"; - -export class User { - public id: number; - public name: string; - public handle: string; - public greenpoints: number; - public joinedAt: Date; - - protected user: SqUser; - - constructor(user: SqUser) { - this.id = user.id; - this.name = user.username; - this.handle = user.handle; - this.greenpoints = user.rankpoints; - this.joinedAt = user.joinedAt; - this.user = user; - } - - /** - * Returns the number of posts the user created - */ - public async numberOfPosts(): Promise { - return this.user.countPosts(); - } - - /** - * Returns all friends of the user. - */ - public async friends(): Promise { - const result = await this.user.getFriends(); - const userFriends = []; - for (const friend of result) { - userFriends.push(new User(friend)); - } - return userFriends; - } - - /** - * Returns all posts for a user. - */ - public async posts({first, offset}: { first: number, offset: number }): Promise { - const postRes = await this.user.getPosts(); - const posts = []; - - for (const post of postRes) { - posts.push(new wrappers.Post(post)); - } - return posts; - } -} diff --git a/src/lib/dataaccess/datamodels/index.ts b/src/lib/dataaccess/datamodels/index.ts deleted file mode 100644 index 9d57bf6..0000000 --- a/src/lib/dataaccess/datamodels/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { - init as datainit, - User as SqUser, - Post as SqPost, - Chat as SqChat, - Request as SqRequest, - PostVotes as SqPostVotes, - ChatMessage as SqChatMessage, - ChatMembers as SqChatMembers, - RequestType as SqRequestType, - UserFriends as SqUserFriends, -} from "./models"; diff --git a/src/lib/dataaccess/datamodels/models.ts b/src/lib/dataaccess/datamodels/models.ts deleted file mode 100644 index 1780124..0000000 --- a/src/lib/dataaccess/datamodels/models.ts +++ /dev/null @@ -1,279 +0,0 @@ -// tslint:disable:object-literal-sort-keys - -import * as sqz from "sequelize"; -import { - Association, - BelongsToGetAssociationMixin, - BelongsToManyAddAssociationMixin, - BelongsToManyCountAssociationsMixin, - BelongsToManyCreateAssociationMixin, - BelongsToManyGetAssociationsMixin, - BelongsToManyHasAssociationMixin, - DataTypes, - HasManyAddAssociationMixin, - HasManyCountAssociationsMixin, - HasManyCreateAssociationMixin, - HasManyGetAssociationsMixin, - HasManyHasAssociationMixin, - HasOneGetAssociationMixin, - Model, - Sequelize, -} from "sequelize"; -import * as wrappers from "../wrappers"; - -const underscored = true; - -enum VoteType { - UPVOTE = "UPVOTE", - DOWNVOTE = "DOWNVOTE", -} - -export enum RequestType { - FRIENDREQUEST = "FRIENDREQUEST", - GROUPINVITE = "GROUPINVITE", - EVENTINVITE = "EVENTINVITE", -} - -export class User extends Model { - - public static associations: { - friends: Association; - posts: Association; - votes: Association; - requests: Association; - }; - - public id!: number; - public username!: string; - public handle!: string; - public email!: string; - public password!: string; - public rankpoints!: number; - - public readonly createdAt!: Date; - public readonly updatedAt!: Date; - - public getFriends!: HasManyGetAssociationsMixin; - public addFriend!: HasManyAddAssociationMixin; - public hasFriend!: HasManyHasAssociationMixin; - public countFriends!: HasManyCountAssociationsMixin; - - public getPosts!: HasManyGetAssociationsMixin; - public addPost!: HasManyAddAssociationMixin; - public hasPost!: HasManyHasAssociationMixin; - public countPosts!: HasManyCountAssociationsMixin; - public createPost!: HasManyCreateAssociationMixin; - - public getReceivedRequests!: HasManyGetAssociationsMixin; - public addReceivedRequest!: HasManyAddAssociationMixin; - public hasReceivedRequest!: HasManyHasAssociationMixin; - public countReceivedRequests!: HasManyCountAssociationsMixin; - public createReceivedRequest!: HasManyCreateAssociationMixin; - - - public getSentRequests!: HasManyGetAssociationsMixin; - public addSentRequest!: HasManyAddAssociationMixin; - public hasSentRequest!: HasManyHasAssociationMixin; - public countSentRequests!: HasManyCountAssociationsMixin; - public createSentRequest!: HasManyCreateAssociationMixin; - - public getChats!: BelongsToManyGetAssociationsMixin; - public addChat!: BelongsToManyAddAssociationMixin; - public hasChat!: BelongsToManyHasAssociationMixin; - public countChats!: BelongsToManyCountAssociationsMixin; - public createChat!: BelongsToManyCreateAssociationMixin; - - /** - * Getter for joined at as the date the entry was created. - */ - public get joinedAt(): Date { - // @ts-ignore - return this.getDataValue("createdAt"); - } - - /** - * Wraps itself into a user - */ - public get user(): wrappers.User { - return new wrappers.User(this); - } - - /** - * returns the username. - */ - public get name(): string { - return this.getDataValue("username"); - } - - /** - * Wraps itself into a profile. - */ - public get profile(): wrappers.Profile { - return new wrappers.Profile(this); - } -} - -export class UserFriends extends Model { -} - -export class Post extends Model { - - public static associations: { - author: Association, - votes: Association, - }; - - public id!: number; - public content!: string; - - public readonly createdAt!: Date; - public readonly updatedAt!: Date; - - public getUser!: BelongsToGetAssociationMixin; - - public getVotes!: HasManyGetAssociationsMixin; - public addVote!: HasManyAddAssociationMixin; - public hasVote!: HasManyHasAssociationMixin; - public countVotes!: HasManyCountAssociationsMixin; - public createVote!: HasManyCreateAssociationMixin; - - /** - * Wraps itself into a Post instance. - */ - public get post(): wrappers.Post { - return new wrappers.Post(this); - } -} - -export class PostVotes extends Model { - public voteType: VoteType; -} - -export class Request extends Model { - public id!: number; - public requestType!: RequestType; - - public getSender!: HasOneGetAssociationMixin; - public getReceiver!: HasOneGetAssociationMixin; -} - -export class Chat extends Model { - public static associations: { - members: Association, - messages: Association, - }; - - public id!: number; - - public readonly createdAt!: Date; - public readonly updatedAt!: Date; - - public getMembers!: BelongsToManyGetAssociationsMixin; - public addMember!: BelongsToManyAddAssociationMixin; - public hasMember!: BelongsToManyHasAssociationMixin; - public countMembers!: BelongsToManyCountAssociationsMixin; - - public getMessages!: HasManyGetAssociationsMixin; - public addMessage!: HasManyAddAssociationMixin; - public hasMessage!: HasManyHasAssociationMixin; - public countMessages!: HasManyCountAssociationsMixin; - public createMessage!: HasManyCreateAssociationMixin; - - /** - * wraps itself into a chatroom. - */ - public get chatroom(): wrappers.Chatroom { - return new wrappers.Chatroom(this); - } -} - -export class ChatMembers extends Model { -} - -export class ChatMessage extends Model { - public id: number; - public content!: string; - public readonly createdAt!: Date; - public readonly updatedAt!: Date; - - public getAuthor!: BelongsToGetAssociationMixin; - public getChat!: BelongsToGetAssociationMixin; - - public get message(): wrappers.ChatMessage { - return new wrappers.ChatMessage(this); - } -} - -export function init(sequelize: Sequelize) { - User.init({ - username: { - allowNull: false, - type: sqz.STRING(128), - }, - handle: { - allowNull: false, - type: sqz.STRING(128), - unique: true, - }, - email: { - allowNull: false, - type: sqz.STRING(128), - unique: true, - }, - password: { - allowNull: false, - type: sqz.STRING(128), - }, - rankpoints: { - allowNull: false, - type: DataTypes.INTEGER, - defaultValue: 0, - }, - }, {sequelize, underscored}); - - UserFriends.init({}, {sequelize, underscored}); - - Post.init({ - content: DataTypes.TEXT, - }, {sequelize, underscored}); - - PostVotes.init({ - voteType: { - type: DataTypes.ENUM, - values: ["UPVOTE", "DOWNVOTE"], - }, - }, {sequelize, underscored}); - - Request.init({ - requestType: { - type: DataTypes.ENUM, - values: ["FRIENDREQUEST", "GROUPINVITE", "EVENTINVITE"], - }, - }, {sequelize, underscored}); - - Chat.init({}, {sequelize, underscored}); - - ChatMembers.init({}, {sequelize, underscored}); - - ChatMessage.init({ - content: { - type: DataTypes.TEXT, - allowNull: false, - }, - }, {sequelize, underscored}); - - User.belongsToMany(User, {through: UserFriends, as: "friends"}); - Post.belongsTo(User, {foreignKey: "userId"}); - User.hasMany(Post, {as: "posts", foreignKey: "userId"}); - Post.belongsToMany(User, {through: PostVotes, as: "votes"}); - User.belongsToMany(Post, {through: PostVotes, as: "votes"}); - User.hasMany(Request, {as: "sentRequests"}); - User.hasMany(Request, {as: "receivedRequests"}); - User.belongsToMany(Chat, {through: ChatMembers}); - Chat.belongsToMany(User, {through: ChatMembers, as: "members"}); - Chat.hasMany(ChatMessage, {as: "messages"}); - ChatMessage.belongsTo(Chat); - ChatMessage.belongsTo(User, {as: "author", foreignKey: "userId"}); - User.hasMany(ChatMessage, {foreignKey: "userId"}); -} - diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 9fc6359..e5f04cc 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -1,18 +1,10 @@ -import {Sequelize} from "sequelize"; +import {Sequelize} from "sequelize-typescript"; import {ChatNotFoundError} from "../errors/ChatNotFoundError"; import {EmailAlreadyRegisteredError} from "../errors/EmailAlreadyRegisteredError"; import {UserNotFoundError} from "../errors/UserNotFoundError"; import globals from "../globals"; import {InternalEvents} from "../InternalEvents"; -import {Chatroom} from "./Chatroom"; -import * as models from "./datamodels"; -import {Post} from "./Post"; -import {Profile} from "./Profile"; -import {User} from "./User"; - -const config = globals.config; -const tableCreationFile = __dirname + "/../../sql/create-tables.sql"; -const tableUpdateFile = __dirname + "/../../sql/update-tables.sql"; +import * as models from "./models"; /** * Generates a new handle from the username and a base64 string of the current time. @@ -35,7 +27,16 @@ namespace dataaccess { export async function init(seq: Sequelize) { sequelize = seq; try { - await models.datainit(sequelize); + await sequelize.addModels([ + models.ChatMember, + models.ChatMessage, + models.ChatRoom, + models.Friendship, + models.Post, + models.PostVote, + models.Request, + models.User, + ]); } catch (err) { globals.logger.error(err.message); globals.logger.debug(err.stack); @@ -46,10 +47,10 @@ namespace dataaccess { * Returns the user by handle. * @param userHandle */ - export async function getUserByHandle(userHandle: string): Promise { - const user = await models.SqUser.findOne({where: {handle: userHandle}}); + export async function getUserByHandle(userHandle: string): Promise { + const user = await models.User.findOne({where: {handle: userHandle}}); if (user) { - return new User(user); + return user; } else { throw new UserNotFoundError(userHandle); } @@ -60,10 +61,10 @@ namespace dataaccess { * @param email * @param password */ - export async function getUserByLogin(email: string, password: string): Promise { - const user = await models.SqUser.findOne({where: {email, password}}); + export async function getUserByLogin(email: string, password: string): Promise { + const user = await models.User.findOne({where: {email, password}}); if (user) { - return new Profile(user); + return user; } else { throw new UserNotFoundError(email); } @@ -75,12 +76,11 @@ namespace dataaccess { * @param email * @param password */ - export async function registerUser(username: string, email: string, password: string) { - const existResult = !!(await models.SqUser.findOne({where: {username, email, password}})); + export async function registerUser(username: string, email: string, password: string): Promise { + const existResult = !!(await models.User.findOne({where: {username, email, password}})); const handle = generateHandle(username); if (!existResult) { - const user = await models.SqUser.create({username, email, password, handle}); - return new Profile(user); + return models.User.create({username, email, password, handle}); } else { throw new EmailAlreadyRegisteredError(email); } @@ -90,10 +90,10 @@ namespace dataaccess { * Returns a post for a given postId.s * @param postId */ - export async function getPost(postId: number): Promise { - const post = await models.SqPost.findByPk(postId); + export async function getPost(postId: number): Promise { + const post = await models.Post.findByPk(postId); if (post) { - return new Post(post); + return post; } else { return null; } @@ -107,18 +107,20 @@ namespace dataaccess { */ export async function getPosts(first: number, offset: number, sort: SortType) { if (sort === SortType.NEW) { - const posts = await models.SqPost.findAll({order: [["createdAt", "DESC"]], limit: first, offset}); - return posts.map((p) => new Post(p)); + return models.Post.findAll({ + include: [{association: "rVotes"}], + limit: first, + offset, + order: [["createdAt", "DESC"]], + }); } else { - const results: models.SqPost[] = await sequelize.query( - `SELECT id FROM ( + return await sequelize.query( + `SELECT * FROM ( SELECT *, - (SELECT count(*) FROM votes WHERE vote_type = 'UPVOTE' AND item_id = posts.id) AS upvotes , - (SELECT count(*) FROM votes WHERE vote_type = 'DOWNVOTE' AND item_id = posts.id) AS downvotes + (SELECT count(*) FROM post_votes WHERE vote_type = 'UPVOTE' AND post_id = posts.id) AS upvotes , + (SELECT count(*) FROM post_votes WHERE vote_type = 'DOWNVOTE' AND post_id = posts.id) AS downvotes FROM posts) AS a ORDER BY (a.upvotes - a.downvotes) DESC LIMIT ? OFFSET ?`, - {replacements: [first, offset], mapToModel: true, model: models.SqPost}); - - return results.map((p) => new Post(p)); + {replacements: [first, offset], mapToModel: true, model: models.Post}) as models.Post[]; } } @@ -128,10 +130,9 @@ namespace dataaccess { * @param authorId * @param type */ - export async function createPost(content: string, authorId: number, type?: string): Promise { + export async function createPost(content: string, authorId: number, type?: string): Promise { type = type || "MISC"; - const sqPost = await models.SqPost.create({content, userId: authorId}); - const post = new Post(sqPost); + const post = await models.Post.create({content, authorId}); globals.internalEmitter.emit(InternalEvents.POSTCREATE, post); return post; } @@ -141,7 +142,7 @@ namespace dataaccess { * @param postId */ export async function deletePost(postId: number): Promise { - await (await models.SqPost.findByPk(postId)).destroy(); + await (await models.Post.findByPk(postId)).destroy(); return true; } @@ -149,15 +150,16 @@ namespace dataaccess { * Creates a chatroom containing two users * @param members */ - export async function createChat(...members: number[]): Promise { + export async function createChat(...members: number[]): Promise { return sequelize.transaction(async (t) => { - const chat = await models.SqChat.create({}, {transaction: t}); + const chat = await models.ChatRoom.create({}, {transaction: t, include: [models.User]}); for (const member of members) { - await chat.addMember(Number(member), {transaction: t}); + const user = await models.User.findByPk(member); + await chat.$add("rMember", user, {transaction: t}); } - const chatroom = new Chatroom(chat); - globals.internalEmitter.emit(InternalEvents.CHATCREATE, chatroom); - return chatroom; + await chat.save({transaction: t}); + globals.internalEmitter.emit(InternalEvents.CHATCREATE, chat); + return chat; }); } @@ -168,22 +170,21 @@ namespace dataaccess { * @param content */ export async function sendChatMessage(authorId: number, chatId: number, content: string) { - const chat = await models.SqChat.findByPk(chatId); + const chat = await models.ChatRoom.findByPk(chatId); if (chat) { - const message = await chat.createMessage({content, userId: authorId}); - globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message.message); - return message.message; + const message = await chat.$create("rMessage", {content, authorId}) as models.ChatMessage; + globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message); + return message; } else { throw new ChatNotFoundError(chatId); } } /** - * Returns all chats. + * Returns all rChats. */ - export async function getAllChats(): Promise { - const chats = await models.SqChat.findAll(); - return chats.map((c) => new Chatroom(c)); + export async function getAllChats(): Promise { + return models.ChatRoom.findAll(); } /** @@ -195,7 +196,7 @@ namespace dataaccess { export async function createRequest(sender: number, receiver: number, requestType?: RequestType) { requestType = requestType || RequestType.FRIENDREQUEST; - const request = await models.SqRequest.create({senderId: sender, receiverId: receiver, requestType}); + const request = await models.Request.create({senderId: sender, receiverId: receiver, requestType}); globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, Request); return request; } diff --git a/src/lib/dataaccess/models/ChatMember.ts b/src/lib/dataaccess/models/ChatMember.ts new file mode 100644 index 0000000..dacd80a --- /dev/null +++ b/src/lib/dataaccess/models/ChatMember.ts @@ -0,0 +1,14 @@ +import {Column, ForeignKey, Model, Table,} from "sequelize-typescript"; +import {ChatRoom} from "./ChatRoom"; +import {User} from "./User"; + +@Table({underscored: true}) +export class ChatMember extends Model { + @ForeignKey(() => User) + @Column + public userId: number; + + @ForeignKey(() => ChatRoom) + @Column + public chatId: number; +} diff --git a/src/lib/dataaccess/models/ChatMessage.ts b/src/lib/dataaccess/models/ChatMessage.ts new file mode 100644 index 0000000..9811579 --- /dev/null +++ b/src/lib/dataaccess/models/ChatMessage.ts @@ -0,0 +1,41 @@ +import * as sqz from "sequelize"; +import {BelongsTo, Column, CreatedAt, ForeignKey, Model, Table,} from "sequelize-typescript"; +import markdown from "../../markdown"; +import {ChatRoom} from "./ChatRoom"; +import {User} from "./User"; + +@Table({underscored: true}) +export class ChatMessage extends Model { + + @Column(sqz.STRING(512)) + public content: string; + + @ForeignKey(() => ChatRoom) + @Column + public chatId: number; + + @ForeignKey(() => User) + @Column + public authorId: number; + + @BelongsTo(() => ChatRoom, "chatId") + public rChat: ChatRoom; + + @BelongsTo(() => User, "authorId") + public rAuthor: User; + + @CreatedAt + public createdAt: Date; + + public async chat(): Promise { + return await this.$get("rChat") as ChatRoom; + } + + public async author(): Promise { + return await this.$get("rAuthor") as User; + } + + public get htmlContent(): string { + return markdown.renderInline(this.getDataValue("content")); + } +} diff --git a/src/lib/dataaccess/models/ChatRoom.ts b/src/lib/dataaccess/models/ChatRoom.ts new file mode 100644 index 0000000..386da0f --- /dev/null +++ b/src/lib/dataaccess/models/ChatRoom.ts @@ -0,0 +1,28 @@ +import {BelongsToMany, CreatedAt, HasMany, Model, Table,} from "sequelize-typescript"; +import {ChatMember} from "./ChatMember"; +import {ChatMessage} from "./ChatMessage"; +import {User} from "./User"; + +@Table({underscored: true}) +export class ChatRoom extends Model { + @BelongsToMany(() => User, () => ChatMember) + public rMembers: User[]; + + @HasMany(() => ChatMessage, "chatId") + public rMessages: ChatMessage[]; + + @CreatedAt + public readonly createdAt!: Date; + + public async members(): Promise { + return await this.$get("rMembers") as User[]; + } + + public async messages(): Promise { + return await this.$get("rMessages") as ChatMessage[]; + } + + public get namespace(): string { + return "/chats/" + this.getDataValue("id"); + } +} diff --git a/src/lib/dataaccess/models/Friendship.ts b/src/lib/dataaccess/models/Friendship.ts new file mode 100644 index 0000000..d383e7f --- /dev/null +++ b/src/lib/dataaccess/models/Friendship.ts @@ -0,0 +1,14 @@ +import {Column, ForeignKey, Model, Table} from "sequelize-typescript"; +import {User} from "./User"; + +@Table({underscored: true}) +export class Friendship extends Model { + + @ForeignKey(() => User) + @Column + public userId: number; + + @ForeignKey(() => User) + @Column + public friendId: number; +} diff --git a/src/lib/dataaccess/models/Post.ts b/src/lib/dataaccess/models/Post.ts new file mode 100644 index 0000000..b027822 --- /dev/null +++ b/src/lib/dataaccess/models/Post.ts @@ -0,0 +1,64 @@ +import * as sqz from "sequelize"; +import {BelongsTo, BelongsToMany, Column, CreatedAt, ForeignKey, Model, Table,} from "sequelize-typescript"; +import markdown from "../../markdown"; +import {PostVote, VoteType} from "./PostVote"; +import {User} from "./User"; + +@Table({underscored: true}) +export class Post extends Model { + @Column(sqz.STRING(2048)) + public content: string; + + @ForeignKey(() => User) + @Column + public authorId: number; + + @BelongsTo(() => User, "authorId") + public rAuthor: User; + + @BelongsToMany(() => User, () => PostVote) + public rVotes: Array; + + @CreatedAt + public readonly createdAt!: Date; + + public async author(): Promise { + return await this.$get("rAuthor") as User; + } + + public async votes(): Promise> { + return await this.$get("rVotes") as Array; + } + + public get htmlContent() { + return markdown.render(this.getDataValue("content")); + } + + public async upvotes() { + return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.UPVOTE).length; + } + + public async downvotes() { + return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.DOWNVOTE).length; + } + + public async vote(userId: number, type: VoteType): Promise { + type = type || VoteType.UPVOTE; + let vote = await PostVote.findOne({where: {user_id: userId, post_id: this.id}}); + if (!vote) { + await this.$add("rVotes", userId); + vote = await PostVote.findOne({where: {user_id: userId, post_id: this.id}}); + } + if (vote) { + if (vote.voteType === type) { + await vote.destroy(); + return null; + } else { + vote.voteType = type; + await vote.save(); + } + } + + return vote.voteType; + } +} diff --git a/src/lib/dataaccess/models/PostVote.ts b/src/lib/dataaccess/models/PostVote.ts new file mode 100644 index 0000000..058176d --- /dev/null +++ b/src/lib/dataaccess/models/PostVote.ts @@ -0,0 +1,23 @@ +import * as sqz from "sequelize"; +import {Column, ForeignKey, Model, Table,} from "sequelize-typescript"; +import {Post} from "./Post"; +import {User} from "./User"; + +export enum VoteType { + UPVOTE = "UPVOTE", + DOWNVOTE = "DOWNVOTE", +} + +@Table({underscored: true}) +export class PostVote extends Model { + @Column({type: sqz.ENUM, values: ["UPVOTE", "DOWNVOTE"]}) + public voteType: VoteType; + + @ForeignKey(() => User) + @Column + public userId: number; + + @ForeignKey(() => Post) + @Column + public postId: number; +} diff --git a/src/lib/dataaccess/models/Request.ts b/src/lib/dataaccess/models/Request.ts new file mode 100644 index 0000000..fffa09f --- /dev/null +++ b/src/lib/dataaccess/models/Request.ts @@ -0,0 +1,37 @@ +import * as sqz from "sequelize"; +import {BelongsTo, Column, ForeignKey, Model, Table,} from "sequelize-typescript"; +import {User} from "./User"; + +export enum RequestType { + FRIENDREQUEST = "FRIENDREQUEST", + GROUPINVITE = "GROUPINVITE", + EVENTINVITE = "EVENTINVITE", +} + +@Table({underscored: true}) +export class Request extends Model { + @Column({type: sqz.ENUM, values: ["FRIENDREQUEST", "GROUPINVITE", "EVENTINVITE"]}) + public requestType: RequestType; + + @ForeignKey(() => User) + @Column + public senderId: number; + + @BelongsTo(() => User, "senderId") + public rSender: User; + + @ForeignKey(() => User) + @Column + public receiverId: number; + + @BelongsTo(() => User, "receiverId") + public rReceiver: User; + + public async receiver(): Promise { + return await this.$get("rReceiver") as User; + } + + public async sender(): Promise { + return await this.$get("rSender") as User; + } +} diff --git a/src/lib/dataaccess/models/User.ts b/src/lib/dataaccess/models/User.ts new file mode 100644 index 0000000..1219173 --- /dev/null +++ b/src/lib/dataaccess/models/User.ts @@ -0,0 +1,105 @@ +import * as sqz from "sequelize"; +import {BelongsToMany, Column, CreatedAt, HasMany, Model, Table, UpdatedAt,} from "sequelize-typescript"; +import {RequestNotFoundError} from "../../errors/RequestNotFoundError"; +import {ChatMember} from "./ChatMember"; +import {ChatMessage} from "./ChatMessage"; +import {ChatRoom} from "./ChatRoom"; +import {Friendship} from "./Friendship"; +import {Post} from "./Post"; +import {PostVote} from "./PostVote"; +import {Request, RequestType} from "./Request"; + +@Table({underscored: true}) +export class User extends Model { + @Column(sqz.STRING(128)) + public username: string; + + @Column(sqz.STRING(128)) + public handle: string; + + @Column(sqz.STRING(128)) + public email: string; + + @Column(sqz.STRING(128)) + public password: string; + + @Column({defaultValue: 0}) + public rankpoints: number; + + @BelongsToMany(() => User, () => Friendship) + public friends: User[]; + + @BelongsToMany(() => Post, () => PostVote) + public votes: Array; + + @BelongsToMany(() => ChatRoom, () => ChatMember) + public rChats: ChatRoom[]; + + @HasMany(() => Post, "authorId") + public rPosts: Post[]; + + @HasMany(() => Request, "receiverId") + public rSentRequests: Request[]; + + @HasMany(() => Request, "receiverId") + public rReceivedRequests: Request[]; + + @HasMany(() => ChatMessage, "authorId") + public messages: ChatMessage[]; + + @CreatedAt + public readonly createdAt!: Date; + + @UpdatedAt + public readonly updatedAt!: Date; + + public get name(): string { + return this.getDataValue("username"); + } + + public get joinedAt(): Date { + return this.getDataValue("createdAt"); + } + + public async chats(): Promise { + return await this.$get("rChats") as ChatRoom[]; + } + + public async sentRequests(): Promise { + return await this.$get("rSentRequests") as Request[]; + } + + public async receivedRequests(): Promise { + return await this.$get("rReceivedRequests") as Request[]; + } + + public async posts({first, offset}: {first: number, offset: number}): Promise { + return await this.$get("rPosts", {limit: first, offset}) as Post[]; + } + + public async numberOfPosts(): Promise { + return this.$count("rPosts"); + } + + public async denyRequest(sender: number, type: RequestType) { + const request = await this.$get("rReceivedRequests", + {where: {senderId: sender, requestType: type}}) as Request[]; + if (request[0]) { + await request[0].destroy(); + } + } + + public async acceptRequest(sender: number, type: RequestType) { + const requests = await this.$get("rReceivedRequests", + {where: {senderId: sender, requestType: type}}) as Request[]; + if (requests.length > 0) { + const request = requests[0]; + if (request.requestType === RequestType.FRIENDREQUEST) { + await this.$add("friends", sender); + await request.destroy(); + } + } else { + throw new RequestNotFoundError(sender, this.id, type); + } + } +} diff --git a/src/lib/dataaccess/models/index.ts b/src/lib/dataaccess/models/index.ts new file mode 100644 index 0000000..9e47059 --- /dev/null +++ b/src/lib/dataaccess/models/index.ts @@ -0,0 +1,8 @@ +export {ChatMember} from "./ChatMember"; +export {ChatMessage} from "./ChatMessage"; +export {ChatRoom} from "./ChatRoom"; +export {Friendship} from "./Friendship"; +export {Post} from "./Post"; +export {PostVote} from "./PostVote"; +export {Request} from "./Request"; +export {User} from "./User"; diff --git a/src/lib/dataaccess/wrappers.ts b/src/lib/dataaccess/wrappers.ts deleted file mode 100644 index 052fb95..0000000 --- a/src/lib/dataaccess/wrappers.ts +++ /dev/null @@ -1,5 +0,0 @@ -export {User} from "./User"; -export {Chatroom} from "./Chatroom"; -export {Post} from "./Post"; -export {Profile} from "./Profile"; -export {ChatMessage} from "./ChatMessage"; diff --git a/src/routes/home.ts b/src/routes/home.ts index 4fb7f79..128088c 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -1,10 +1,7 @@ import {Router} from "express"; import {Namespace, Server} from "socket.io"; import dataaccess from "../lib/dataaccess"; -import {ChatMessage} from "../lib/dataaccess/ChatMessage"; -import {Chatroom} from "../lib/dataaccess/Chatroom"; -import {Request} from "../lib/dataaccess/datamodels/models"; -import {Post} from "../lib/dataaccess/Post"; +import {ChatMessage, ChatRoom, Post, Request, User} from "../lib/dataaccess/models"; import globals from "../lib/globals"; import {InternalEvents} from "../lib/InternalEvents"; import Route from "../lib/Route"; @@ -37,18 +34,18 @@ class HomeRoute extends Route { socket.on("postCreate", async (content) => { if (socket.handshake.session.userId) { const post = await dataaccess.createPost(content, socket.handshake.session.userId); - io.emit("post", Object.assign(post, {htmlContent: post.htmlContent()})); + io.emit("post", Object.assign(post, {htmlContent: post.htmlContent})); } else { socket.emit("error", "Not logged in!"); } }); globals.internalEmitter.on(InternalEvents.REQUESTCREATE, async (request: Request) => { - if ((await request.getSender()).id === socket.handshake.session.userId) { + if ((await request.$get("sender") as User).id === socket.handshake.session.userId) { socket.emit("request", request); } }); globals.internalEmitter.on(InternalEvents.GQLPOSTCREATE, async (post: Post) => { - socket.emit("post", Object.assign(post, {htmlContent: post.htmlContent()})); + socket.emit("post", Object.assign(post, {htmlContent: post.htmlContent})); }); }); @@ -56,7 +53,7 @@ class HomeRoute extends Route { for (const chat of chats) { chatRooms[chat.id] = this.getChatSocketNamespace(chat.id); } - globals.internalEmitter.on(InternalEvents.CHATCREATE, (chat: Chatroom) => { + globals.internalEmitter.on(InternalEvents.CHATCREATE, (chat: ChatRoom) => { chatRooms[chat.id] = this.getChatSocketNamespace(chat.id); }); } @@ -82,15 +79,15 @@ class HomeRoute extends Route { if (socket.handshake.session.userId) { const userId = socket.handshake.session.userId; const message = await dataaccess.sendChatMessage(userId, chatId, content); - socket.broadcast.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent()})); - socket.emit("chatMessageSent", Object.assign(message, {htmlContent: message.htmlContent()})); + socket.broadcast.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent})); + socket.emit("chatMessageSent", Object.assign(message, {htmlContent: message.htmlContent})); } else { socket.emit("error", "Not logged in!"); } }); globals.internalEmitter.on(InternalEvents.GQLCHATMESSAGE, async (message: ChatMessage) => { - if ((await message.chat()).id === chatId) { - socket.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent()})); + if ((await message.$get("chat") as ChatRoom).id === chatId) { + socket.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent})); } }); }); diff --git a/tsconfig.json b/tsconfig.json index 6fedd9d..f8deaa4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,9 @@ "target": "es2018", "allowJs": true, "moduleResolution": "node", - "module": "commonjs" + "module": "commonjs", + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": [ "src/**/*" From c68f11080f14bd9cd26bdb9286279532b797c5c4 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 13 Oct 2019 18:17:47 +0200 Subject: [PATCH 4/4] Data access improvements - fixed votes - added column constraints - added post not found error to vote --- .gitignore | 1 + src/default-config.yaml | 2 +- src/graphql/resolvers.ts | 11 +- src/lib/MemoryCache.ts | 73 ---------- .../{dataaccess/index.ts => dataaccess.ts} | 10 +- src/lib/errors/graphqlErrors.ts | 7 +- src/lib/globals.ts | 5 - src/lib/{dataaccess => }/models/ChatMember.ts | 8 +- .../{dataaccess => }/models/ChatMessage.ts | 13 +- src/lib/{dataaccess => }/models/ChatRoom.ts | 0 src/lib/{dataaccess => }/models/Friendship.ts | 8 +- src/lib/{dataaccess => }/models/Post.ts | 30 ++-- src/lib/{dataaccess => }/models/PostVote.ts | 11 +- src/lib/{dataaccess => }/models/Request.ts | 12 +- src/lib/{dataaccess => }/models/User.ts | 31 +++- src/lib/{dataaccess => }/models/index.ts | 0 src/routes/home.ts | 2 +- src/sql/create-tables.sql | 137 ------------------ src/sql/update-tables.sql | 19 --- 19 files changed, 97 insertions(+), 283 deletions(-) delete mode 100644 src/lib/MemoryCache.ts rename src/lib/{dataaccess/index.ts => dataaccess.ts} (95%) rename src/lib/{dataaccess => }/models/ChatMember.ts (60%) rename src/lib/{dataaccess => }/models/ChatMessage.ts (73%) rename src/lib/{dataaccess => }/models/ChatRoom.ts (100%) rename src/lib/{dataaccess => }/models/Friendship.ts (57%) rename src/lib/{dataaccess => }/models/Post.ts (63%) rename src/lib/{dataaccess => }/models/PostVote.ts (57%) rename src/lib/{dataaccess => }/models/Request.ts (75%) rename src/lib/{dataaccess => }/models/User.ts (82%) rename src/lib/{dataaccess => }/models/index.ts (100%) delete mode 100644 src/sql/create-tables.sql delete mode 100644 src/sql/update-tables.sql diff --git a/.gitignore b/.gitignore index 92b6a08..982d25a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist .idea config.yaml sqz-force +greenvironment.db diff --git a/src/default-config.yaml b/src/default-config.yaml index 58a8ecc..0c24242 100644 --- a/src/default-config.yaml +++ b/src/default-config.yaml @@ -1,6 +1,6 @@ # database connection info database: - connectionUri: "sqlite://:memory:" + connectionUri: "sqlite://greenvironment.db" # http server configuration server: diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 8867c30..a764a9c 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -1,8 +1,8 @@ import {GraphQLError} from "graphql"; import * as status from "http-status"; import dataaccess from "../lib/dataaccess"; -import * as models from "../lib/dataaccess/models"; -import {NotLoggedInGqlError} from "../lib/errors/graphqlErrors"; +import * as models from "../lib/models"; +import {NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors"; import globals from "../lib/globals"; import {InternalEvents} from "../lib/InternalEvents"; import {is} from "../lib/regex"; @@ -103,7 +103,12 @@ export function resolver(req: any, res: any): any { if (postId && type) { if (req.session.userId) { const post = await models.Post.findByPk(postId); - return await post.vote(req.session.userId, type); + if (post) { + return await post.vote(req.session.userId, type); + } else { + res.status(400); + return new PostNotFoundGqlError(postId); + } } else { res.status(status.UNAUTHORIZED); return new NotLoggedInGqlError(); diff --git a/src/lib/MemoryCache.ts b/src/lib/MemoryCache.ts deleted file mode 100644 index 1e76834..0000000 --- a/src/lib/MemoryCache.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as crypto from "crypto"; -import {EventEmitter} from "events"; - -export class MemoryCache extends EventEmitter { - private cacheItems: any = {}; - private cacheExpires: any = {}; - private expireCheck: NodeJS.Timeout; - - /** - * Creates interval function. - * @param ttl - */ - constructor(private ttl: number = 500) { - super(); - this.expireCheck = setInterval(() => this.checkExpires(), ttl / 2); - } - - /** - * Creates a md5 hash of the given key. - * @param key - */ - public hashKey(key: string): string { - const hash = crypto.createHash("sha1"); - const data = hash.update(key, "utf8"); - return data.digest("hex"); - } - - /** - * Sets an entry. - * @param key - * @param value - */ - public set(key: string, value: any) { - this.cacheItems[key] = value; - this.cacheExpires[key] = Date.now() + this.ttl; - this.emit("set", key, value); - } - - /** - * Returns the entry stored with the given key. - * @param key - */ - public get(key: string) { - if (this.cacheItems.hasOwnProperty(key)) { - this.emit("hit", key, this.cacheItems[key]); - return this.cacheItems[key]; - } else { - this.emit("miss", key); - } - } - - /** - * Deletes a cache item. - * @param key - */ - public delete(key: string) { - this.emit("delete", key); - delete this.cacheItems[key]; - } - - /** - * Checks expires and clears items that are over the expire value. - */ - private checkExpires() { - for (const [key, value] of Object.entries(this.cacheExpires)) { - if (value < Date.now()) { - this.emit("delete", key); - delete this.cacheItems[key]; - delete this.cacheExpires[key]; - } - } - } -} diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess.ts similarity index 95% rename from src/lib/dataaccess/index.ts rename to src/lib/dataaccess.ts index e5f04cc..e13faca 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess.ts @@ -1,9 +1,9 @@ import {Sequelize} from "sequelize-typescript"; -import {ChatNotFoundError} from "../errors/ChatNotFoundError"; -import {EmailAlreadyRegisteredError} from "../errors/EmailAlreadyRegisteredError"; -import {UserNotFoundError} from "../errors/UserNotFoundError"; -import globals from "../globals"; -import {InternalEvents} from "../InternalEvents"; +import {ChatNotFoundError} from "./errors/ChatNotFoundError"; +import {EmailAlreadyRegisteredError} from "./errors/EmailAlreadyRegisteredError"; +import {UserNotFoundError} from "./errors/UserNotFoundError"; +import globals from "./globals"; +import {InternalEvents} from "./InternalEvents"; import * as models from "./models"; /** diff --git a/src/lib/errors/graphqlErrors.ts b/src/lib/errors/graphqlErrors.ts index 9e0b1b5..b33a883 100644 --- a/src/lib/errors/graphqlErrors.ts +++ b/src/lib/errors/graphqlErrors.ts @@ -1,8 +1,13 @@ import {GraphQLError} from "graphql"; export class NotLoggedInGqlError extends GraphQLError { - constructor() { super("Not logged in"); } } + +export class PostNotFoundGqlError extends GraphQLError { + constructor(postId: number) { + super(`Post '${postId}' not found!`); + } +} diff --git a/src/lib/globals.ts b/src/lib/globals.ts index 0a6669a..6fe2899 100644 --- a/src/lib/globals.ts +++ b/src/lib/globals.ts @@ -9,7 +9,6 @@ import {EventEmitter} from "events"; import * as fsx from "fs-extra"; import * as yaml from "js-yaml"; import * as winston from "winston"; -import {MemoryCache} from "./MemoryCache"; const configPath = "config.yaml"; const defaultConfig = __dirname + "/../default-config.yaml"; @@ -28,7 +27,6 @@ if (!(fsx.pathExistsSync(configPath))) { */ namespace globals { export const config = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8")); - export const cache = new MemoryCache(1200); export const logger = winston.createLogger({ transports: [ new winston.transports.Console({ @@ -44,9 +42,6 @@ namespace globals { ], }); export const internalEmitter = new EventEmitter(); - cache.on("set", (key) => logger.debug(`Caching '${key}'.`)); - cache.on("miss", (key) => logger.debug(`Cache miss for '${key}'`)); - cache.on("hit", (key) => logger.debug(`Cache hit for '${key}'`)); } export default globals; diff --git a/src/lib/dataaccess/models/ChatMember.ts b/src/lib/models/ChatMember.ts similarity index 60% rename from src/lib/dataaccess/models/ChatMember.ts rename to src/lib/models/ChatMember.ts index dacd80a..f2b7aad 100644 --- a/src/lib/dataaccess/models/ChatMember.ts +++ b/src/lib/models/ChatMember.ts @@ -1,14 +1,16 @@ -import {Column, ForeignKey, Model, Table,} from "sequelize-typescript"; +import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; import {ChatRoom} from "./ChatRoom"; import {User} from "./User"; @Table({underscored: true}) export class ChatMember extends Model { @ForeignKey(() => User) - @Column + @NotNull + @Column({allowNull: false}) public userId: number; @ForeignKey(() => ChatRoom) - @Column + @NotNull + @Column({allowNull: false}) public chatId: number; } diff --git a/src/lib/dataaccess/models/ChatMessage.ts b/src/lib/models/ChatMessage.ts similarity index 73% rename from src/lib/dataaccess/models/ChatMessage.ts rename to src/lib/models/ChatMessage.ts index 9811579..3b7c357 100644 --- a/src/lib/dataaccess/models/ChatMessage.ts +++ b/src/lib/models/ChatMessage.ts @@ -1,21 +1,24 @@ import * as sqz from "sequelize"; -import {BelongsTo, Column, CreatedAt, ForeignKey, Model, Table,} from "sequelize-typescript"; -import markdown from "../../markdown"; +import {BelongsTo, Column, CreatedAt, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; +import markdown from "../markdown"; import {ChatRoom} from "./ChatRoom"; import {User} from "./User"; @Table({underscored: true}) export class ChatMessage extends Model { - @Column(sqz.STRING(512)) + @NotNull + @Column({type: sqz.STRING(512), allowNull: false}) public content: string; @ForeignKey(() => ChatRoom) - @Column + @NotNull + @Column({allowNull: false}) public chatId: number; @ForeignKey(() => User) - @Column + @NotNull + @Column({allowNull: false}) public authorId: number; @BelongsTo(() => ChatRoom, "chatId") diff --git a/src/lib/dataaccess/models/ChatRoom.ts b/src/lib/models/ChatRoom.ts similarity index 100% rename from src/lib/dataaccess/models/ChatRoom.ts rename to src/lib/models/ChatRoom.ts diff --git a/src/lib/dataaccess/models/Friendship.ts b/src/lib/models/Friendship.ts similarity index 57% rename from src/lib/dataaccess/models/Friendship.ts rename to src/lib/models/Friendship.ts index d383e7f..4d7773b 100644 --- a/src/lib/dataaccess/models/Friendship.ts +++ b/src/lib/models/Friendship.ts @@ -1,14 +1,16 @@ -import {Column, ForeignKey, Model, Table} from "sequelize-typescript"; +import {Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript"; import {User} from "./User"; @Table({underscored: true}) export class Friendship extends Model { @ForeignKey(() => User) - @Column + @NotNull + @Column({allowNull: false}) public userId: number; @ForeignKey(() => User) - @Column + @NotNull + @Column({allowNull: false}) public friendId: number; } diff --git a/src/lib/dataaccess/models/Post.ts b/src/lib/models/Post.ts similarity index 63% rename from src/lib/dataaccess/models/Post.ts rename to src/lib/models/Post.ts index b027822..93151a3 100644 --- a/src/lib/dataaccess/models/Post.ts +++ b/src/lib/models/Post.ts @@ -1,16 +1,18 @@ import * as sqz from "sequelize"; -import {BelongsTo, BelongsToMany, Column, CreatedAt, ForeignKey, Model, Table,} from "sequelize-typescript"; -import markdown from "../../markdown"; +import {BelongsTo, BelongsToMany, Column, CreatedAt, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; +import markdown from "../markdown"; import {PostVote, VoteType} from "./PostVote"; import {User} from "./User"; @Table({underscored: true}) export class Post extends Model { - @Column(sqz.STRING(2048)) + @NotNull + @Column({type: sqz.STRING(2048), allowNull: false}) public content: string; @ForeignKey(() => User) - @Column + @NotNull + @Column({allowNull: false}) public authorId: number; @BelongsTo(() => User, "authorId") @@ -44,21 +46,25 @@ export class Post extends Model { public async vote(userId: number, type: VoteType): Promise { type = type || VoteType.UPVOTE; - let vote = await PostVote.findOne({where: {user_id: userId, post_id: this.id}}); + let votes = await this.$get("rVotes", {where: {id: userId}}) as Array; + let vote = votes[0] || null; + let created = false; if (!vote) { - await this.$add("rVotes", userId); - vote = await PostVote.findOne({where: {user_id: userId, post_id: this.id}}); + await this.$add("rVote", userId); + votes = await this.$get("rVotes", {where: {id: userId}}) as Array; + vote = votes[0] || null; + created = true; } if (vote) { - if (vote.voteType === type) { - await vote.destroy(); + if (vote.PostVote.voteType === type && !created) { + await vote.PostVote.destroy(); return null; } else { - vote.voteType = type; - await vote.save(); + vote.PostVote.voteType = type; + await vote.PostVote.save(); } } - return vote.voteType; + return vote.PostVote.voteType; } } diff --git a/src/lib/dataaccess/models/PostVote.ts b/src/lib/models/PostVote.ts similarity index 57% rename from src/lib/dataaccess/models/PostVote.ts rename to src/lib/models/PostVote.ts index 058176d..50517f2 100644 --- a/src/lib/dataaccess/models/PostVote.ts +++ b/src/lib/models/PostVote.ts @@ -1,5 +1,5 @@ import * as sqz from "sequelize"; -import {Column, ForeignKey, Model, Table,} from "sequelize-typescript"; +import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; import {Post} from "./Post"; import {User} from "./User"; @@ -10,14 +10,17 @@ export enum VoteType { @Table({underscored: true}) export class PostVote extends Model { - @Column({type: sqz.ENUM, values: ["UPVOTE", "DOWNVOTE"]}) + @NotNull + @Column({type: sqz.ENUM, values: ["UPVOTE", "DOWNVOTE"], defaultValue: "UPVOTE", allowNull: false}) public voteType: VoteType; @ForeignKey(() => User) - @Column + @NotNull + @Column({allowNull: false}) public userId: number; @ForeignKey(() => Post) - @Column + @NotNull + @Column({allowNull: false}) public postId: number; } diff --git a/src/lib/dataaccess/models/Request.ts b/src/lib/models/Request.ts similarity index 75% rename from src/lib/dataaccess/models/Request.ts rename to src/lib/models/Request.ts index fffa09f..87f6f85 100644 --- a/src/lib/dataaccess/models/Request.ts +++ b/src/lib/models/Request.ts @@ -1,5 +1,5 @@ import * as sqz from "sequelize"; -import {BelongsTo, Column, ForeignKey, Model, Table,} from "sequelize-typescript"; +import {BelongsTo, Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; import {User} from "./User"; export enum RequestType { @@ -10,18 +10,22 @@ export enum RequestType { @Table({underscored: true}) export class Request extends Model { - @Column({type: sqz.ENUM, values: ["FRIENDREQUEST", "GROUPINVITE", "EVENTINVITE"]}) + @NotNull + @Column({type: sqz.ENUM, values: ["FRIENDREQUEST", "GROUPINVITE", "EVENTINVITE"], + defaultValue: "FRIENDREQUEST", allowNull: false}) public requestType: RequestType; @ForeignKey(() => User) - @Column + @NotNull + @Column({allowNull: false}) public senderId: number; @BelongsTo(() => User, "senderId") public rSender: User; @ForeignKey(() => User) - @Column + @NotNull + @Column({allowNull: false}) public receiverId: number; @BelongsTo(() => User, "receiverId") diff --git a/src/lib/dataaccess/models/User.ts b/src/lib/models/User.ts similarity index 82% rename from src/lib/dataaccess/models/User.ts rename to src/lib/models/User.ts index 1219173..1c4d567 100644 --- a/src/lib/dataaccess/models/User.ts +++ b/src/lib/models/User.ts @@ -1,6 +1,16 @@ import * as sqz from "sequelize"; -import {BelongsToMany, Column, CreatedAt, HasMany, Model, Table, UpdatedAt,} from "sequelize-typescript"; -import {RequestNotFoundError} from "../../errors/RequestNotFoundError"; +import { + BelongsToMany, + Column, + CreatedAt, + HasMany, + Model, + NotNull, + Table, + Unique, + UpdatedAt, +} from "sequelize-typescript"; +import {RequestNotFoundError} from "../errors/RequestNotFoundError"; import {ChatMember} from "./ChatMember"; import {ChatMessage} from "./ChatMessage"; import {ChatRoom} from "./ChatRoom"; @@ -11,19 +21,26 @@ import {Request, RequestType} from "./Request"; @Table({underscored: true}) export class User extends Model { - @Column(sqz.STRING(128)) + @NotNull + @Column({type: sqz.STRING(128), allowNull: false}) public username: string; - @Column(sqz.STRING(128)) + @NotNull + @Unique + @Column({type: sqz.STRING(128), allowNull: false, unique: true}) public handle: string; - @Column(sqz.STRING(128)) + @Unique + @NotNull + @Column({type: sqz.STRING(128), allowNull: false, unique: true}) public email: string; - @Column(sqz.STRING(128)) + @NotNull + @Column({type: sqz.STRING(128), allowNull: false}) public password: string; - @Column({defaultValue: 0}) + @NotNull + @Column({defaultValue: 0, allowNull: false}) public rankpoints: number; @BelongsToMany(() => User, () => Friendship) diff --git a/src/lib/dataaccess/models/index.ts b/src/lib/models/index.ts similarity index 100% rename from src/lib/dataaccess/models/index.ts rename to src/lib/models/index.ts diff --git a/src/routes/home.ts b/src/routes/home.ts index 128088c..102b88f 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -1,7 +1,7 @@ import {Router} from "express"; import {Namespace, Server} from "socket.io"; import dataaccess from "../lib/dataaccess"; -import {ChatMessage, ChatRoom, Post, Request, User} from "../lib/dataaccess/models"; +import {ChatMessage, ChatRoom, Post, Request, User} from "../lib/models"; import globals from "../lib/globals"; import {InternalEvents} from "../lib/InternalEvents"; import Route from "../lib/Route"; diff --git a/src/sql/create-tables.sql b/src/sql/create-tables.sql deleted file mode 100644 index 70e49e2..0000000 --- a/src/sql/create-tables.sql +++ /dev/null @@ -1,137 +0,0 @@ ---create functions -DO $$BEGIN - - IF NOT EXISTS(SELECT 1 from pg_proc WHERE proname = 'function_exists') THEN - CREATE FUNCTION function_exists(text) RETURNS boolean LANGUAGE plpgsql AS $BODY$ - BEGIN - RETURN EXISTS(SELECT 1 from pg_proc WHERE proname = $1); - END $BODY$; - END IF; - - IF NOT function_exists('type_exists') THEN - CREATE FUNCTION type_exists(text) RETURNS boolean LANGUAGE plpgsql AS $BODY$ - BEGIN - RETURN EXISTS (SELECT 1 FROM pg_type WHERE typname = $1); - END $BODY$; - END IF; - -END$$; - ---create types -DO $$ BEGIN - - IF NOT type_exists('votetype') THEN - CREATE TYPE votetype AS enum ('DOWNVOTE', 'UPVOTE'); - END IF; - - IF NOT type_exists('posttype') THEN - CREATE TYPE posttype AS enum ('MISC', 'ACTION', 'IMAGE', 'TEXT'); - END IF; - - IF NOT type_exists('requesttype') THEN - CREATE TYPE requesttype AS enum ('FRIENDREQUEST'); - END IF; - -END$$; - --- create functions relying on types - -DO $$ BEGIN - - IF NOT function_exists('cast_to_votetype') THEN - CREATE FUNCTION cast_to_votetype(text) RETURNS votetype LANGUAGE plpgsql AS $BODY$ - BEGIN - RETURN CASE WHEN $1::votetype IS NULL THEN 'UPVOTE' ELSE $1::votetype END; - END $BODY$; - END IF; - - IF NOT function_exists('cast_to_posttype') THEN - CREATE FUNCTION cast_to_posttype(text) RETURNS posttype LANGUAGE plpgsql AS $BODY$ - BEGIN - RETURN CASE WHEN $1::posttype IS NULL THEN 'MISC' ELSE $1::posttype END; - END $BODY$; - END IF; - -END$$; - --- create tables -DO $$ BEGIN - - CREATE TABLE IF NOT EXISTS "user_sessions" ( - "sid" varchar NOT NULL, - "sess" json NOT NULL, - "expire" timestamp(6) NOT NULL, - PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE - ) WITH (OIDS=FALSE); - - CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - name varchar(128) NOT NULL, - handle varchar(128) UNIQUE NOT NULL, - password varchar(1024) NOT NULL, - email varchar(128) UNIQUE NOT NULL, - greenpoints INTEGER DEFAULT 0, - joined_at TIMESTAMP DEFAULT now() - ); - - CREATE TABLE IF NOT EXISTS posts ( - id BIGSERIAL PRIMARY KEY, - upvotes INTEGER DEFAULT 0, - downvotes INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT now(), - content text, - author SERIAL REFERENCES users (id) ON DELETE CASCADE, - type posttype NOT NULL DEFAULT 'MISC' - ); - - CREATE TABLE IF NOT EXISTS votes ( - user_id SERIAL REFERENCES users (id) ON DELETE CASCADE, - item_id BIGSERIAL REFERENCES posts (id) ON DELETE CASCADE, - vote_type votetype DEFAULT 'DOWNVOTE', - PRIMARY KEY (user_id, item_id) - ); - - CREATE TABLE IF NOT EXISTS events ( - id BIGSERIAL PRIMARY KEY, - time TIMESTAMP, - owner SERIAL REFERENCES users (id) - ); - - CREATE TABLE IF NOT EXISTS event_members ( - event BIGSERIAL REFERENCES events (id), - member SERIAL REFERENCES users (id), - PRIMARY KEY (event, member) - ); - - CREATE TABLE IF NOT EXISTS chats ( - id BIGSERIAL PRIMARY KEY - ); - - CREATE TABLE IF NOT EXISTS chat_messages ( - chat BIGSERIAL REFERENCES chats (id) ON DELETE CASCADE, - author SERIAL REFERENCES users (id) ON DELETE SET NULL, - content VARCHAR(1024) NOT NULL, - created_at TIMESTAMP DEFAULT now(), - PRIMARY KEY (chat, author, created_at) - ); - - CREATE TABLE IF NOT EXISTS chat_members ( - chat BIGSERIAL REFERENCES chats (id) ON DELETE CASCADE, - member SERIAL REFERENCES users (id) ON DELETE CASCADE, - PRIMARY KEY (chat, member) - ); - - CREATE TABLE IF NOT EXISTS user_friends ( - user_id SERIAL REFERENCES users (id) ON DELETE CASCADE, - friend_id SERIAL REFERENCES users (id) ON DELETE CASCADE, - PRIMARY KEY (user_id, friend_id) - ); - - CREATE TABLE IF NOT EXISTS requests ( - sender SERIAL REFERENCES users (id) ON DELETE CASCADE, - receiver SERIAL REFERENCES users (id) ON DELETE CASCADE, - type requesttype DEFAULT 'FRIENDREQUEST', - PRIMARY KEY (sender, receiver, type) - ); - -END $$; diff --git a/src/sql/update-tables.sql b/src/sql/update-tables.sql deleted file mode 100644 index 858a214..0000000 --- a/src/sql/update-tables.sql +++ /dev/null @@ -1,19 +0,0 @@ -DO $$ BEGIN - - ALTER TABLE IF EXISTS votes - ADD COLUMN IF NOT EXISTS vote_type votetype DEFAULT 'UPVOTE', - ALTER COLUMN vote_type TYPE votetype USING cast_to_votetype(vote_type::text), - ALTER COLUMN vote_type DROP DEFAULT, - ALTER COLUMN vote_type SET DEFAULT 'UPVOTE'; - - ALTER TABLE IF EXISTS posts - ALTER COLUMN type TYPE posttype USING cast_to_posttype(type::text), - ALTER COLUMN type DROP DEFAULT, - ALTER COLUMN type SET DEFAULT 'MISC', - DROP COLUMN IF EXISTS upvotes, - DROP COLUMN IF EXISTS downvotes; - - ALTER TABLE requests - ADD COLUMN IF NOT EXISTS type requesttype DEFAULT 'FRIENDREQUEST'; - -END $$;