From 87c08df02bd18e417e757600af54cf811cee7c0d Mon Sep 17 00:00:00 2001 From: Jorge Vargas Date: Mon, 19 Aug 2024 00:28:48 -0600 Subject: [PATCH] Set up sequelize --- codegen.ts | 1 + package-lock.json | 268 ++++++++++++++++ package.json | 5 + src/constants/index.ts | 2 + src/graphql/apolloClient.mts | 18 +- src/graphql/resolvers/index.js | 7 + src/graphql/resolvers/mutations/comments.js | 54 ++++ src/graphql/resolvers/mutations/create.js | 81 +++++ src/graphql/resolvers/mutations/index.js | 12 + src/graphql/resolvers/mutations/requests.js | 86 +++++ src/graphql/resolvers/mutations/site.js | 37 +++ src/graphql/resolvers/mutations/update.js | 335 ++++++++++++++++++++ src/graphql/resolvers/mutations/user.js | 220 +++++++++++++ src/graphql/resolvers/queries/album.ts | 58 ++++ src/graphql/resolvers/queries/index.ts | 15 + src/graphql/resolvers/queries/requests.js | 76 +++++ src/graphql/resolvers/queries/search.ts | 82 +++++ src/graphql/resolvers/queries/site.js | 36 +++ src/graphql/resolvers/queries/user.js | 33 ++ src/graphql/resolvers/queries/vgmdb.ts | 9 + src/graphql/resolvers/types/album.js | 154 +++++++++ src/graphql/resolvers/types/index.js | 8 + src/graphql/resolvers/types/user.js | 46 +++ src/graphql/typeDefs/album.graphql | 2 +- src/sequelize/index.js | 22 ++ src/sequelize/models/album.js | 22 ++ src/sequelize/models/animation.js | 35 ++ src/sequelize/models/artist.js | 20 ++ src/sequelize/models/category.js | 19 ++ src/sequelize/models/classification.js | 19 ++ src/sequelize/models/comment.js | 12 + src/sequelize/models/config.js | 23 ++ src/sequelize/models/disc.js | 11 + src/sequelize/models/download.js | 11 + src/sequelize/models/game.js | 27 ++ src/sequelize/models/index.js | 45 +++ src/sequelize/models/link.js | 13 + src/sequelize/models/log.js | 17 + src/sequelize/models/platform.js | 27 ++ src/sequelize/models/publisher.js | 23 ++ src/sequelize/models/request.js | 21 ++ src/sequelize/models/role.js | 14 + src/sequelize/models/series.js | 24 ++ src/sequelize/models/store.js | 10 + src/sequelize/models/submission.js | 26 ++ src/sequelize/models/user.js | 23 ++ src/sequelize/relations.js | 68 ++++ 47 files changed, 2171 insertions(+), 6 deletions(-) create mode 100644 src/constants/index.ts create mode 100644 src/graphql/resolvers/index.js create mode 100644 src/graphql/resolvers/mutations/comments.js create mode 100644 src/graphql/resolvers/mutations/create.js create mode 100644 src/graphql/resolvers/mutations/index.js create mode 100644 src/graphql/resolvers/mutations/requests.js create mode 100644 src/graphql/resolvers/mutations/site.js create mode 100644 src/graphql/resolvers/mutations/update.js create mode 100644 src/graphql/resolvers/mutations/user.js create mode 100644 src/graphql/resolvers/queries/album.ts create mode 100644 src/graphql/resolvers/queries/index.ts create mode 100644 src/graphql/resolvers/queries/requests.js create mode 100644 src/graphql/resolvers/queries/search.ts create mode 100644 src/graphql/resolvers/queries/site.js create mode 100644 src/graphql/resolvers/queries/user.js create mode 100644 src/graphql/resolvers/queries/vgmdb.ts create mode 100644 src/graphql/resolvers/types/album.js create mode 100644 src/graphql/resolvers/types/index.js create mode 100644 src/graphql/resolvers/types/user.js create mode 100644 src/sequelize/index.js create mode 100644 src/sequelize/models/album.js create mode 100644 src/sequelize/models/animation.js create mode 100644 src/sequelize/models/artist.js create mode 100644 src/sequelize/models/category.js create mode 100644 src/sequelize/models/classification.js create mode 100644 src/sequelize/models/comment.js create mode 100644 src/sequelize/models/config.js create mode 100644 src/sequelize/models/disc.js create mode 100644 src/sequelize/models/download.js create mode 100644 src/sequelize/models/game.js create mode 100644 src/sequelize/models/index.js create mode 100644 src/sequelize/models/link.js create mode 100644 src/sequelize/models/log.js create mode 100644 src/sequelize/models/platform.js create mode 100644 src/sequelize/models/publisher.js create mode 100644 src/sequelize/models/request.js create mode 100644 src/sequelize/models/role.js create mode 100644 src/sequelize/models/series.js create mode 100644 src/sequelize/models/store.js create mode 100644 src/sequelize/models/submission.js create mode 100644 src/sequelize/models/user.js create mode 100644 src/sequelize/relations.js diff --git a/codegen.ts b/codegen.ts index 0943826..f5e4c54 100644 --- a/codegen.ts +++ b/codegen.ts @@ -15,6 +15,7 @@ const config: CodegenConfig = { resolverGeneration: 'disabled', typesPluginsConfig: { contextType: '../client.mts#ResolverContext', + maybeValue: 'T' }, add: { './types.generated.ts': { content: '// @ts-nocheck' }, diff --git a/package-lock.json b/package-lock.json index 8349a59..ba2abd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,15 +15,20 @@ "@auth/core": "^0.32.0", "@eddeee888/gcg-typescript-resolver-files": "^0.10.4", "@graphql-codegen/cli": "^5.0.2", + "@graphql-tools/load-files": "^7.0.0", "@graphql-tools/resolvers-composition": "^7.0.1", "@graphql-tools/schema": "^10.0.4", "astro": "^4.13.1", "auth-astro": "^4.1.2", "graphql-scalars": "^1.23.0", + "lodash": "^4.17.21", + "mysql2": "^3.11.0", + "sequelize": "^6.37.3", "tailwindcss": "^3.4.8" }, "devDependencies": { "@parcel/watcher": "^2.4.1", + "@types/lodash": "^4.17.7", "@typescript-eslint/parser": "^6.21.0", "concurrently": "^8.2.2", "eslint": "^8.57.0", @@ -2185,6 +2190,22 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@graphql-tools/load-files": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/load-files/-/load-files-7.0.0.tgz", + "integrity": "sha512-P98amERIwI7FD8Bsq6xUbz9Mj63W8qucfrE/WQjad5jFMZYdFFt46a99FFdfx8S/ZYgpAlj/AZbaTtWLitMgNQ==", + "dependencies": { + "globby": "11.1.0", + "tslib": "^2.4.0", + "unixify": "1.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@graphql-tools/load/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3200,6 +3221,12 @@ "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==" }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -3234,6 +3261,11 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, + "node_modules/@types/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==" + }, "node_modules/@types/ws": { "version": "8.5.12", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", @@ -4198,6 +4230,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.1.tgz", + "integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ==", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axe-core": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz", @@ -5398,6 +5438,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5525,6 +5573,11 @@ "url": "https://dotenvx.com" } }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" + }, "node_modules/dset": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.3.tgz", @@ -6737,6 +6790,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7484,6 +7545,14 @@ "node": ">=8" } }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ] + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -8105,6 +8174,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -8904,6 +8978,11 @@ "node": ">=8" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -9829,6 +9908,25 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -9852,6 +9950,44 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, + "node_modules/mysql2": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz", + "integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mysql2/node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "engines": { + "node": ">=16.14" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -9862,6 +9998,25 @@ "thenify-all": "^1.0.0" } }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -10668,6 +10823,11 @@ "node": ">=8" } }, + "node_modules/pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -11489,6 +11649,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/retry-as-promised": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==" + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -11796,6 +11961,80 @@ "upper-case-first": "^2.0.2" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/sequelize": { + "version": "6.37.3", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.3.tgz", + "integrity": "sha512-V2FTqYpdZjPy3VQrZvjTPnOoLm0KudCRXfGWp48QwhyPPp2yW8z0p0sCYZd/em847Tl2dVxJJ1DR+hF+O77T7A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/server-destroy": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", @@ -12073,6 +12312,14 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -12562,6 +12809,11 @@ "node": ">=0.6" } }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==" + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -13057,6 +13309,14 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/value-or-promise": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", @@ -13561,6 +13821,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 91a63aa..bf98a2f 100644 --- a/package.json +++ b/package.json @@ -19,15 +19,20 @@ "@auth/core": "^0.32.0", "@eddeee888/gcg-typescript-resolver-files": "^0.10.4", "@graphql-codegen/cli": "^5.0.2", + "@graphql-tools/load-files": "^7.0.0", "@graphql-tools/resolvers-composition": "^7.0.1", "@graphql-tools/schema": "^10.0.4", "astro": "^4.13.1", "auth-astro": "^4.1.2", "graphql-scalars": "^1.23.0", + "lodash": "^4.17.21", + "mysql2": "^3.11.0", + "sequelize": "^6.37.3", "tailwindcss": "^3.4.8" }, "devDependencies": { "@parcel/watcher": "^2.4.1", + "@types/lodash": "^4.17.7", "@typescript-eslint/parser": "^6.21.0", "concurrently": "^8.2.2", "eslint": "^8.57.0", diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..6ee6bea --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,2 @@ +export const PLACEHOLDER = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMUlEQVQImWN4fGrVhZ0z/v+5zZAc5yfOwGCtrsbg4em/f7ZvZ7w2Q15Vi6e1iggPAwBwDg7L//0+xAAAAABJRU5ErkJggg==' diff --git a/src/graphql/apolloClient.mts b/src/graphql/apolloClient.mts index 08c98ba..a7bd788 100644 --- a/src/graphql/apolloClient.mts +++ b/src/graphql/apolloClient.mts @@ -1,19 +1,27 @@ -import { makeExecutableSchema } from "@graphql-tools/schema"; -import { ApolloClient, InMemoryCache } from "@apollo/client"; +import ApolloPackage from '@apollo/client' +const { ApolloClient, InMemoryCache } = ApolloPackage; import { SchemaLink } from "@apollo/client/link/schema" +import { makeExecutableSchema } from "@graphql-tools/schema"; +import { loadFilesSync } from '@graphql-tools/load-files' +import { mergeResolvers } from '@graphql-tools/merge' +import path from "node:path" import { typeDefs } from "./__generated__/typeDefs.generated"; -import { resolvers } from "./__generated__/resolvers.generated"; +// import { resolvers } from "./__generated__/resolvers.generated"; +import db from "@/sequelize"; +import resolverArray from '@/graphql/resolvers' + +export const resolvers = mergeResolvers(resolverArray) const schema = makeExecutableSchema({ typeDefs, resolvers }) -export type ResolverContext = { request?: Request; /*session?: Session */ } +export type ResolverContext = { request?: Request, db: any /*session?: Session */ } export async function getApolloClient(request?: Request) { // const session = request ? await getSession(request) : undefined return new ApolloClient({ ssrMode: true, - link: new SchemaLink({ schema, context: { request } }), + link: new SchemaLink({ schema, context: { request, db } }), cache: new InMemoryCache() }) } \ No newline at end of file diff --git a/src/graphql/resolvers/index.js b/src/graphql/resolvers/index.js new file mode 100644 index 0000000..7a27753 --- /dev/null +++ b/src/graphql/resolvers/index.js @@ -0,0 +1,7 @@ +// import mutations from './mutations' +import queries from './queries' +// import types from './types' + +const resolvers = { /*...mutations,*/ ...queries /*...types*/ } + +export default resolvers diff --git a/src/graphql/resolvers/mutations/comments.js b/src/graphql/resolvers/mutations/comments.js new file mode 100644 index 0000000..083272a --- /dev/null +++ b/src/graphql/resolvers/mutations/comments.js @@ -0,0 +1,54 @@ +import { composeResolvers } from '@graphql-tools/resolvers-composition' +// import axios from 'axios' + +import { isAuthedApp } from '@/server/utils/resolvers' +import { getSession, getUser } from '@/next/utils/getSession' + +// const token = process.env.IRONCLAD + +const resolversComposition = { + 'Mutation.*': [isAuthedApp] +} + +const resolvers = { + Mutation: { + updateComment: async (_, { text, anon, albumId }, { db }) => { + const { username } = await getSession() + const row = await db.models.comment.findOne({ + where: { albumId, username } + }) + + if (row) { + await row.update({ text, anon }) + await row.save() + } else await db.models.comment.create({ albumId, username, text, anon }) + + return true + }, + addFavorite: async (_, { albumId }, { db }) => { + const user = await getUser(db) + await user.addAlbum(albumId) + return true + }, + removeFavorite: async (_, { albumId }, { db }) => { + const user = await getUser(db) + await user.removeAlbum(albumId) + return true + }, + rateAlbum: async (_, { albumId, score }, { db }) => { + const { username } = await getSession() + const row = await db.models.rating.findOne({ + where: { albumId, username } + }) + + if (row) { + await row.update({ score }) + await row.save() + } else await db.models.rating.create({ albumId, username, score }) + + return true + } + } +} + +export default composeResolvers(resolvers, resolversComposition) diff --git a/src/graphql/resolvers/mutations/create.js b/src/graphql/resolvers/mutations/create.js new file mode 100644 index 0000000..604e789 --- /dev/null +++ b/src/graphql/resolvers/mutations/create.js @@ -0,0 +1,81 @@ +import { composeResolvers } from '@graphql-tools/resolvers-composition' + +import { createLog, createUpdateLog } from '@/server/utils/log' +import { getImgColor, img } from '@/server/utils/image' +import { hasRole } from '@/server/utils/resolvers' +import { handleComplete } from '@/server/utils/requests' +import { slugify } from '@/server/utils/slugify' +import { UserInputError } from '@/next/server/utils/graphQLErrors' + +const resolversComposition = { 'Mutation.*': hasRole('CREATE') } +const resolvers = { + Mutation: { + createAlbum: async (parent, data, { db }, info) => + db.transaction(async (transaction) => { + data.artists = data.artists + ? data.artists.map((artist) => { + return { name: artist, slug: slugify(artist) } + }) + : [] + await db.models.artist.bulkCreate(data.artists, { + ignoreDuplicates: true, + transaction + }) + + const album = await db.models.album.create(data, { + include: [ + db.models.disc, + db.models.store, + { + model: db.models.download, + include: [db.models.link] + } + ], + transaction + }) + + await Promise.all([ + album.setArtists( + data.artists + .filter(({ slug }) => slug.length > 0) + .map(({ slug }) => slug), + { transaction } + ), + album.setCategories(data.categories || [], { transaction }), + album.setClassifications(data.classifications || [], { transaction }), + album.setPlatforms(data.platforms || [], { transaction }), + album.setGames(data.games || [], { transaction }), + album.setAnimations(data.animations || [], { transaction }), + album.setRelated(data.related || [], { transaction }), + createLog(db, 'createAlbum', data, transaction) + ]) + + const { id } = album.dataValues + album.placeholder = data.cover + ? await img(data.cover, 'album', id) + : undefined + album.headerColor = data.cover + ? await getImgColor(`album/${id}`) + : undefined + + await album.save({ transaction }) + + if (album.status === 'show') handleComplete(db, data, album) + + return album + }), + + deleteAlbum: async (parent, { id }, { db }, info) => { + const album = await db.models.album.findByPk(id) + if (!album) throw UserInputError('Not Found') + + return db.transaction(async (transaction) => { + await createUpdateLog(db, 'deleteAlbum', album, transaction) + await album.destroy({ transaction }) + return 1 + }) + } + } +} + +export default composeResolvers(resolvers, resolversComposition) diff --git a/src/graphql/resolvers/mutations/index.js b/src/graphql/resolvers/mutations/index.js new file mode 100644 index 0000000..4e41fce --- /dev/null +++ b/src/graphql/resolvers/mutations/index.js @@ -0,0 +1,12 @@ +import merge from 'lodash/merge' + +import comments from './comments' +import create from './create' +import requests from './requests' +import site from './site' +import update from './update' +import user from './user' + +const mutations = merge(comments, create, requests, site, update, user) + +export default mutations diff --git a/src/graphql/resolvers/mutations/requests.js b/src/graphql/resolvers/mutations/requests.js new file mode 100644 index 0000000..0d42839 --- /dev/null +++ b/src/graphql/resolvers/mutations/requests.js @@ -0,0 +1,86 @@ +import { composeResolvers } from '@graphql-tools/resolvers-composition' +import { mergeResolvers } from '@graphql-tools/merge' + +import { hasRole, isAuthedApp } from '@/server/utils/resolvers' +import { getUser } from '@/next/utils/getSession' +import { requestPOST } from '@/server/utils/requests' +import { UserInputError } from '@/next/server/utils/graphQLErrors' + +const resolvers = { + Mutation: { + editRequest: async (parent, data, { db }, info) => { + const request = await db.models.request.findByPk(data.id) + if (!request) throw UserInputError('Request not found') + + await db.transaction(async (transaction) => { + await request.set(data, { transaction }) + + if (request.changed('state')) { + switch (request.state) { + case 'complete': + await requestPOST('complete', { requestId: request.id }) + break + + case 'hold': + await requestPOST('hold', { + requestId: request.id, + reason: data.reason + }) + break + } + } + + await request.save({ transaction }) + }) + + return request + }, + + rejectRequest: async (parent, data, { db }, info) => { + const request = await db.models.request.findByPk(data.id) + if (!request) throw UserInputError('Request not found') + + await requestPOST('reject', { + requestId: request.id, + reason: data.reason + }) + return true + } + } +} + +const submitActions = { + Mutation: { + submitAlbum: async (parent, data, { db }, info) => { + const { request: requestId, title, vgmdb, links } = data + let request + + if (requestId) { + request = await db.models.request.findByPk(requestId) + + if (!request) throw UserInputError('Request not found') + if (request.state === 'complete') + throw UserInputError('Request already complete') + } + + const user = await getUser(db) + + return db.models.submission.create({ + title, + vgmdb, + links, + requestId, + userUsername: user.username + }) + } + } +} + +const requestResolvers = composeResolvers(resolvers, { + 'Mutation.*': hasRole('REQUESTS') +}) +const submitResolvers = composeResolvers(submitActions, { + 'Mutation.*': [isAuthedApp] +}) + +export default mergeResolvers([requestResolvers, submitResolvers]) diff --git a/src/graphql/resolvers/mutations/site.js b/src/graphql/resolvers/mutations/site.js new file mode 100644 index 0000000..d4737c1 --- /dev/null +++ b/src/graphql/resolvers/mutations/site.js @@ -0,0 +1,37 @@ +import { composeResolvers } from '@graphql-tools/resolvers-composition' +import fs from 'fs-extra' +import path from 'path' + +import { img } from '@/server/utils/image' +import { hasRole } from '@/server/utils/resolvers' +import { UserInputError } from '@/next/server/utils/graphQLErrors' + +const resolversComposition = { 'Mutation.*': hasRole('UPDATE') } +const resolvers = { + Mutation: { + config: async (parent, data, { db, payload }, info) => + db.models.config + .upsert(data) + .then(() => db.models.config.findByPk(data.name)), + + uploadBanner: async (parent, { banner }, { db, payload }) => { + const timestamp = Date.now() + await img(banner, 'live', timestamp) + await db.models.config.upsert({ name: 'banner', value: timestamp }) + + return 1 + }, + + selectBanner: async (parent, { name }, { db }) => { + const filePath = path.join('/var/www/soc_img/img/live', `${name}.png`) + if (!(await fs.pathExists(filePath))) + throw UserInputError(`Banner '${name}' doesnt exist`) + + await db.models.config.upsert({ name: 'banner', value: name }) + + return 1 + } + } +} + +export default composeResolvers(resolvers, resolversComposition) diff --git a/src/graphql/resolvers/mutations/update.js b/src/graphql/resolvers/mutations/update.js new file mode 100644 index 0000000..c43d8fd --- /dev/null +++ b/src/graphql/resolvers/mutations/update.js @@ -0,0 +1,335 @@ +import { composeResolvers } from '@graphql-tools/resolvers-composition' + +import { createLog, createUpdateLog } from '@/server/utils/log' +import { img, getImgColor } from '@/server/utils/image' +import { hasRole } from '@/server/utils/resolvers' +import { handleComplete } from '@/server/utils/requests' +import { slugify } from '@/server/utils/slugify' + +const resolversComposition = { 'Mutation.*': hasRole('UPDATE') } +const resolvers = { + Mutation: { + createPublisher: async (parent, data, { db }, info) => + db.transaction(async (transaction) => { + const pub = await db.models.publisher.create(data, { transaction }) + data.id = pub.id + + await createLog(db, 'createPublisher', data, transaction) + + return pub + }), + updatePublisher: async (parent, { id, name }, { db }, info) => { + const pub = await db.models.publisher.findByPk(id) + pub.name = name + + return db.transaction(async (transaction) => { + await pub.save({ transaction }) + + await createUpdateLog(db, 'updatePublisher', pub, transaction) + return pub + }) + }, + deletePublisher: async (parent, { id }, { db }) => { + const pub = await db.models.publisher.findByPk(id) + + return db.transaction(async (transaction) => { + await pub.destroy({ transaction }) + + await createLog(db, 'deletePublisher', pub.dataValues, transaction) + }) + }, + + createPlatform: async (parent, data, { db }, info) => + db.transaction(async (transaction) => { + const plat = db.models.platform.create(data, { transaction }) + data.id = plat.id + + await createLog(db, 'createPlatform', data, transaction) + return plat + }), + updatePlatform: async (parent, { key, name, type }, { db }, info) => { + const plat = await db.models.platform.findByPk(key) + if (name) plat.name = name + if (type !== plat.type) plat.type = type + + return db.transaction(async (transaction) => { + await plat.save({ transaction }) + + await createUpdateLog(db, 'updatePlatform', plat, transaction) + return plat + }) + }, + deletePlatform: async (parent, { key }, { db }) => { + const plat = await db.models.platform.findByPk(key) + + return db.transaction(async (transaction) => { + await plat.destroy({ transaction }) + await createLog(db, 'deletePlatform', plat.dataValues, transaction) + }) + }, + + createStudio: async (parent, data, { db }, info) => + db.transaction(async (transaction) => { + const studio = db.models.studio.create(data, { transaction }) + data.slug = studio.slug + await createLog(db, 'createStudio', data, transaction) + return studio + }), + updateStudio: async (parent, { slug, name }, { db }, info) => { + const studio = await db.models.studio.findByPk(slug) + studio.name = name + + return db.transaction(async (transaction) => { + await studio.save({ transaction }) + await createUpdateLog(db, 'updateStudio', studio, transaction) + return studio + }) + }, + deleteStudio: async (parent, { slug, name }, { db }, info) => { + const studio = await db.models.studio.findByPk(slug) + + return db.transaction(async (transaction) => { + studio.destroy({ transaction }) + await createLog(db, 'deleteStudio', studio.dataValues, transaction) + }) + }, + + createSeries: async (parent, data, { db }, info) => + db.transaction(async (transaction) => { + const series = await db.models.series.create(data, { transaction }) + const { slug } = series.dataValues + + series.placeholder = data.cover + ? await img(data.cover, 'series', slug) + : undefined + series.headerColor = data.cover + ? await getImgColor(`series/${slug}`) + : undefined + await series.save({ transaction }) + + await createLog(db, 'createSeries', data, transaction) + return series + }), + updateSeries: async (parent, { slug, name, cover }, { db }, info) => { + const series = await db.models.series.findByPk(slug) + if (name) series.name = name + if (cover) { + series.placeholder = await img(cover, 'series', slug) + series.headerColor = await getImgColor(`series/${slug}`) + } + + return db.transaction(async (transaction) => { + await series.save({ transaction }) + await createUpdateLog(db, 'updateSeries', series, transaction) + return series + }) + }, + deleteSeries: async (parent, { slug }, { db }) => { + const series = await db.models.series.findByPk(slug) + + return db.transaction(async (transaction) => { + await series.destroy({ transaction }) + await createLog(db, 'deleteSeries', series.dataValues, transaction) + }) + }, + + createGame: async (parent, data, { db }, info) => { + const game = await db.models.game.create(data) + + return db.transaction(async (transaction) => { + await Promise.all([ + game.setSeries(data.series, { transaction }), + game.setPublishers(data.publishers, { transaction }), + game.setPlatforms(data.platforms, { transaction }) + ]) + + game.placeholder = data.cover + ? await img(data.cover, 'game', data.slug) + : '' + game.headerColor = data.cover + ? await getImgColor(`game/${data.slug}`) + : undefined + + await game.save({ transaction }) + await createLog(db, 'createGame', data, transaction) + + return game + }) + }, + updateGame: async (parent, args, { db }, info) => { + const { + slug, + name, + cover, + releaseDate, + series = [], + publishers, + platforms + } = args + const game = await db.models.game.findByPk(slug) + + game.name = name + game.releaseDate = releaseDate + + if (cover) { + game.placeholder = await img(cover, 'game', slug) + series.headerColor = await getImgColor(`game/${slug}`) + } + + // make more comprehensible log + return db.transaction(async (transaction) => { + game.setSeries(series, { transaction }) + game.setPublishers(publishers, { transaction }) + game.setPlatforms(platforms, { transaction }) + await game.save({ transaction }) + await createUpdateLog(db, 'updateGame', game, transaction) + + return game + }) + }, + deleteGame: async (parent, { slug }, { db }) => { + const game = await db.models.game.findByPk(slug) + const log = { + ...game.dataValues, + series: await game.getSeries(), + publishers: await game.getPublishers(), + platforms: await game.getPlatforms() + } + + return db.transaction(async (transaction) => { + await game.destroy({ transaction }) + await createLog(db, 'deleteSeries', log, transaction) + }) + }, + + createAnimation: async (parent, data, { db }, info) => { + return db.transaction(async (transaction) => { + const anim = await db.models.animation.create(data, { transaction }) + await anim.setStudios(data.studios, { transaction }) + + anim.placeholder = data.cover + ? await img(data.cover, 'anim', anim.id) + : '' + anim.headerColor = data.cover + ? await getImgColor(`anim/${anim.id}`) + : undefined + await anim.save({ transaction }) + + await createLog(db, 'createAnimation', data, transaction) + + return anim + }) + }, + updateAnimation: async (parent, data, { db }, info) => { + const anim = await db.models.animation.findByPk(data.id) + Object.entries(data).forEach(([key, value]) => { + anim[key] = value + }) + + if (data.cover) { + anim.placeholder = await img(data.cover, 'anim', anim.id) + anim.headerColor = await getImgColor(`anim/${anim.id}`) + } + + return db.transaction(async (transaction) => { + anim.setStudios(data.studios, { transaction }) + + await anim.save({ transaction }) + await createUpdateLog(db, 'updateAnimation', anim, transaction) + return anim + }) + }, + deleteAnimation: async (parent, { id }, { db }) => { + const anim = await db.models.animation.findByPk(id) + + const log = { + ...anim.dataValues, + studios: await anim.getStudios() + } + + return db.transaction(async (transaction) => { + await anim.destroy({ transaction }) + await createLog(db, 'deleteAnim', log, transaction) + }) + }, + + updateAlbum: async (parent, data, { db }, info) => { + try { + const album = await db.models.album.findByPk(data.id) + const triggerPost = + data.status !== album.status.repeat(1) && data.status === 'show' + data.artists = data.artists + ? data.artists.map((artist) => { + return { name: artist, slug: slugify(artist) } + }) + : [] + + await db.transaction(async (transaction) => { + await db.models.artist.bulkCreate(data.artists, { + ignoreDuplicates: true, + transaction + }) + + // implement better log lol lmao + + await Promise.all([ + album.update(data, { transaction }), + album.setArtists( + data.artists.map(({ slug }) => slug), + { transaction } + ), + album.setCategories(data.categories || [], { transaction }), + album.setClassifications(data.classifications || [], { + transaction + }), + album.setPlatforms(data.platforms || [], { transaction }), + album.setGames(data.games || []), + { transaction }, + album.setRelated(data.related || [], { transaction }), + album.setAnimations(data.animations || [], { transaction }), + db.models.disc + .destroy({ where: { albumId: album.dataValues.id }, transaction }) + .then(() => + (data.discs || []).map((disc) => + album.createDisc(disc, { transaction }) + ) + ), + db.models.store + .destroy({ where: { albumId: album.dataValues.id }, transaction }) + .then(() => + (data.stores || []).map((store) => + album.createStore(store, { transaction }) + ) + ), + db.models.download + .destroy({ where: { albumId: album.dataValues.id }, transaction }) + .then(() => + (data.downloads || []).map((download) => + album.createDownload(download, { + include: [db.models.link], + transaction + }) + ) + ), + createUpdateLog(db, 'updateAlbum', album, transaction) + ]) + + if (data.cover) { + album.placeholder = await img(data.cover, 'album', album.id) + album.headerColor = await getImgColor(`album/${album.id}`) + await album.save({ transaction }) + } + }) + + if (triggerPost) handleComplete(db, data, album) + + return album + } catch (err) { + console.log(err) + throw new Error(err.message) + } + } + } +} + +export default composeResolvers(resolvers, resolversComposition) diff --git a/src/graphql/resolvers/mutations/user.js b/src/graphql/resolvers/mutations/user.js new file mode 100644 index 0000000..d5a7817 --- /dev/null +++ b/src/graphql/resolvers/mutations/user.js @@ -0,0 +1,220 @@ +import bcrypt from 'bcrypt' +import generator from 'generate-password' +import { composeResolvers } from '@graphql-tools/resolvers-composition' +import { DateTime } from 'luxon' +import { Op } from 'sequelize' +import path from 'path' +import fs from 'fs-extra' +import sharp from 'sharp' + +import { createForgor } from '@/server/utils/forgor' +import { isAuthedApp } from '@/server/utils/resolvers' +import { processImage } from '@/server/utils/image' +import { getSession, getUser } from '@/next/utils/getSession' +import { + ForbiddenError, + UserInputError +} from '@/next/server/utils/graphQLErrors' + +const resolversComposition = { + 'Mutation.updateUser': [isAuthedApp] +} + +const streamToString = (stream) => { + const chunks = [] + return new Promise((resolve, reject) => { + stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))) + stream.on('error', (err) => reject(err)) + stream.on('end', () => resolve(Buffer.concat(chunks))) + }) +} + +async function cropPFP(streamItem, username, imgId) { + const { createReadStream } = await streamItem + const pathString = '/var/www/soc_img/img/user' + const fullPath = path.join(pathString, `${username}_${imgId}.png`) + + await fs.ensureDir(pathString) + + const image = await streamToString(createReadStream()) + let sharpImage = sharp(image) + const metadata = await sharpImage.metadata() + const { width, height } = metadata + + if (width !== height) { + sharpImage = sharpImage.extract( + width > height + ? { + left: Math.floor((width - height) / 2), + top: 0, + width: height, + height + } + : { + left: 0, + top: Math.floor((height - width) / 2), + width, + height: width + } + ) + } + + await sharpImage.resize({ width: 200, height: 200 }).png().toFile(fullPath) + + return await processImage(fullPath) +} + +const resolvers = { + Mutation: { + login: async (_, { username, password }, { db }) => { + const user = await db.models.user.findByPk(username) + if (!user) throw UserInputError() + + const valid = await bcrypt.compare(password, user.password) + if (!valid) throw UserInputError() + + const session = await getSession() + session.username = user.username + // Remove this when new site version is fully implemented + session.permissions = (await user.getRoles()) + .map((r) => r.permissions) + .flat() + await session.save() + + return 200 + }, + logout: async () => { + const session = await getSession() + await session.destroy() + + return 200 + }, + registerUser: async (_, { username, email, pfp }, { db }) => { + await Promise.all([ + db.models.user.findByPk(username).then((result) => { + if (result) throw UserInputError('Username already in use') + }), + db.models.user.findOne({ where: { email } }).then((result) => { + if (result) throw UserInputError('Email already in use') + }) + ]) + + const password = generator.generate({ + length: 30, + numbers: true, + upercase: true, + strict: true + }) + + return db.transaction(async (transaction) => { + const user = await db.models.user.create( + { username, email, password: await bcrypt.hash(password, 10) }, + { transaction } + ) + if (pfp) { + const imgId = Date.now() + user.placeholder = await cropPFP(pfp, username, imgId) + user.imgId = imgId + } else { + user.placeholder = + 'data:image/webp;base64,UklGRlQAAABXRUJQVlA4IEgAAACwAQCdASoEAAQAAUAmJZgCdAEO9p5AAPa//NFYLcn+a7b+3z7ynq/qXv+iG0yH/y1D9eBf9pqWugq9G0RnxmxwsjaA2bW8AAA=' + } + + await user.save({ transaction }) + await createForgor(user, db, transaction) + + return true + }) + }, + updateUserRoles: async ( + parent, + { username, roles }, + { db, payload }, + info + ) => { + const user = await db.models.user.findByPk(username) + user.setRoles(roles) + await user.save() + return true + }, + deleteUser: async (parent, { username }, { db, payload }, info) => { + const user = await db.models.user.findByPk(username) + if (!user) throw UserInputError('Not Found') + user.destroy() + return 1 + }, + + createForgorLink: async (_, { key }, { db }) => { + const user = await db.models.user.findOne({ + where: { [Op.or]: [{ username: key }, { email: key }] } + }) + if (!user) throw UserInputError('Not Found') + + await createForgor(user, db) + return true + }, + updatePass: async (_, { key, pass }, { db }) => { + const row = await db.models.forgor.findByPk(key) + if (!row) throw ForbiddenError() + + const now = DateTime.now() + const expires = DateTime.fromJSDate(row.expires) + + if (now > expires) throw ForbiddenError() + + const user = await db.models.user.findByPk(row.username) + user.password = await bcrypt.hash(pass, 10) + + return db.transaction(async (transaction) => { + await user.save({ transaction }) + await row.destroy({ transaction }) + return true + }) + }, + updateUser: async (_, { username, email, password, pfp }, { db }) => { + const user = await getUser(db) + if (username) user.username = username + if (email) user.email = email + if (password) user.password = await bcrypt.hash(password, 10) + if (pfp) { + const pathString = '/var/www/soc_img/img/user' + await fs.remove( + path.join(pathString, `${user.username}_${user.imgId}.png`) + ) + + const imgId = Date.now() + user.placeholder = await cropPFP(pfp, username, imgId) + user.imgId = imgId + } + + await user.save() + return true + }, + + createRole: async (parent, args, { db, payload }) => + db.models.role.create(args), + updateRole: async (parent, { key, name, permissions }, { db, payload }) => { + const role = await db.models.role.findByPk(key) + if (!role) throw UserInputError('Not Found') + + if (role.name !== name) { + await db.query( + `UPDATE roles SET name = "${name}" WHERE name = "${key}"` + ) + } + role.permissions = permissions + + await role.save() + return role + }, + deleteRole: async (parent, { name }, { db, payload }) => { + const role = await db.models.role.findByPk(name) + if (!role) throw UserInputError('Not Found') + await role.destroy() + + return name + } + } +} + +export default composeResolvers(resolvers, resolversComposition) diff --git a/src/graphql/resolvers/queries/album.ts b/src/graphql/resolvers/queries/album.ts new file mode 100644 index 0000000..d3d3340 --- /dev/null +++ b/src/graphql/resolvers/queries/album.ts @@ -0,0 +1,58 @@ +import { Op } from 'sequelize' + +const resolvers = { + Query: { + artists: (parent, args, { db }, info) => db.models.artist.findAll(), + platforms: (parent, args, { db }, info) => db.models.platform.findAll(), + publishers: (parent, args, { db }, info) => db.models.publisher.findAll(), + publisher: (parent, { id }, { db }, info) => + db.models.publisher.findByPk(id), + categories: (parent, args, { db }, info) => db.models.category.findAll(), + classifications: (parent, args, { db }, info) => + db.models.classification.findAll(), + series: (parent, args, { db }, info) => db.models.series.findAll(), + games: (parent, args, { db }, info) => db.models.game.findAll(), + game: (parent, { slug }, { db }, info) => db.models.game.findByPk(slug), + album: (_, { id }, { db }) => db.models.album.findByPk(id), + downloads: (parent, { id }, { db }) => + db.models.download.findAll({ where: { albumId: id } }), + albums: (_, __, { db }, info) => + db.models.album.findAll({ + }), + + platform: async (parent, { id }, { db }) => db.models.platform.findByPk(id), + animation: (parent, { id }, { db }) => db.models.animation.findByPk(id), + animations: (parent, args, { db }) => db.models.animation.findAll(), + studio: (parent, { slug }, { db }) => db.models.studio.findByPk(slug), + studios: (parent, { slug }, { db }) => db.models.studio.findAll(), + seriesOne: (parent, { slug }, { db }, info) => + db.models.series.findByPk(slug), + albumCount: async (parent, params, { db }) => db.models.album.count(), + recentSeries: (parent, { limit }, { db }) => + db.models.series.findAll({ + limit, + order: [['createdAt', 'DESC']] + }), + recentPublishers: (parent, { limit }, { db }) => + db.models.publisher.findAll({ + limit, + order: [['createdAt', 'DESC']] + }), + recentPlatforms: (parent, { limit, type }, { db }) => + db.models.platform.findAll({ + limit, + order: [['createdAt', 'DESC']], + where: { type: { [Op.like]: `%${type}%` } } + }), + + getRandomAlbum: async (parent, { limit = 1 }, { db }) => { + const result = await db.models.album.findAll({ + order: db.random(), + limit + }) + return result + } + } +} + +export default resolvers diff --git a/src/graphql/resolvers/queries/index.ts b/src/graphql/resolvers/queries/index.ts new file mode 100644 index 0000000..1ce60ed --- /dev/null +++ b/src/graphql/resolvers/queries/index.ts @@ -0,0 +1,15 @@ +import merge from 'lodash/merge' + +import search from './search' +/* +import album from './album' +import requests from './requests' + +import site from './site' +import user from './user' +import vgmdb from './vgmdb' +*/ + +const queries = merge(search/*album, requests, search, site, user, vgmdb*/) + +export default queries diff --git a/src/graphql/resolvers/queries/requests.js b/src/graphql/resolvers/queries/requests.js new file mode 100644 index 0000000..f124470 --- /dev/null +++ b/src/graphql/resolvers/queries/requests.js @@ -0,0 +1,76 @@ +import { Op, fn, col, where } from 'sequelize' + +const resolvers = { + Query: { + requests: (_, { + state = ['complete', 'hold', 'pending'], + donator = [true, false] + }, { db }) => db.models.request.findAll({ where: { state, donator } }), + request: (_, { link }, { db }) => db.models.request.findOne({ where: { link } }), + + searchRequests: async (_, { + state = ['complete', 'hold', 'pending'], + donator = [true, false], + limit = 10, + page = 0, + filter + }, { db }) => { + const options = { limit, offset: limit * page } + const optionsWhere = { state, donator } + + async function exactSearch () { + if (!filter) return + + const results = await db.models.request.findAndCountAll({ + where: { + ...optionsWhere, + [Op.or]: [ + { id: filter }, + { link: filter }, + { user: filter }, + { userID: filter } + ] + }, + ...options + }) + + if (results.rows.length > 0) return results + } + + function looseSearch () { + return db.models.request.findAndCountAll({ + where: [ + optionsWhere, + where(fn('LOWER', col('title')), { [Op.like]: `%${filter || ''}%` }) + ], + ...options + }) + } + + return await exactSearch() || looseSearch() + }, + + submissions: (_, args, context) => { + const { filter = '', state = ['pending'] } = args + const { db } = context + + return db.models.submission.findAll({ + where: { + [Op.and]: [ + { state: { [Op.in]: state } }, + { + [Op.or]: [ + { id: filter }, + { vgmdb: filter }, + { userUsername: filter }, + where(fn('LOWER', col('title')), { [Op.like]: `%${filter.toLowerCase()}%` }) + ] + } + ] + } + }) + } + } +} + +export default resolvers diff --git a/src/graphql/resolvers/queries/search.ts b/src/graphql/resolvers/queries/search.ts new file mode 100644 index 0000000..2eccb6e --- /dev/null +++ b/src/graphql/resolvers/queries/search.ts @@ -0,0 +1,82 @@ +import Sequelize from 'sequelize' +const { Op, literal } = Sequelize +import type { Resolvers } from '@/graphql/__generated__/types.generated' + +const fuzzySearch = (words: string[]) => `^${words.map(w => `(?=.*\b${w}\b)`)}.+/i` + +const resolvers: Resolvers = { + Query: { + searchAlbum: (parent, args, { db }) => { + const { title, categories, limit = 10, offset = 0, order = ['createdAt'], mode = 'DESC', status = ['show'] } = args + const titleWords = title?.split(' ') || [] + + return db.models.album.findAndCountAll({ + limit, offset, + where: { + [Op.or]: [ + { title: { [Op.regexp]: fuzzySearch(titleWords) } }, + { subTitle: { [Op.regexp]: fuzzySearch(titleWords) } } + ], + status: { [Op.in]: status } + }, + include: categories ? [{ model: db.models.category, where: { name: { [Op.in]: categories } } }] : [], + order: [literal('`album`.`status` = \'coming\' DESC'), ...order.map(o => [o, mode])] + }) + + }, + /* searchAlbumByArtist: async (parent, { name, categories, limit, page = 0, order = ['createdAt'], mode = 'DESC', status = ['show'] }, { db }) => { + const include = [{ model: db.models.artist, where: { name: { [Op.like]: `%${name}%` } } }] + + if (categories) include.push({ model: db.models.class, where: { name: { [Op.in]: categories } } }) + + return searchPage({ limit, page, model: 'album' }, { + where: { status: { [Op.in]: status } }, + include, + order: order.map(o => [o, mode]) + }, db) + }, + searchAnimation: (parent, { title = '', limit, page = 0, order = 'createdAt', mode = 'DESC' }, { db }) => searchPage({ title, limit, page, model: 'animation' }, { + where: { title: { [Op.like]: `%${title}%` } }, + order: [[order, mode]] + }, db), + searchStudio: (parent, { name = '', limit, page = 0, order = 'createdAt', mode = 'DESC' }, { db }) => searchPage({ name, limit, page, model: 'studio' }, { + where: { name: { [Op.like]: `%${name}%` } }, + order: [[order, mode]] + }, db), + searchGame: (parent, { name = '', limit, page = 0, order = 'createdAt', mode = 'DESC' }, { db }) => searchPage({ name, limit, page, model: 'game' }, { + where: { name: { [Op.like]: `%${name}%` } }, + order: [[order, mode]] + }, db), + searchSeries: (parent, { name = '', limit, page = 0, order = 'createdAt', mode = 'DESC' }, { db }) => searchPage({ name, limit, page, model: 'series' }, { + where: { name: { [Op.like]: `%${name}%` } }, + order: [[order, mode]] + }, db), + searchSeriesByName: (parent, { name }, { db }) => db.models.series.findAll({ + where: { + name: { + [Op.like]: `%${name}%` + } + } + }), + searchPublishersByName: (parent, { name }, { db }) => db.models.publisher.findAll({ + where: { + name: { + [Op.like]: `%${name}%` + } + } + }), + searchPlatformsByName: (parent, { name, categories }, { db }) => db.models.platform.findAll({ + where: { + name: { + [Op.like]: `%${name}%` + }, + type: { [Op.or]: categories } + } + }), + searchPlatformsByCategories: (parent, { categories }, { db }) => categories.length === 0 + ? [] + : db.models.platform.findAll({ where: { type: { [Op.or]: categories } } }) */ + } +} + +export default resolvers diff --git a/src/graphql/resolvers/queries/site.js b/src/graphql/resolvers/queries/site.js new file mode 100644 index 0000000..056e6ac --- /dev/null +++ b/src/graphql/resolvers/queries/site.js @@ -0,0 +1,36 @@ +import fg from 'fast-glob' +import { composeResolvers } from '@graphql-tools/resolvers-composition' + +import { hasRole } from '@/server/utils/resolvers' + +const resolversComposition = { 'Query.banners': hasRole('UPDATE') } +const resolvers = { + Query: { + config: (parent, { name }, { db }, info) => { + return db.models.config + .findOrCreate({ where: { name } }) + .then(() => db.models.config.findByPk(name)) + }, + + highlight: async (parent, args, { db }) => { + const { value } = await db.models.config.findByPk('highlight') + return db.models.album.findByPk(value) + }, + + banners: async (parent, args) => { + const filePaths = await fg(['/var/www/soc_img/img/live/**/*.png']) + const images = filePaths.map((f) => f.split('/').pop()) + + return images + }, + + recentComments: async (parent, { limit = 5 }, { db }) => { + return db.models.comment.findAll({ + limit, + order: [['updatedAt', 'DESC']] + }) + } + } +} + +export default composeResolvers(resolvers, resolversComposition) diff --git a/src/graphql/resolvers/queries/user.js b/src/graphql/resolvers/queries/user.js new file mode 100644 index 0000000..342cb3a --- /dev/null +++ b/src/graphql/resolvers/queries/user.js @@ -0,0 +1,33 @@ +import { Op } from 'sequelize' +import { composeResolvers } from '@graphql-tools/resolvers-composition' + +import info from '@/next/constants/info.json' +import { hasRole } from '@/server/utils/resolvers' +import { getUser } from '@/next/utils/getSession' + +const { permissions } = info + +const resolversComposition = { 'Query.users': hasRole('MANAGE_USER') } +const resolvers = { + Query: { + me: (parent, args, { db }) => getUser(db), + permissions: () => permissions, + roles: (parent, args, { db }) => db.models.role.findAll(), + users: (parent, args, { db }) => { + const search = args.search.trim() + if (search.length < 3) return [] + + return db.models.user.findAll({ + where: { + [Op.or]: [ + { username: { [Op.like]: `%${search}%` } }, + { email: search } + ] + } + }) + }, + user: async (_, { user }, { db }) => user + } +} + +export default composeResolvers(resolvers, resolversComposition) diff --git a/src/graphql/resolvers/queries/vgmdb.ts b/src/graphql/resolvers/queries/vgmdb.ts new file mode 100644 index 0000000..b63fa0c --- /dev/null +++ b/src/graphql/resolvers/queries/vgmdb.ts @@ -0,0 +1,9 @@ +import getPuppeteer from 'vgmdb-parser/lib/puppeteer' + +const resolvers = { + Query: { + vgmdb: (_, { url }) => getPuppeteer(url) + } +} + +export default resolvers diff --git a/src/graphql/resolvers/types/album.js b/src/graphql/resolvers/types/album.js new file mode 100644 index 0000000..9010e4d --- /dev/null +++ b/src/graphql/resolvers/types/album.js @@ -0,0 +1,154 @@ +import { GraphQLUpload } from 'graphql-upload-minimal' + +import { + checkHeaderColor, + solveRating, + checkPlaceholder +} from '@/server/utils/resolvers' + +import { getUser } from '@/next/utils/getSession' + +const resolvers = { + Upload: GraphQLUpload, + Album: { + artists: (parent, args, context, info) => parent.getArtists(), + categories: (parent, args, context, info) => parent.getCategories(), + classifications: (parent, args, context, info) => + parent.getClassifications(), + platforms: (parent, args, context, info) => + parent.getPlatforms({ order: ['name'] }), + games: (parent, args, context, info) => parent.getGames(), + discs: (parent, args, context, info) => + parent.getDiscs({ order: [['number', 'ASC']] }), + related: (parent, args, context, info) => parent.getRelated(), + stores: (parent) => parent.getStores(), + animations: (parent) => parent.getAnimations(), + downloads: (parent) => parent.getDownloads(), + comments: (parent) => parent.getComments(), + isFavorite: async (album, _, { db }) => { + const user = await getUser(db) + return user ? album.hasUser(user.username) : null + }, + selfComment: async (album, _, { db }) => { + const user = await getUser(db) + return user + ? db.models.comment.findOne({ + where: { albumId: album.id, username: user.username } + }) + : null + }, + selfScore: async (album, _, { db }) => { + const user = await getUser(db) + return user + ? ( + await db.models.rating.findOne({ + where: { albumId: album.id, username: user.username } + }) + )?.score + : null + }, + favorites: (album, _, { db }) => album.countUsers(), + placeholder: (album, _, { db }) => checkPlaceholder(album, 'album'), + headerColor: (album, _, { db }) => checkHeaderColor(album, 'album'), + avgRating: async (album, _, { db }) => solveRating(album) + }, + + Comment: { + username: (parent) => (parent.anon ? null : parent.username), + album: (comment, _, { db }) => comment.getAlbum() + }, + + Category: { + albums: (parent) => parent.getAlbums(), + count: (parent, args, { db }) => + db.models.album.count({ + include: [{ model: db.models.category, where: { name: parent.name } }] + }) + }, + + Download: { + links: async (download) => { + const links = await download.getLinks() + const filterLinks = links.filter( + (link) => !link.url.includes('adshrink.it') + ) + const outLinks = filterLinks.length === 0 ? links : filterLinks + + // return outLinks.filter((link) => link.provider !== 'TERABOX') + return outLinks + } + }, + + Link: { + url: async (link) => { + const download = await link.getDownload() + const links = await download.getLinks() + + return links.every((link) => link.url.includes('adshrink.it')) + ? link.directUrl + : link.url + }, + + directUrl: async (link, args, { db }) => { + const download = await link.getDownload() + const links = await download.getLinks() + + const fallback = links.every((link) => link.url.includes('adshrink.it')) + if (fallback) return + + const user = await getUser(db) + if (!user) return null + + const roles = await user.getRoles() + const perms = roles.map((r) => r.permissions).flat() + + const donator = perms.includes('DIRECT') + if (!donator) return null + + return link.directUrl + } + }, + + Game: { + albums: async (game, { order = [] }) => game.getAlbums({ order }), + series: (parent, args, context, info) => parent.getSeries(), + publishers: (parent, args, context, info) => parent.getPublishers(), + platforms: (parent, args, context, info) => + parent.getPlatforms({ order: ['name'] }), + placeholder: (game, _, { db }) => checkPlaceholder(game, 'game'), + headerColor: (game, _, { db }) => checkHeaderColor(game, 'game') + }, + + Platform: { + albums: (parent) => parent.getAlbums(), + games: (platform, args, { db }) => platform.getGames() + }, + + Animation: { + studios: (parent) => parent.getStudios(), + albums: (anim, { order = [] }) => anim.getAlbums({ order }), + placeholder: (anim, _, { db }) => checkPlaceholder(anim, 'anim'), + headerColor: (anim, _, { db }) => checkPlaceholder(anim, 'anim') + }, + + Studio: { + animations: (studio) => studio.getAnimations() + }, + + Series: { + games: (parent, args, context, info) => parent.getGames(), + placeholder: (series, _, { db }) => checkPlaceholder(series, 'series'), + headerColor: (series, _, { db }) => checkPlaceholder(series, 'series') + }, + + Publisher: { + games: (parent, args, context, info) => parent.getGames() + }, + + Disc: { + album: (parent) => parent.getAlbum(), + tracks: (parent) => parent.body.split(',') + } +} + +export default resolvers diff --git a/src/graphql/resolvers/types/index.js b/src/graphql/resolvers/types/index.js new file mode 100644 index 0000000..943346f --- /dev/null +++ b/src/graphql/resolvers/types/index.js @@ -0,0 +1,8 @@ +import merge from 'lodash/merge' + +import album from './album' +import user from './user' + +const types = merge(album, user) + +export default types diff --git a/src/graphql/resolvers/types/user.js b/src/graphql/resolvers/types/user.js new file mode 100644 index 0000000..06c7d98 --- /dev/null +++ b/src/graphql/resolvers/types/user.js @@ -0,0 +1,46 @@ +import { Op } from 'sequelize' + +import pages from '@/next/constants/pages.json' +import { getUser } from '@/next/utils/getSession' + +const userResolvable = { + roles: parent => parent.getRoles(), + permissions: async parent => { + const roles = await parent.getRoles() + return roles.map(r => r.permissions).flat() + }, + pages: async parent => { + const roles = await parent.getRoles() + const permissions = roles.map(r => r.permissions).flat() + + return pages.filter(({ perms }) => perms.length === 0 || perms.some(r => permissions.includes(r))) + }, + comments: (user, _, { db }) => user.getComments({ where: { albumId: { [Op.not]: null } } }), + favorites: user => user.getAlbums(), + imgUrl: async user => `https://cdn.sittingonclouds.net/user/${ + user.imgId ? `${user.username}_${user.imgId}` : 'default' + }.png` +} + +const funcs = { + User: userResolvable, + UserMe: userResolvable, + Role: { permissions: parent => typeof parent.permissions === 'string' || parent.permissions instanceof String ? JSON.parse(parent.permissions) : parent.permissions }, + Submission: { + submitter: submission => submission.getUser(), + links: async (submission, _, { db }) => { + const user = await getUser(db) + if (!user) return null + + const roles = await user.getRoles() + const perms = roles.map(r => r.permissions).flat() + + if (!perms.includes('REQUESTS')) return null + + return submission.links + }, + request: submission => submission.getRequest() + } +} + +export default funcs diff --git a/src/graphql/typeDefs/album.graphql b/src/graphql/typeDefs/album.graphql index cba63ba..f915428 100644 --- a/src/graphql/typeDefs/album.graphql +++ b/src/graphql/typeDefs/album.graphql @@ -145,8 +145,8 @@ type Query { searchAlbum( title: String categories: [String] + offset: Int limit: Int - page: Int order: [String] mode: String status: [String!] diff --git a/src/sequelize/index.js b/src/sequelize/index.js new file mode 100644 index 0000000..f942e49 --- /dev/null +++ b/src/sequelize/index.js @@ -0,0 +1,22 @@ +import mysql2 from 'mysql2' +import { Sequelize } from 'sequelize' + +import relations from './relations' +import models from './models' + +const options = process.env.GITHUB_ACTIONS + ? 'sqlite::memory:' + : JSON.parse(import.meta.env.SEQUELIZE) + +if (!process.env.GITHUB_ACTIONS && options.dialect === 'mysql') + options.dialectModule = mysql2 + +if (import.meta.env.DEV && options.logging === undefined) + options.logging = console.log + +const db = new Sequelize(options) + +Object.values(models).forEach((model) => model(db)) +relations(db) + +export default db diff --git a/src/sequelize/models/album.js b/src/sequelize/models/album.js new file mode 100644 index 0000000..e0c067a --- /dev/null +++ b/src/sequelize/models/album.js @@ -0,0 +1,22 @@ +import { DataTypes } from 'sequelize' +import { PLACEHOLDER } from '@/constants' + +const model = (sequelize) => + sequelize.define('album', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + title: DataTypes.STRING, + subTitle: DataTypes.TEXT, + releaseDate: DataTypes.DATEONLY, + label: DataTypes.STRING, + vgmdb: DataTypes.STRING, + description: DataTypes.STRING, + status: { type: DataTypes.STRING, defaultValue: 'show' }, + placeholder: { type: DataTypes.TEXT, defaultValue: PLACEHOLDER }, + headerColor: { type: DataTypes.STRING, defaultValue: '#ffffff' } + }) + +export default model diff --git a/src/sequelize/models/animation.js b/src/sequelize/models/animation.js new file mode 100644 index 0000000..e86b34a --- /dev/null +++ b/src/sequelize/models/animation.js @@ -0,0 +1,35 @@ +import { DataTypes } from 'sequelize' +import { PLACEHOLDER } from '@/constants' + +const animation = (sequelize) => { + sequelize.define( + 'animation', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + title: { type: DataTypes.STRING, unique: true }, + subTitle: { type: DataTypes.STRING }, + releaseDate: DataTypes.DATEONLY, + placeholder: { type: DataTypes.TEXT, defaultValue: PLACEHOLDER }, + headerColor: { type: DataTypes.STRING, defaultValue: '#ffffff' } + }, + { freezeTableName: true } + ) + + sequelize.define( + 'studio', + { + slug: { + type: DataTypes.STRING, + primaryKey: true + }, + name: DataTypes.STRING + }, + { freezeTableName: true } + ) +} + +export default animation diff --git a/src/sequelize/models/artist.js b/src/sequelize/models/artist.js new file mode 100644 index 0000000..72226dd --- /dev/null +++ b/src/sequelize/models/artist.js @@ -0,0 +1,20 @@ +import { DataTypes } from 'sequelize' + +const model = (sequelize) => { + const Artist = sequelize.define( + 'artist', + { + slug: { + type: DataTypes.STRING, + primaryKey: true + }, + name: DataTypes.STRING + }, + { + freezeTableName: true + } + ) + return Artist +} + +export default model diff --git a/src/sequelize/models/category.js b/src/sequelize/models/category.js new file mode 100644 index 0000000..5fdabe0 --- /dev/null +++ b/src/sequelize/models/category.js @@ -0,0 +1,19 @@ +import { DataTypes } from 'sequelize' +const model = (sequelize) => { + const Category = sequelize.define( + 'category', + { + name: { + type: DataTypes.STRING, + primaryKey: true + } + }, + { + freezeTableName: true + } + ) + + return Category +} + +export default model diff --git a/src/sequelize/models/classification.js b/src/sequelize/models/classification.js new file mode 100644 index 0000000..31ecb5b --- /dev/null +++ b/src/sequelize/models/classification.js @@ -0,0 +1,19 @@ +import { DataTypes } from 'sequelize' +const model = (sequelize) => { + const Classification = sequelize.define( + 'classification', + { + name: { + type: DataTypes.STRING, + primaryKey: true + } + }, + { + freezeTableName: true + } + ) + + return Classification +} + +export default model diff --git a/src/sequelize/models/comment.js b/src/sequelize/models/comment.js new file mode 100644 index 0000000..def2e3f --- /dev/null +++ b/src/sequelize/models/comment.js @@ -0,0 +1,12 @@ +import { DataTypes } from 'sequelize' + +const model = (sequelize) => { + sequelize.define('comment', { + text: DataTypes.STRING(300), + anon: DataTypes.BOOLEAN + }) + + sequelize.define('rating', { score: DataTypes.INTEGER }) +} + +export default model diff --git a/src/sequelize/models/config.js b/src/sequelize/models/config.js new file mode 100644 index 0000000..834aa3c --- /dev/null +++ b/src/sequelize/models/config.js @@ -0,0 +1,23 @@ +import { DataTypes } from 'sequelize' +const model = (sequelize) => { + const config = sequelize.define( + 'config', + { + name: { + type: DataTypes.STRING, + primaryKey: true + }, + value: { + type: DataTypes.STRING, + defaultValue: '' + } + }, + { + freezeTableName: true + } + ) + + return config +} + +export default model diff --git a/src/sequelize/models/disc.js b/src/sequelize/models/disc.js new file mode 100644 index 0000000..1d9ceea --- /dev/null +++ b/src/sequelize/models/disc.js @@ -0,0 +1,11 @@ +import { DataTypes } from 'sequelize' +const model = (sequelize) => { + const Disc = sequelize.define('disc', { + number: DataTypes.INTEGER, + body: DataTypes.TEXT + }) + + return Disc +} + +export default model diff --git a/src/sequelize/models/download.js b/src/sequelize/models/download.js new file mode 100644 index 0000000..72b7743 --- /dev/null +++ b/src/sequelize/models/download.js @@ -0,0 +1,11 @@ +import { DataTypes } from 'sequelize' +const model = (sequelize) => { + const Download = sequelize.define('download', { + title: DataTypes.STRING, + small: DataTypes.BOOLEAN + }) + + return Download +} + +export default model diff --git a/src/sequelize/models/game.js b/src/sequelize/models/game.js new file mode 100644 index 0000000..1530de0 --- /dev/null +++ b/src/sequelize/models/game.js @@ -0,0 +1,27 @@ +import { DataTypes } from 'sequelize' +import { PLACEHOLDER } from '@/constants' + +const model = (sequelize) => { + const Game = sequelize.define( + 'game', + { + slug: { + type: DataTypes.STRING, + primaryKey: true + }, + name: { + type: DataTypes.STRING + }, + releaseDate: DataTypes.DATEONLY, + placeholder: { type: DataTypes.TEXT, defaultValue: PLACEHOLDER }, + headerColor: { type: DataTypes.STRING, defaultValue: '#ffffff' } + }, + { + freezeTableName: true + } + ) + + return Game +} + +export default model diff --git a/src/sequelize/models/index.js b/src/sequelize/models/index.js new file mode 100644 index 0000000..4ffec39 --- /dev/null +++ b/src/sequelize/models/index.js @@ -0,0 +1,45 @@ +import album from './album' +import animation from './animation' +import artist from './artist' +import category from './category' +import classification from './classification' +import comment from './comment' +import config from './config' +import disc from './disc' +import download from './download' +import game from './game' +import link from './link' +import log from './log' +import platform from './platform' +import publisher from './publisher' +import request from './request' +import role from './role' +import series from './series' +import store from './store' +import submission from './submission' +import user from './user' + +const models = { + album, + animation, + artist, + category, + classification, + comment, + config, + disc, + download, + game, + link, + log, + platform, + publisher, + request, + role, + series, + store, + submission, + user +} + +export default models diff --git a/src/sequelize/models/link.js b/src/sequelize/models/link.js new file mode 100644 index 0000000..27604ec --- /dev/null +++ b/src/sequelize/models/link.js @@ -0,0 +1,13 @@ +import { DataTypes } from 'sequelize' +const model = (sequelize) => { + const Link = sequelize.define('link', { + url: DataTypes.STRING, + directUrl: DataTypes.STRING, + provider: DataTypes.STRING, + custom: DataTypes.STRING + }) + + return Link +} + +export default model diff --git a/src/sequelize/models/log.js b/src/sequelize/models/log.js new file mode 100644 index 0000000..5a60c5c --- /dev/null +++ b/src/sequelize/models/log.js @@ -0,0 +1,17 @@ +import { DataTypes } from 'sequelize' +const model = (sequelize) => { + return sequelize.define('log', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + action: DataTypes.STRING, + data: { + type: DataTypes.TEXT, + allowNull: true + } + }) +} + +export default model diff --git a/src/sequelize/models/platform.js b/src/sequelize/models/platform.js new file mode 100644 index 0000000..2a6297b --- /dev/null +++ b/src/sequelize/models/platform.js @@ -0,0 +1,27 @@ +import { DataTypes } from 'sequelize' +const model = (sequelize) => { + const Platform = sequelize.define( + 'platform', + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + name: { + type: DataTypes.STRING + }, + type: { + type: DataTypes.STRING, + defaultValue: 'Game' + } + }, + { + freezeTableName: true + } + ) + + return Platform +} + +export default model diff --git a/src/sequelize/models/publisher.js b/src/sequelize/models/publisher.js new file mode 100644 index 0000000..a0ccdf2 --- /dev/null +++ b/src/sequelize/models/publisher.js @@ -0,0 +1,23 @@ +import { DataTypes } from 'sequelize' +const model = (sequelize) => { + const Publisher = sequelize.define( + 'publisher', + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + name: { + type: DataTypes.STRING + } + }, + { + freezeTableName: true + } + ) + + return Publisher +} + +export default model diff --git a/src/sequelize/models/request.js b/src/sequelize/models/request.js new file mode 100644 index 0000000..e9c6d1b --- /dev/null +++ b/src/sequelize/models/request.js @@ -0,0 +1,21 @@ +import { DataTypes } from 'sequelize' + +const request = (sequelize) => + sequelize.define('request', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + title: DataTypes.STRING, + link: DataTypes.STRING, + user: DataTypes.STRING, + userID: DataTypes.STRING, + state: { type: DataTypes.STRING, allowNull: false }, + donator: { type: DataTypes.BOOLEAN, allowNull: false }, + reason: DataTypes.STRING, + comments: DataTypes.STRING, + message: DataTypes.STRING + }) + +export default request diff --git a/src/sequelize/models/role.js b/src/sequelize/models/role.js new file mode 100644 index 0000000..1e6b189 --- /dev/null +++ b/src/sequelize/models/role.js @@ -0,0 +1,14 @@ +import { DataTypes } from 'sequelize' +const model = (sequelize) => { + const Role = sequelize.define('role', { + name: { + type: DataTypes.STRING, + primaryKey: true + }, + permissions: DataTypes.JSON + }) + + return Role +} + +export default model diff --git a/src/sequelize/models/series.js b/src/sequelize/models/series.js new file mode 100644 index 0000000..da1f94c --- /dev/null +++ b/src/sequelize/models/series.js @@ -0,0 +1,24 @@ +import { DataTypes } from 'sequelize' +import { PLACEHOLDER } from '@/constants' + +const model = (sequelize) => { + const Series = sequelize.define( + 'series', + { + slug: { + type: DataTypes.STRING, + primaryKey: true + }, + name: { type: DataTypes.STRING }, + placeholder: { type: DataTypes.TEXT, defaultValue: PLACEHOLDER }, + headerColor: { type: DataTypes.STRING, defaultValue: '#ffffff' } + }, + { + freezeTableName: true + } + ) + + return Series +} + +export default model diff --git a/src/sequelize/models/store.js b/src/sequelize/models/store.js new file mode 100644 index 0000000..2d85e21 --- /dev/null +++ b/src/sequelize/models/store.js @@ -0,0 +1,10 @@ +import { DataTypes } from 'sequelize' +const model = (sequelize) => { + const Store = sequelize.define('store', { + url: DataTypes.STRING, + provider: DataTypes.STRING + }) + return Store +} + +export default model diff --git a/src/sequelize/models/submission.js b/src/sequelize/models/submission.js new file mode 100644 index 0000000..0d0de12 --- /dev/null +++ b/src/sequelize/models/submission.js @@ -0,0 +1,26 @@ +import { DataTypes } from 'sequelize' + +const model = (sequelize) => + sequelize.define('submission', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + state: { + type: DataTypes.STRING, + defaultValue: 'pending' + }, + title: DataTypes.STRING, + vgmdb: { + type: DataTypes.STRING, + allowNull: true + }, + links: DataTypes.TEXT, + score: { + type: DataTypes.INTEGER, + defaultValue: 0 + } + }) + +export default model diff --git a/src/sequelize/models/user.js b/src/sequelize/models/user.js new file mode 100644 index 0000000..69212f2 --- /dev/null +++ b/src/sequelize/models/user.js @@ -0,0 +1,23 @@ +import { DataTypes } from 'sequelize' + +const model = (sequelize) => { + const User = sequelize.define('user', { + username: { + type: DataTypes.STRING, + primaryKey: true + }, + email: DataTypes.STRING, + password: DataTypes.STRING, + placeholder: { type: DataTypes.TEXT }, + imgId: DataTypes.STRING + }) + + sequelize.define('forgor', { + key: { type: DataTypes.STRING, primaryKey: true }, + expires: DataTypes.DATE + }) + + return User +} + +export default model diff --git a/src/sequelize/relations.js b/src/sequelize/relations.js new file mode 100644 index 0000000..7d66ac5 --- /dev/null +++ b/src/sequelize/relations.js @@ -0,0 +1,68 @@ +export default function relations (sequelize) { + const { + album, classification, disc, download, link, + publisher, game, series, + platform, artist, category, store, + animation, studio, + user, role, forgor, log, comment, rating, + submission, request + } = sequelize.models + + user.belongsToMany(role, { through: 'User_Role' }) + log.belongsTo(user, { foreignKey: 'username' }) + forgor.belongsTo(user, { foreignKey: 'username' }) + + submission.belongsTo(user) + submission.belongsTo(request) + user.hasMany(submission) + request.hasMany(submission) + + classification.belongsToMany(album, { through: 'Album_Classification' }) + + disc.belongsTo(album) + + download.hasMany(link) + link.belongsTo(download) + + game.belongsToMany(publisher, { through: 'Publisher_Game' }) + game.belongsToMany(album, { through: 'Album_Game' }) + game.belongsToMany(series, { through: 'Series_Game' }) + + game.belongsToMany(platform, { through: 'Game_Platform' }) + platform.belongsToMany(game, { through: 'Game_Platform' }) + + album.belongsToMany(artist, { onDelete: 'SET NULL', through: 'Album_Artist' }) + album.belongsToMany(classification, { onDelete: 'SET NULL', through: 'Album_Classification' }) + album.belongsToMany(category, { onDelete: 'SET NULL', through: 'Album_Category' }) + album.belongsToMany(platform, { onDelete: 'SET NULL', through: 'Album_Platform' }) + album.belongsToMany(game, { onDelete: 'SET NULL', through: 'Album_Game' }) + album.belongsToMany(animation, { through: 'Album_Animation' }) + album.hasMany(disc, { onDelete: 'SET NULL' }) + album.hasMany(download, { onDelete: 'SET NULL' }) + album.hasMany(store, { onDelete: 'SET NULL' }) + album.belongsToMany(album, { onDelete: 'SET NULL', through: 'related_album', as: 'related' }) + + platform.belongsToMany(album, { through: 'Album_Platform' }) + + publisher.belongsToMany(game, { through: 'Publisher_Game' }) + + series.belongsToMany(game, { through: 'Series_Game' }) + + animation.belongsToMany(studio, { through: 'Studio_Animation' }) + studio.belongsToMany(animation, { through: 'Studio_Animation' }) + + animation.belongsToMany(album, { through: 'Album_Animation' }) + + album.hasMany(comment, { onDelete: 'SET NULL' }) + comment.belongsTo(album) + user.hasMany(comment, { foreignKey: 'username' }) + comment.belongsTo(user, { foreignKey: 'username' }) + + album.hasMany(rating) + rating.belongsTo(album) + user.hasMany(rating, { foreignKey: 'username' }) + rating.belongsTo(user, { foreignKey: 'username' }) + + user.belongsToMany(album, { through: 'favorites', foreignKey: 'username' }) + album.belongsToMany(user, { through: 'favorites' }) +}