diff --git a/.github/workflows/check-documentation.yml b/.github/workflows/check-documentation.yml index 27e761fa5..6b2cddeac 100644 --- a/.github/workflows/check-documentation.yml +++ b/.github/workflows/check-documentation.yml @@ -54,7 +54,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7 - name: Setup Node 20 - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.0.3 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.0.3 with: node-version: '20' diff --git a/.github/workflows/deploy-documentation.yml b/.github/workflows/deploy-documentation.yml index d1816bddd..13e0fb963 100644 --- a/.github/workflows/deploy-documentation.yml +++ b/.github/workflows/deploy-documentation.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7 - name: Setup Node 20 - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.0.3 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.0.3 with: node-version: 20 diff --git a/.gitignore b/.gitignore index d9d081e31..6f71f582b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ yarn-error.log* kubeconfig.yaml backup-cron-job.log .vscode +.nuxt node_modules/ cypress/videos diff --git a/backend/.eslintrc.cjs b/backend/.eslintrc.cjs index cff4c1de1..51adb6831 100644 --- a/backend/.eslintrc.cjs +++ b/backend/.eslintrc.cjs @@ -16,6 +16,7 @@ module.exports = { 'plugin:promise/recommended', 'plugin:security/recommended-legacy', 'plugin:@eslint-community/eslint-comments/recommended', + 'prettier', ], settings: { 'import/parsers': { @@ -178,9 +179,10 @@ module.exports = { { files: ['*.ts', '*.tsx'], extends: [ - // 'plugin:@typescript-eslint/recommended', - // 'plugin:@typescript-eslint/recommended-requiring-type-checking', - // 'plugin:@typescript-eslint/strict', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'plugin:@typescript-eslint/strict', + 'prettier', ], rules: { // allow explicitly defined dangling promises @@ -192,6 +194,11 @@ module.exports = { 'import/unambiguous': 'off', // this is not compatible with typeorm, due to joined tables can be null, but are not defined as nullable '@typescript-eslint/no-unnecessary-condition': 'off', + // respect underscore as acceptable unused variable + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], }, parserOptions: { tsconfigRootDir: __dirname, diff --git a/backend/Dockerfile b/backend/Dockerfile index 40b78225a..e1c244069 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -23,6 +23,7 @@ COPY . . ONBUILD COPY ./branding/constants/ src/config/tmp ONBUILD RUN tools/replace-constants.sh ONBUILD COPY ./branding/email/ src/middleware/helpers/email/ +ONBUILD COPY ./branding/data/ src/db/data ONBUILD RUN yarn install --production=false --frozen-lockfile --non-interactive ONBUILD RUN yarn run build ONBUILD RUN mkdir /build diff --git a/backend/README.md b/backend/README.md index bfc875d95..7d8bbfb15 100644 --- a/backend/README.md +++ b/backend/README.md @@ -120,6 +120,20 @@ When using `CATEGORIES_ACTIVE=true` you also want to seed the categories with: yarn db:data:categories ``` +### Branding Data + +You might need to seed some branding specific data into the database. + +To do so, run: + +```sh +# in backend with database running (In docker or local) +yarn db:data:branding + +# for docker +docker exec backend yarn db:data:branding +``` + ### Seed Data For a predefined set of test data you can seed the database with: diff --git a/backend/branding/data/.gitkeep b/backend/branding/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/package.json b/backend/package.json index 1422b0cc3..60ecba12e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,8 @@ "db:reset:withmigrations": "ts-node --require tsconfig-paths/register src/db/reset-with-migrations.ts", "db:seed": "ts-node --require tsconfig-paths/register src/db/seed.ts", "db:data:admin": "ts-node --require tsconfig-paths/register src/db/admin.ts", + "db:data:badges": "ts-node --require tsconfig-paths/register src/db/badges.ts", + "db:data:branding": "ts-node --require tsconfig-paths/register src/db/data-production.ts", "db:data:categories": "ts-node --require tsconfig-paths/register src/db/categories.ts", "db:migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --store ./src/db/migrate/store.ts", "db:migrate:create": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create", @@ -42,7 +44,7 @@ "babel-eslint": "~10.1.0", "babel-jest": "~29.7.0", "babel-plugin-transform-runtime": "^6.23.0", - "bcryptjs": "~2.4.3", + "bcryptjs": "~3.0.2", "body-parser": "^1.20.3", "cheerio": "~1.0.0", "cors": "~2.8.5", @@ -56,9 +58,9 @@ "graphql-shield": "~7.2.2", "graphql-subscriptions": "^1.1.0", "graphql-tag": "~2.10.3", - "graphql-upload": "^11.0.0", + "graphql-upload": "^13.0.0", "helmet": "~8.1.0", - "ioredis": "^4.16.1", + "ioredis": "^5.6.1", "jsonwebtoken": "~8.5.1", "languagedetect": "^2.0.0", "linkify-html": "^4.2.0", @@ -87,10 +89,10 @@ "neo4j-graphql-js": "^2.11.5", "neode": "^0.4.9", "node-fetch": "^2.7.0", - "nodemailer": "^6.10.0", + "nodemailer": "^6.10.1", "nodemailer-html-to-text": "^3.2.0", "request": "~2.88.2", - "sanitize-html": "~2.15.0", + "sanitize-html": "~2.16.0", "slug": "~9.1.0", "subscriptions-transport-ws": "^0.9.19", "trunc-html": "~1.1.2", @@ -99,8 +101,8 @@ "xregexp": "^5.1.2" }, "devDependencies": { - "@eslint-community/eslint-plugin-eslint-comments": "^4.4.1", - "@faker-js/faker": "9.6.0", + "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", + "@faker-js/faker": "9.7.0", "@types/jest": "^29.5.14", "@types/node": "^22.14.1", "@typescript-eslint/eslint-plugin": "^5.62.0", @@ -109,7 +111,7 @@ "eslint": "^8.57.1", "eslint-config-prettier": "^10.1.1", "eslint-config-standard": "^17.1.0", - "eslint-import-resolver-typescript": "^4.3.1", + "eslint-import-resolver-typescript": "^4.3.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.11.0", "eslint-plugin-n": "^17.17.0", @@ -122,9 +124,9 @@ "prettier": "^3.5.3", "require-json5": "^1.3.0", "rosie": "^2.1.1", - "ts-jest": "^29.3.1", + "ts-jest": "^29.3.2", "ts-node": "^10.9.2", - "tsc-alias": "^1.8.14", + "tsc-alias": "^1.8.15", "tsconfig-paths": "^4.2.0", "typescript": "^5.8.3" }, diff --git a/backend/public/img/badges/indiegogo_en_bear.svg b/backend/public/img/badges/trophy_blue_bear.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_bear.svg rename to backend/public/img/badges/trophy_blue_bear.svg diff --git a/backend/public/img/badges/indiegogo_en_panda.svg b/backend/public/img/badges/trophy_blue_panda.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_panda.svg rename to backend/public/img/badges/trophy_blue_panda.svg diff --git a/backend/public/img/badges/indiegogo_en_rabbit.svg b/backend/public/img/badges/trophy_blue_rabbit.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_rabbit.svg rename to backend/public/img/badges/trophy_blue_rabbit.svg diff --git a/backend/public/img/badges/indiegogo_en_racoon.svg b/backend/public/img/badges/trophy_blue_racoon.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_racoon.svg rename to backend/public/img/badges/trophy_blue_racoon.svg diff --git a/backend/public/img/badges/indiegogo_en_rhino.svg b/backend/public/img/badges/trophy_blue_rhino.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_rhino.svg rename to backend/public/img/badges/trophy_blue_rhino.svg diff --git a/backend/public/img/badges/indiegogo_en_tiger.svg b/backend/public/img/badges/trophy_blue_tiger.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_tiger.svg rename to backend/public/img/badges/trophy_blue_tiger.svg diff --git a/backend/public/img/badges/indiegogo_en_turtle.svg b/backend/public/img/badges/trophy_blue_turtle.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_turtle.svg rename to backend/public/img/badges/trophy_blue_turtle.svg diff --git a/backend/public/img/badges/indiegogo_en_whale.svg b/backend/public/img/badges/trophy_blue_whale.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_whale.svg rename to backend/public/img/badges/trophy_blue_whale.svg diff --git a/backend/public/img/badges/indiegogo_en_wolf.svg b/backend/public/img/badges/trophy_blue_wolf.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_wolf.svg rename to backend/public/img/badges/trophy_blue_wolf.svg diff --git a/backend/public/img/badges/fundraisingbox_de_airship.svg b/backend/public/img/badges/trophy_green_airship.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_airship.svg rename to backend/public/img/badges/trophy_green_airship.svg diff --git a/backend/public/img/badges/fundraisingbox_de_alienship.svg b/backend/public/img/badges/trophy_green_alienship.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_alienship.svg rename to backend/public/img/badges/trophy_green_alienship.svg diff --git a/backend/public/img/badges/fundraisingbox_de_balloon.svg b/backend/public/img/badges/trophy_green_balloon.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_balloon.svg rename to backend/public/img/badges/trophy_green_balloon.svg diff --git a/backend/public/img/badges/wooold_de_bee.svg b/backend/public/img/badges/trophy_green_bee.svg similarity index 100% rename from backend/public/img/badges/wooold_de_bee.svg rename to backend/public/img/badges/trophy_green_bee.svg diff --git a/backend/public/img/badges/fundraisingbox_de_bigballoon.svg b/backend/public/img/badges/trophy_green_bigballoon.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_bigballoon.svg rename to backend/public/img/badges/trophy_green_bigballoon.svg diff --git a/backend/public/img/badges/wooold_de_butterfly.svg b/backend/public/img/badges/trophy_green_butterfly.svg similarity index 100% rename from backend/public/img/badges/wooold_de_butterfly.svg rename to backend/public/img/badges/trophy_green_butterfly.svg diff --git a/backend/public/img/badges/fundraisingbox_de_crane.svg b/backend/public/img/badges/trophy_green_crane.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_crane.svg rename to backend/public/img/badges/trophy_green_crane.svg diff --git a/backend/public/img/badges/wooold_de_double_rainbow.svg b/backend/public/img/badges/trophy_green_doublerainbow.svg similarity index 100% rename from backend/public/img/badges/wooold_de_double_rainbow.svg rename to backend/public/img/badges/trophy_green_doublerainbow.svg diff --git a/backend/public/img/badges/wooold_de_end_of_rainbow.svg b/backend/public/img/badges/trophy_green_endrainbow.svg similarity index 100% rename from backend/public/img/badges/wooold_de_end_of_rainbow.svg rename to backend/public/img/badges/trophy_green_endrainbow.svg diff --git a/backend/public/img/badges/wooold_de_flower.svg b/backend/public/img/badges/trophy_green_flower.svg similarity index 100% rename from backend/public/img/badges/wooold_de_flower.svg rename to backend/public/img/badges/trophy_green_flower.svg diff --git a/backend/public/img/badges/fundraisingbox_de_glider.svg b/backend/public/img/badges/trophy_green_glider.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_glider.svg rename to backend/public/img/badges/trophy_green_glider.svg diff --git a/backend/public/img/badges/fundraisingbox_de_helicopter.svg b/backend/public/img/badges/trophy_green_helicopter.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_helicopter.svg rename to backend/public/img/badges/trophy_green_helicopter.svg diff --git a/backend/public/img/badges/wooold_de_lifetree.svg b/backend/public/img/badges/trophy_green_lifetree.svg similarity index 100% rename from backend/public/img/badges/wooold_de_lifetree.svg rename to backend/public/img/badges/trophy_green_lifetree.svg diff --git a/backend/public/img/badges/wooold_de_magic_rainbow.svg b/backend/public/img/badges/trophy_green_magicrainbow.svg similarity index 100% rename from backend/public/img/badges/wooold_de_magic_rainbow.svg rename to backend/public/img/badges/trophy_green_magicrainbow.svg diff --git a/backend/public/img/badges/fundraisingbox_de_starter.svg b/backend/public/img/badges/trophy_green_starter.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_starter.svg rename to backend/public/img/badges/trophy_green_starter.svg diff --git a/backend/public/img/badges/wooold_de_super_founder.svg b/backend/public/img/badges/trophy_green_superfounder.svg similarity index 100% rename from backend/public/img/badges/wooold_de_super_founder.svg rename to backend/public/img/badges/trophy_green_superfounder.svg diff --git a/backend/public/img/badges/user_role_admin.svg b/backend/public/img/badges/verification_red_admin.svg similarity index 100% rename from backend/public/img/badges/user_role_admin.svg rename to backend/public/img/badges/verification_red_admin.svg diff --git a/backend/public/img/badges/user_role_developer.svg b/backend/public/img/badges/verification_red_developer.svg similarity index 100% rename from backend/public/img/badges/user_role_developer.svg rename to backend/public/img/badges/verification_red_developer.svg diff --git a/backend/public/img/badges/user_role_moderator.svg b/backend/public/img/badges/verification_red_moderator.svg similarity index 100% rename from backend/public/img/badges/user_role_moderator.svg rename to backend/public/img/badges/verification_red_moderator.svg diff --git a/backend/scripts/build.copy.files.sh b/backend/scripts/build.copy.files.sh index da76a623c..7279291d6 100755 --- a/backend/scripts/build.copy.files.sh +++ b/backend/scripts/build.copy.files.sh @@ -14,14 +14,14 @@ mkdir -p build/src/middleware/helpers/email/templates/de/ cp -r src/middleware/helpers/email/templates/de/*.html build/src/middleware/helpers/email/templates/de/ # gql files -mkdir -p build/src/schema/types/ -cp -r src/schema/types/*.gql build/src/schema/types/ +mkdir -p build/src/graphql/types/ +cp -r src/graphql/types/*.gql build/src/graphql/types/ -mkdir -p build/src/schema/types/enum/ -cp -r src/schema/types/enum/*.gql build/src/schema/types/enum/ +mkdir -p build/src/graphql/types/enum/ +cp -r src/graphql/types/enum/*.gql build/src/graphql/types/enum/ -mkdir -p build/src/schema/types/scalar/ -cp -r src/schema/types/scalar/*.gql build/src/schema/types/scalar/ +mkdir -p build/src/graphql/types/scalar/ +cp -r src/graphql/types/scalar/*.gql build/src/graphql/types/scalar/ -mkdir -p build/src/schema/types/type/ -cp -r src/schema/types/type/*.gql build/src/schema/types/type/ \ No newline at end of file +mkdir -p build/src/graphql/types/type/ +cp -r src/graphql/types/type/*.gql build/src/graphql/types/type/ \ No newline at end of file diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 328bbbd61..cf07297df 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ /* eslint-disable n/no-process-env */ /* eslint-disable n/no-unpublished-require */ /* eslint-disable n/no-missing-require */ @@ -19,6 +27,7 @@ if (require.resolve) { } // Use Cypress env or process.env +// eslint-disable-next-line @typescript-eslint/no-explicit-any declare let Cypress: any | undefined const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env // eslint-disable-line no-undef diff --git a/backend/src/constants/badges.ts b/backend/src/constants/badges.ts new file mode 100644 index 000000000..bccebb39a --- /dev/null +++ b/backend/src/constants/badges.ts @@ -0,0 +1,2 @@ +// this file is duplicated in `backend/src/constants/badges` and `webapp/constants/badges.js` +export const TROPHY_BADGES_SELECTED_MAX = 9 diff --git a/backend/src/db/admin.ts b/backend/src/db/admin.ts index f1575214f..ae6bca49e 100644 --- a/backend/src/db/admin.ts +++ b/backend/src/db/admin.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ import { hashSync } from 'bcryptjs' import { v4 as uuid } from 'uuid' diff --git a/backend/src/db/badges.ts b/backend/src/db/badges.ts new file mode 100644 index 000000000..b4e879357 --- /dev/null +++ b/backend/src/db/badges.ts @@ -0,0 +1,17 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { getNeode } from './neo4j' +import { trophies, verification } from './seed/badges' + +// eslint-disable-next-line import/newline-after-import +;(async function () { + const neode = getNeode() + try { + await trophies() + await verification() + } finally { + await neode.close() + } +})() diff --git a/backend/src/db/categories.ts b/backend/src/db/categories.ts index f550c4d94..b3774e7b9 100644 --- a/backend/src/db/categories.ts +++ b/backend/src/db/categories.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { categories } from '@constants/categories' import { getDriver } from './neo4j' diff --git a/backend/src/db/compiler.ts b/backend/src/db/compiler.ts index 2d897762f..9c2140f2a 100644 --- a/backend/src/db/compiler.ts +++ b/backend/src/db/compiler.ts @@ -1,5 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable import/no-commonjs */ -// eslint-disable-next-line n/no-unpublished-require +// eslint-disable-next-line n/no-unpublished-require, @typescript-eslint/no-var-requires const tsNode = require('ts-node') // eslint-disable-next-line import/no-unassigned-import, import/no-extraneous-dependencies, n/no-unpublished-require require('tsconfig-paths/register') diff --git a/backend/src/db/data-branding.ts b/backend/src/db/data-branding.ts new file mode 100644 index 000000000..e9af41840 --- /dev/null +++ b/backend/src/db/data-branding.ts @@ -0,0 +1,22 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-misused-promises */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { readdir } from 'node:fs/promises' +import path from 'node:path' + +const dataFolder = path.join(__dirname, 'data/') + +;(async function () { + const files = await readdir(dataFolder) + files.forEach(async (file) => { + if (file.slice(0, -3).endsWith('-branding')) { + const importedModule = await import(path.join(dataFolder, file)) + if (!importedModule.default) { + throw new Error('Your data file must export a default function') + } + await importedModule.default() + } + }) +})() diff --git a/backend/src/db/data/.gitkeep b/backend/src/db/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/db/factories.ts b/backend/src/db/factories.ts index 136f0fe50..a0230a467 100644 --- a/backend/src/db/factories.ts +++ b/backend/src/db/factories.ts @@ -1,10 +1,17 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ import { faker } from '@faker-js/faker' import { hashSync } from 'bcryptjs' import { Factory } from 'rosie' import slugify from 'slug' import { v4 as uuid } from 'uuid' -import CONFIG from '@config/index' import generateInviteCode from '@schema/resolvers/helpers/generateInviteCode' import { getDriver, getNeode } from './neo4j' @@ -12,7 +19,7 @@ import { getDriver, getNeode } from './neo4j' const neode = getNeode() const uniqueImageUrl = (imageUrl) => { - const newUrl = new URL(imageUrl, CONFIG.CLIENT_URI) + const newUrl = new URL(imageUrl) newUrl.search = `random=${uuid()}` return newUrl.toString() } @@ -40,25 +47,34 @@ Factory.define('category') .attr('id', uuid) .attr('icon', 'globe') .attr('name', 'Global Peace & Nonviolence') - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Category', buildObject) }) Factory.define('badge') .attr('type', 'crowdfunding') .attr('status', 'permanent') - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Badge', buildObject) }) Factory.define('image') - .attr('url', faker.image.url) - .attr('aspectRatio', 1.3333333333333333) + .attr('width', 400) + .attr('height', 300) + .attr('blur', 0) .attr('alt', faker.lorem.sentence) .attr('type', 'image/jpeg') - .after((buildObject, options) => { - const { url: imageUrl } = buildObject - if (imageUrl) buildObject.url = uniqueImageUrl(imageUrl) + .attr('url', null) + .after((buildObject, _options) => { + if (!buildObject.url) { + buildObject.url = faker.image.urlPicsumPhotos({ + width: buildObject.width, + height: buildObject.height, + blur: buildObject.blur, + }) + } + buildObject.url = uniqueImageUrl(buildObject.url) + buildObject.aspectRatio = buildObject.width / buildObject.height return neode.create('Image', buildObject) }) @@ -85,21 +101,21 @@ Factory.define('basicUser') Factory.define('userWithoutEmailAddress') .extend('basicUser') .option('about', faker.lorem.paragraph) - .after(async (buildObject, options) => { + .after(async (buildObject, _options) => { return neode.create('User', buildObject) }) Factory.define('userWithAboutNull') .extend('basicUser') .option('about', null) - .after(async (buildObject, options) => { + .after(async (buildObject, _options) => { return neode.create('User', buildObject) }) Factory.define('userWithAboutEmpty') .extend('basicUser') .option('about', '') - .after(async (buildObject, options) => { + .after(async (buildObject, _options) => { return neode.create('User', buildObject) }) @@ -224,7 +240,7 @@ Factory.define('donations') .attr('showDonations', true) .attr('goal', 15000) .attr('progress', 7000) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Donations', buildObject) }) @@ -235,13 +251,13 @@ const emailDefaults = { Factory.define('emailAddress') .attrs(emailDefaults) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('EmailAddress', buildObject) }) Factory.define('unverifiedEmailAddress') .attr(emailDefaults) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('UnverifiedEmailAddress', buildObject) }) @@ -281,11 +297,11 @@ Factory.define('location') id: 'country.10743216036480410', type: 'country', }) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Location', buildObject) }) -Factory.define('report').after((buildObject, options) => { +Factory.define('report').after((buildObject, _options) => { return neode.create('Report', buildObject) }) @@ -293,7 +309,7 @@ Factory.define('tag') .attrs({ name: '#human-connection', }) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Tag', buildObject) }) @@ -301,7 +317,7 @@ Factory.define('socialMedia') .attrs({ url: 'https://mastodon.social/@Gargron', }) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('SocialMedia', buildObject) }) diff --git a/backend/src/db/migrate/store.ts b/backend/src/db/migrate/store.ts index aa8bd66d1..947364e4d 100644 --- a/backend/src/db/migrate/store.ts +++ b/backend/src/db/migrate/store.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver, getNeode } from '@db/neo4j' class Store { diff --git a/backend/src/db/migrate/template.ts b/backend/src/db/migrate/template.ts index f9eb1a338..ce538f260 100644 --- a/backend/src/db/migrate/template.ts +++ b/backend/src/db/migrate/template.ts @@ -1,8 +1,16 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = '' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -11,7 +19,6 @@ export async function up(next) { // Implement your migration here. await transaction.run(``) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -24,7 +31,7 @@ export async function up(next) { } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -33,7 +40,6 @@ export async function down(next) { // Implement your migration here. await transaction.run(``) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) diff --git a/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts b/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts index df4cec41e..d13eeecf9 100644 --- a/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts +++ b/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable promise/prefer-await-to-callbacks */ import { throwError, concat } from 'rxjs' @@ -21,12 +29,14 @@ export function up(next) { rxSession .beginTransaction() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any flatMap((txc: any) => concat( txc .run('MATCH (email:EmailAddress) RETURN email {.email}') .records() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any map((record: any) => { const { email } = record.get('email') const normalizedEmail = normalizeEmail(email) @@ -48,6 +58,7 @@ export function up(next) { ) .records() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any map((r: any) => ({ oldEmail: email, email: r.get('email'), @@ -61,7 +72,7 @@ export function up(next) { ), ) .subscribe({ - next: ({ user, email, oldUser, oldEmail }) => + next: ({ user, email, _oldUser, oldEmail }) => // eslint-disable-next-line no-console console.log(` Merged: diff --git a/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts b/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts index 89cef62fc..249464257 100644 --- a/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts +++ b/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable promise/prefer-await-to-callbacks */ import { throwError, concat } from 'rxjs' @@ -15,6 +23,7 @@ export function up(next) { rxSession .beginTransaction() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any flatMap((transaction: any) => concat( transaction @@ -26,6 +35,7 @@ export function up(next) { ) .records() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any map((record: any) => { const { id: locationId } = record.get('location') return { locationId } @@ -43,6 +53,7 @@ export function up(next) { ) .records() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any map((record: any) => ({ location: record.get('location'), updatedLocation: record.get('updatedLocation'), diff --git a/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts b/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts index 4743ff175..cfc00fcfe 100644 --- a/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts +++ b/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = ` @@ -8,7 +16,7 @@ export const description = ` A blocked user will still be able to see your contributions, but will not be able to interact with them and vice versa. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() diff --git a/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts b/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts index 84e15f9fb..a1eab53fa 100644 --- a/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts +++ b/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts @@ -1,3 +1,12 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = ` diff --git a/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts b/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts index 79b46a1ff..f7bcb0810 100644 --- a/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts +++ b/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = diff --git a/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts b/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts index 2a30d769e..a22a38127 100644 --- a/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts +++ b/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = ` diff --git a/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts b/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts index f0531b6c8..9d97aab7e 100644 --- a/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts +++ b/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable security/detect-non-literal-fs-filename */ import https from 'https' import { existsSync, createReadStream } from 'node:fs' diff --git a/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts b/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts index 355eb8476..61be45099 100644 --- a/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts +++ b/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ /* eslint-disable no-console */ import { getDriver } from '@db/neo4j' diff --git a/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts b/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts index 5ce75ab28..36b29f477 100644 --- a/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts +++ b/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = diff --git a/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts b/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts index a2b5ff159..1a3e97dba 100644 --- a/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts +++ b/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = diff --git a/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts b/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts index 0190ead48..f5fbd24de 100644 --- a/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts +++ b/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable security/detect-non-literal-fs-filename */ import { existsSync } from 'node:fs' diff --git a/backend/src/db/migrations/1613589876420-null_mutation.ts b/backend/src/db/migrations/1613589876420-null_mutation.ts index 8efe667be..daeba5dca 100644 --- a/backend/src/db/migrations/1613589876420-null_mutation.ts +++ b/backend/src/db/migrations/1613589876420-null_mutation.ts @@ -1,9 +1,6 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ 'use strict' -export async function up(next) { - next() -} +export async function up(_next) {} -export async function down(next) { - next() -} +export async function down(_next) {} diff --git a/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts b/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts index ce3515ac7..9bb7ab996 100644 --- a/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts +++ b/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts @@ -1,10 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { getDriver } from '@db/neo4j' export const description = ` This migration adds the clickedCount property to all posts, setting it to 0. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -15,7 +19,6 @@ export async function up(next) { SET p.clickedCount = 0 `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -28,7 +31,7 @@ export async function up(next) { } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -39,7 +42,6 @@ export async function down(next) { REMOVE p.clickedCount `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) diff --git a/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts b/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts index 5615aa4e0..9ebfee85c 100644 --- a/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts +++ b/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts @@ -1,10 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { getDriver } from '@db/neo4j' export const description = ` This migration adds the viewedTeaserCount property to all posts, setting it to 0. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -15,7 +19,6 @@ export async function up(next) { SET p.viewedTeaserCount = 0 `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -28,7 +31,7 @@ export async function up(next) { } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -39,7 +42,6 @@ export async function down(next) { REMOVE p.viewedTeaserCount `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) diff --git a/backend/src/db/migrations/20210506150512-add-donations-node.ts b/backend/src/db/migrations/20210506150512-add-donations-node.ts index 3d01f28bb..78919d46e 100644 --- a/backend/src/db/migrations/20210506150512-add-donations-node.ts +++ b/backend/src/db/migrations/20210506150512-add-donations-node.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { v4 as uuid } from 'uuid' import { getDriver } from '@db/neo4j' @@ -5,7 +13,7 @@ import { getDriver } from '@db/neo4j' export const description = 'This migration adds a Donations node with default settings to the database.' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -27,7 +35,6 @@ export async function up(next) { { donationId }, ) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -40,7 +47,7 @@ export async function up(next) { } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -53,7 +60,6 @@ export async function down(next) { RETURN donationInfo `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) diff --git a/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts b/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts index bd886db02..e4ed26095 100644 --- a/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts +++ b/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts @@ -1,8 +1,16 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = '' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -17,7 +25,6 @@ export async function up(next) { `, ) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -30,7 +37,7 @@ export async function up(next) { } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -45,7 +52,6 @@ export async function down(next) { `, ) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) diff --git a/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts b/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts index c53edb9a0..9fa0ffcd2 100644 --- a/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts +++ b/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = ` @@ -5,7 +13,7 @@ export const description = ` Additional we like to have fulltext indices the keys 'name', 'slug', 'about', and 'description'. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -26,7 +34,6 @@ export async function up(next) { `) */ await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -39,7 +46,7 @@ export async function up(next) { } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -59,7 +66,6 @@ export async function down(next) { `) await transaction.commit() */ - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) diff --git a/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts b/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts index 765042aad..bf3541e7c 100644 --- a/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts +++ b/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts @@ -1,8 +1,16 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = '' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -34,7 +42,6 @@ export async function up(next) { ) await transaction.commit() */ - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -47,7 +54,7 @@ export async function up(next) { } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -60,7 +67,6 @@ export async function down(next) { await transaction.run(`CALL db.index.fulltext.drop("tag_fulltext_search")`) await transaction.commit() */ - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) diff --git a/backend/src/db/migrations/20230329150329-article-label-for-posts.ts b/backend/src/db/migrations/20230329150329-article-label-for-posts.ts index f33aa818a..1c1259ca0 100644 --- a/backend/src/db/migrations/20230329150329-article-label-for-posts.ts +++ b/backend/src/db/migrations/20230329150329-article-label-for-posts.ts @@ -1,8 +1,16 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = 'Add to all existing posts the Article label' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -14,7 +22,6 @@ export async function up(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -27,7 +34,7 @@ export async function up(next) { } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -39,7 +46,6 @@ export async function down(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) diff --git a/backend/src/db/migrations/20230608130637-add-postType-property.ts b/backend/src/db/migrations/20230608130637-add-postType-property.ts index 26c99ce48..e4f1033b5 100644 --- a/backend/src/db/migrations/20230608130637-add-postType-property.ts +++ b/backend/src/db/migrations/20230608130637-add-postType-property.ts @@ -1,8 +1,16 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = 'Add postType property Article to all posts' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -14,7 +22,6 @@ export async function up(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -27,7 +34,7 @@ export async function up(next) { } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -39,7 +46,6 @@ export async function down(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) diff --git a/backend/src/db/migrations/20231017141022-fix-event-dates.ts b/backend/src/db/migrations/20231017141022-fix-event-dates.ts index b2edf17dc..b3d7d14bd 100644 --- a/backend/src/db/migrations/20231017141022-fix-event-dates.ts +++ b/backend/src/db/migrations/20231017141022-fix-event-dates.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = ` @@ -5,7 +13,7 @@ Transform event start and end date of format 'YYYY-MM-DD HH:MM:SS' in CEST to ISOString in UTC. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -34,7 +42,6 @@ export async function up(next) { `) } await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -47,14 +54,13 @@ export async function up(next) { } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() try { // No sense in running this down - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) diff --git a/backend/src/db/migrations/20250331130323-author-observes-own-post.ts b/backend/src/db/migrations/20250331130323-author-observes-own-post.ts index 619b5f1fa..e088e12c1 100644 --- a/backend/src/db/migrations/20250331130323-author-observes-own-post.ts +++ b/backend/src/db/migrations/20250331130323-author-observes-own-post.ts @@ -1,10 +1,18 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = ` All authors observe their posts. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -21,7 +29,6 @@ export async function up(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -34,7 +41,7 @@ export async function up(next) { } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -47,7 +54,6 @@ export async function down(next) { RETURN p `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) diff --git a/backend/src/db/migrations/20250331140313-commenter-observes-post.ts b/backend/src/db/migrations/20250331140313-commenter-observes-post.ts index cc9a82160..f3f358f20 100644 --- a/backend/src/db/migrations/20250331140313-commenter-observes-post.ts +++ b/backend/src/db/migrations/20250331140313-commenter-observes-post.ts @@ -1,10 +1,18 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = ` All users commenting a post observe the post. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -21,7 +29,6 @@ export async function up(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -34,7 +41,7 @@ export async function up(next) { } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -48,7 +55,6 @@ export async function down(next) { RETURN p `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) diff --git a/backend/src/db/migrations/20250405030454-email-notification-settings.ts b/backend/src/db/migrations/20250405030454-email-notification-settings.ts index 8b02e866a..f17c88fa9 100644 --- a/backend/src/db/migrations/20250405030454-email-notification-settings.ts +++ b/backend/src/db/migrations/20250405030454-email-notification-settings.ts @@ -1,9 +1,17 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = 'Transforms the `sendNotificationEmails` property on User to a multi value system' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -22,7 +30,6 @@ export async function up(next) { REMOVE user.sendNotificationEmails `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -35,7 +42,7 @@ export async function up(next) { } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -54,7 +61,6 @@ export async function down(next) { REMOVE user.emailNotificationsGroupMemberRoleChanged `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) diff --git a/backend/src/db/migrations/20250414220436-delete-old-badges.ts b/backend/src/db/migrations/20250414220436-delete-old-badges.ts new file mode 100644 index 000000000..9c1d2b8e1 --- /dev/null +++ b/backend/src/db/migrations/20250414220436-delete-old-badges.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable security/detect-non-literal-fs-filename */ +import { getDriver } from '@db/neo4j' + +export const description = '' + +export async function up(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + await transaction.run(` + MATCH (badge:Badge) + DETACH DELETE badge + `) + await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + session.close() + } +} + +export async function down(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // cannot be rolled back + // Implement your migration here. + // await transaction.run(``) + // await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + session.close() + } +} diff --git a/backend/src/db/neo4j.ts b/backend/src/db/neo4j.ts index c94c552f0..b7c0eec56 100644 --- a/backend/src/db/neo4j.ts +++ b/backend/src/db/neo4j.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable import/no-named-as-default-member */ import neo4j from 'neo4j-driver' import Neode from 'neode' diff --git a/backend/src/db/reset-with-migrations.ts b/backend/src/db/reset-with-migrations.ts index fc3d86b09..78db831ce 100644 --- a/backend/src/db/reset-with-migrations.ts +++ b/backend/src/db/reset-with-migrations.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable n/no-process-exit */ import CONFIG from '@config/index' diff --git a/backend/src/db/reset.ts b/backend/src/db/reset.ts index 0f316faf8..a381799c6 100644 --- a/backend/src/db/reset.ts +++ b/backend/src/db/reset.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable n/no-process-exit */ import CONFIG from '@config/index' diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index 34a6ebb03..08594d1b4 100644 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable n/no-process-exit */ import { faker } from '@faker-js/faker' import { createTestClient } from 'apollo-server-testing' @@ -5,19 +9,18 @@ import sample from 'lodash/sample' import CONFIG from '@config/index' import { categories } from '@constants/categories' -import { createCommentMutation } from '@graphql/comments' -import { - createGroupMutation, - joinGroupMutation, - changeGroupMemberRoleMutation, -} from '@graphql/groups' -import { createMessageMutation } from '@graphql/messages' -import { createPostMutation } from '@graphql/posts' -import { createRoomMutation } from '@graphql/rooms' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createCommentMutation } from '@graphql/queries/createCommentMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { createMessageMutation } from '@graphql/queries/createMessageMutation' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import { createRoomMutation } from '@graphql/queries/createRoomMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' import createServer from '@src/server' import Factory from './factories' import { getNeode, getDriver } from './neo4j' +import { trophies, verification } from './seed/badges' if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) { throw new Error(`You cannot seed the database in a non-staging and real production environment!`) @@ -124,32 +127,28 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] await Hamburg.relateTo(Germany, 'isIn') await Paris.relateTo(France, 'isIn') - // badges - const racoon = await Factory.build('badge', { - id: 'indiegogo_en_racoon', - icon: '/img/badges/indiegogo_en_racoon.svg', - }) - const rabbit = await Factory.build('badge', { - id: 'indiegogo_en_rabbit', - icon: '/img/badges/indiegogo_en_rabbit.svg', - }) - const wolf = await Factory.build('badge', { - id: 'indiegogo_en_wolf', - icon: '/img/badges/indiegogo_en_wolf.svg', - }) - const bear = await Factory.build('badge', { - id: 'indiegogo_en_bear', - icon: '/img/badges/indiegogo_en_bear.svg', - }) - const turtle = await Factory.build('badge', { - id: 'indiegogo_en_turtle', - icon: '/img/badges/indiegogo_en_turtle.svg', - }) - const rhino = await Factory.build('badge', { - id: 'indiegogo_en_rhino', - icon: '/img/badges/indiegogo_en_rhino.svg', - }) + const { + trophyAirship, + trophyBee, + trophyStarter, + trophyFlower, + trophyPanda, + trophyTiger, + trophyAlienship, + trophyBalloon, + trophyMagicrainbow, + trophySuperfounder, + trophyBigballoon, + trophyLifetree, + trophyRacoon, + trophyRhino, + trophyWolf, + trophyTurtle, + trophyBear, + trophyRabbit, + } = await trophies() + const { verificationAdmin, verificationModerator, verificationDeveloper } = await verification() // users const peterLustig = await Factory.build( 'user', @@ -243,14 +242,50 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] await jennyRostock.relateTo(Paris, 'isIn') await huey.relateTo(Paris, 'isIn') - await peterLustig.relateTo(racoon, 'rewarded') - await peterLustig.relateTo(rhino, 'rewarded') - await peterLustig.relateTo(wolf, 'rewarded') - await bobDerBaumeister.relateTo(racoon, 'rewarded') - await bobDerBaumeister.relateTo(turtle, 'rewarded') - await jennyRostock.relateTo(bear, 'rewarded') - await dagobert.relateTo(rabbit, 'rewarded') + // badges + await peterLustig.relateTo(trophyRacoon, 'rewarded') + await peterLustig.relateTo(trophyRhino, 'rewarded') + await peterLustig.relateTo(trophyWolf, 'rewarded') + await peterLustig.relateTo(trophyAirship, 'rewarded') + await peterLustig.relateTo(verificationAdmin, 'verifies') + await peterLustig.relateTo(trophyRacoon, 'selected', { slot: 0 }) + await peterLustig.relateTo(trophyRhino, 'selected', { slot: 1 }) + await peterLustig.relateTo(trophyAirship, 'selected', { slot: 5 }) + await bobDerBaumeister.relateTo(trophyRacoon, 'rewarded') + await bobDerBaumeister.relateTo(trophyTurtle, 'rewarded') + await bobDerBaumeister.relateTo(trophyBee, 'rewarded') + await bobDerBaumeister.relateTo(verificationModerator, 'verifies') + await bobDerBaumeister.relateTo(trophyRacoon, 'selected', { slot: 1 }) + await bobDerBaumeister.relateTo(trophyTurtle, 'selected', { slot: 2 }) + + await jennyRostock.relateTo(trophyBear, 'rewarded') + await jennyRostock.relateTo(trophyStarter, 'rewarded') + await jennyRostock.relateTo(trophyFlower, 'rewarded') + await jennyRostock.relateTo(trophyBear, 'selected', { slot: 0 }) + await jennyRostock.relateTo(trophyStarter, 'selected', { slot: 1 }) + await jennyRostock.relateTo(trophyFlower, 'selected', { slot: 2 }) + + await huey.relateTo(trophyPanda, 'rewarded') + await huey.relateTo(trophyTiger, 'rewarded') + await huey.relateTo(trophyAlienship, 'rewarded') + await huey.relateTo(trophyBalloon, 'rewarded') + await huey.relateTo(trophyMagicrainbow, 'rewarded') + await huey.relateTo(trophySuperfounder, 'rewarded') + await huey.relateTo(verificationDeveloper, 'verifies') + await huey.relateTo(trophyPanda, 'selected', { slot: 0 }) + await huey.relateTo(trophyTiger, 'selected', { slot: 1 }) + await huey.relateTo(trophyAlienship, 'selected', { slot: 2 }) + + await dewey.relateTo(trophyBigballoon, 'rewarded') + await dewey.relateTo(trophyLifetree, 'rewarded') + await dewey.relateTo(trophyBigballoon, 'selected', { slot: 7 }) + await dewey.relateTo(trophyLifetree, 'selected', { slot: 8 }) + + await louie.relateTo(trophyRabbit, 'rewarded') + await louie.relateTo(trophyRabbit, 'selected', { slot: 4 }) + + // Friends await peterLustig.relateTo(bobDerBaumeister, 'friends') await peterLustig.relateTo(jennyRostock, 'friends') await bobDerBaumeister.relateTo(jennyRostock, 'friends') @@ -635,9 +670,9 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat16'], author: peterLustig, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'food', width: 300, height: 169 }), + width: 300, + height: 169, sensitive: true, - aspectRatio: 300 / 169, }), }, ) @@ -651,8 +686,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat1'], author: bobDerBaumeister, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'technics', width: 300, height: 1500 }), - aspectRatio: 300 / 1500, + width: 300, + height: 1500, }), }, ) @@ -699,8 +734,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat6'], author: peterLustig, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'city', width: 300, height: 857 }), - aspectRatio: 300 / 857, + width: 300, + height: 857, }), }, ) @@ -738,8 +773,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat11'], author: louie, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'people', width: 300, height: 901 }), - aspectRatio: 300 / 901, + width: 300, + height: 901, }), }, ) @@ -764,8 +799,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat14'], author: jennyRostock, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'abstract', width: 300, height: 200 }), - aspectRatio: 300 / 450, + width: 300, + height: 200, }), }, ) @@ -824,7 +859,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] mutation: createPostMutation(), variables: { id: 'p8', - image: faker.image.urlLoremFlickr({ category: 'nature' }), title: `Quantum Flow Theory explains Quantum Gravity`, content: hashtagAndMention1, categoryIds: ['cat8'], @@ -878,6 +912,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] authenticatedUser = null + // eslint-disable-next-line @typescript-eslint/no-explicit-any const comments: any[] = [] comments.push( await Factory.build( @@ -1052,6 +1087,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] await huey.relateTo(p9, 'shouted') await louie.relateTo(p10, 'shouted') + // eslint-disable-next-line @typescript-eslint/no-explicit-any const reports: any[] = [] reports.push( await Factory.build('report'), @@ -1159,6 +1195,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] closed: true, }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any const additionalUsers: any[] = [] for (let i = 0; i < 30; i++) { const user = await Factory.build('user') @@ -1180,9 +1217,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: jennyRostock, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'abstract' }), - }), }, ) } @@ -1231,9 +1265,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: peterLustig, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'city' }), - }), }, ) } @@ -1282,9 +1313,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: dewey, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'food' }), - }), }, ) } @@ -1333,9 +1361,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: louie, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'technics' }), - }), }, ) } @@ -1384,9 +1409,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: bobDerBaumeister, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'people' }), - }), }, ) } @@ -1435,9 +1457,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: huey, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'nature' }), - }), }, ) } diff --git a/backend/src/db/seed/badges.ts b/backend/src/db/seed/badges.ts new file mode 100644 index 000000000..ce8bff9ba --- /dev/null +++ b/backend/src/db/seed/badges.ts @@ -0,0 +1,188 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import Factory from '@db/factories' + +export const trophies = async () => { + return { + // Blue Animals + trophyBear: await Factory.build('badge', { + id: 'trophy_bear', + type: 'trophy', + description: 'You earned a Bear', + icon: '/img/badges/trophy_blue_bear.svg', + }), + trophyPanda: await Factory.build('badge', { + id: 'trophy_panda', + type: 'trophy', + description: 'You earned a Panda', + icon: '/img/badges/trophy_blue_panda.svg', + }), + trophyRabbit: await Factory.build('badge', { + id: 'trophy_rabbit', + type: 'trophy', + description: 'You earned a Rabbit', + icon: '/img/badges/trophy_blue_rabbit.svg', + }), + trophyRacoon: await Factory.build('badge', { + id: 'trophy_racoon', + type: 'trophy', + description: 'You earned a Racoon', + icon: '/img/badges/trophy_blue_racoon.svg', + }), + trophyRhino: await Factory.build('badge', { + id: 'trophy_rhino', + type: 'trophy', + description: 'You earned a Rhino', + icon: '/img/badges/trophy_blue_rhino.svg', + }), + trophyTiger: await Factory.build('badge', { + id: 'trophy_tiger', + type: 'trophy', + description: 'You earned a Tiger', + icon: '/img/badges/trophy_blue_tiger.svg', + }), + trophyTurtle: await Factory.build('badge', { + id: 'trophy_turtle', + type: 'trophy', + description: 'You earned a Turtle', + icon: '/img/badges/trophy_blue_turtle.svg', + }), + trophyWhale: await Factory.build('badge', { + id: 'trophy_whale', + type: 'trophy', + description: 'You earned a Whale', + icon: '/img/badges/trophy_blue_whale.svg', + }), + trophyWolf: await Factory.build('badge', { + id: 'trophy_wolf', + type: 'trophy', + description: 'You earned a Wolf', + icon: '/img/badges/trophy_blue_wolf.svg', + }), + // Green Transports + trophyAirship: await Factory.build('badge', { + id: 'trophy_airship', + type: 'trophy', + description: 'You earned an Airship', + icon: '/img/badges/trophy_green_airship.svg', + }), + trophyAlienship: await Factory.build('badge', { + id: 'trophy_alienship', + type: 'trophy', + description: 'You earned an Alienship', + icon: '/img/badges/trophy_green_alienship.svg', + }), + trophyBalloon: await Factory.build('badge', { + id: 'trophy_balloon', + type: 'trophy', + description: 'You earned a Balloon', + icon: '/img/badges/trophy_green_balloon.svg', + }), + trophyBigballoon: await Factory.build('badge', { + id: 'trophy_bigballoon', + type: 'trophy', + description: 'You earned a Big Balloon', + icon: '/img/badges/trophy_green_bigballoon.svg', + }), + trophyCrane: await Factory.build('badge', { + id: 'trophy_crane', + type: 'trophy', + description: 'You earned a Crane', + icon: '/img/badges/trophy_green_crane.svg', + }), + trophyGlider: await Factory.build('badge', { + id: 'trophy_glider', + type: 'trophy', + description: 'You earned a Glider', + icon: '/img/badges/trophy_green_glider.svg', + }), + trophyHelicopter: await Factory.build('badge', { + id: 'trophy_helicopter', + type: 'trophy', + description: 'You earned a Helicopter', + icon: '/img/badges/trophy_green_helicopter.svg', + }), + // Green Animals + trophyBee: await Factory.build('badge', { + id: 'trophy_bee', + type: 'trophy', + description: 'You earned a Bee', + icon: '/img/badges/trophy_green_bee.svg', + }), + trophyButterfly: await Factory.build('badge', { + id: 'trophy_butterfly', + type: 'trophy', + description: 'You earned a Butterfly', + icon: '/img/badges/trophy_green_butterfly.svg', + }), + // Green Plants + trophyFlower: await Factory.build('badge', { + id: 'trophy_flower', + type: 'trophy', + description: 'You earned a Flower', + icon: '/img/badges/trophy_green_flower.svg', + }), + trophyLifetree: await Factory.build('badge', { + id: 'trophy_lifetree', + type: 'trophy', + description: 'You earned the tree of life', + icon: '/img/badges/trophy_green_lifetree.svg', + }), + // Green Misc + trophyDoublerainbow: await Factory.build('badge', { + id: 'trophy_doublerainbow', + type: 'trophy', + description: 'You earned the Double Rainbow', + icon: '/img/badges/trophy_green_doublerainbow.svg', + }), + trophyEndrainbow: await Factory.build('badge', { + id: 'trophy_endrainbow', + type: 'trophy', + description: 'You earned the End of the Rainbow', + icon: '/img/badges/trophy_green_endrainbow.svg', + }), + trophyMagicrainbow: await Factory.build('badge', { + id: 'trophy_magicrainbow', + type: 'trophy', + description: 'You earned the Magic Rainbow', + icon: '/img/badges/trophy_green_magicrainbow.svg', + }), + trophyStarter: await Factory.build('badge', { + id: 'trophy_starter', + type: 'trophy', + description: 'You earned the Starter Badge', + icon: '/img/badges/trophy_green_starter.svg', + }), + trophySuperfounder: await Factory.build('badge', { + id: 'trophy_superfounder', + type: 'trophy', + description: 'You earned the Super Founder Badge', + icon: '/img/badges/trophy_green_superfounder.svg', + }), + } +} + +export const verification = async () => { + return { + // Red Role + verificationModerator: await Factory.build('badge', { + id: 'verification_moderator', + type: 'verification', + description: 'You are a Moderator', + icon: '/img/badges/verification_red_moderator.svg', + }), + verificationAdmin: await Factory.build('badge', { + id: 'verification_admin', + type: 'verification', + description: 'You are an Administrator', + icon: '/img/badges/verification_red_admin.svg', + }), + verificationDeveloper: await Factory.build('badge', { + id: 'verification_developer', + type: 'verification', + description: 'You are a Developer', + icon: '/img/badges/verification_red_developer.svg', + }), + } +} diff --git a/backend/src/graphql/groups.ts b/backend/src/graphql/groups.ts deleted file mode 100644 index a7cfc3351..000000000 --- a/backend/src/graphql/groups.ts +++ /dev/null @@ -1,216 +0,0 @@ -import gql from 'graphql-tag' - -// ------ mutations - -export const createGroupMutation = () => { - return gql` - mutation ( - $id: ID - $name: String! - $slug: String - $about: String - $description: String! - $groupType: GroupType! - $actionRadius: GroupActionRadius! - $categoryIds: [ID] - $locationName: String # empty string '' sets it to null - ) { - CreateGroup( - id: $id - name: $name - slug: $slug - about: $about - description: $description - groupType: $groupType - actionRadius: $actionRadius - categoryIds: $categoryIds - locationName: $locationName - ) { - id - name - slug - createdAt - updatedAt - disabled - deleted - about - description - descriptionExcerpt - groupType - actionRadius - categories { - id - slug - name - icon - } - locationName - location { - name - nameDE - nameEN - } - myRole - } - } - ` -} - -export const updateGroupMutation = () => { - return gql` - mutation ( - $id: ID! - $name: String - $slug: String - $about: String - $description: String - $actionRadius: GroupActionRadius - $categoryIds: [ID] - $avatar: ImageInput - $locationName: String # empty string '' sets it to null - ) { - UpdateGroup( - id: $id - name: $name - slug: $slug - about: $about - description: $description - actionRadius: $actionRadius - categoryIds: $categoryIds - avatar: $avatar - locationName: $locationName - ) { - id - name - slug - createdAt - updatedAt - disabled - deleted - about - description - descriptionExcerpt - groupType - actionRadius - categories { - id - slug - name - icon - } - # avatar # test this as result - locationName - location { - name - nameDE - nameEN - } - myRole - } - } - ` -} - -export const joinGroupMutation = () => { - return gql` - mutation ($groupId: ID!, $userId: ID!) { - JoinGroup(groupId: $groupId, userId: $userId) { - id - name - slug - myRoleInGroup - } - } - ` -} - -export const leaveGroupMutation = () => { - return gql` - mutation ($groupId: ID!, $userId: ID!) { - LeaveGroup(groupId: $groupId, userId: $userId) { - id - name - slug - myRoleInGroup - } - } - ` -} - -export const changeGroupMemberRoleMutation = () => { - return gql` - mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) { - ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) { - id - name - slug - myRoleInGroup - } - } - ` -} - -export const removeUserFromGroupMutation = () => { - return gql` - mutation ($groupId: ID!, $userId: ID!) { - RemoveUserFromGroup(groupId: $groupId, userId: $userId) { - id - name - slug - myRoleInGroup - } - } - ` -} - -// ------ queries - -export const groupQuery = () => { - return gql` - query ($isMember: Boolean, $id: ID, $slug: String) { - Group(isMember: $isMember, id: $id, slug: $slug) { - id - name - slug - createdAt - updatedAt - disabled - deleted - about - description - descriptionExcerpt - groupType - actionRadius - categories { - id - slug - name - icon - } - avatar { - url - } - locationName - location { - name - nameDE - nameEN - } - myRole - } - } - ` -} - -export const groupMembersQuery = () => { - return gql` - query ($id: ID!) { - GroupMembers(id: $id) { - id - name - slug - myRoleInGroup - } - } - ` -} diff --git a/backend/src/graphql/messages.ts b/backend/src/graphql/messages.ts deleted file mode 100644 index 2842c7230..000000000 --- a/backend/src/graphql/messages.ts +++ /dev/null @@ -1,50 +0,0 @@ -import gql from 'graphql-tag' - -export const createMessageMutation = () => { - return gql` - mutation ($roomId: ID!, $content: String!) { - CreateMessage(roomId: $roomId, content: $content) { - id - content - senderId - username - avatar - date - saved - distributed - seen - } - } - ` -} - -export const messageQuery = () => { - return gql` - query ($roomId: ID!, $first: Int, $offset: Int) { - Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) { - _id - id - indexId - content - senderId - author { - id - } - username - avatar - date - saved - distributed - seen - } - } - ` -} - -export const markMessagesAsSeen = () => { - return gql` - mutation ($messageIds: [String!]) { - MarkMessagesAsSeen(messageIds: $messageIds) - } - ` -} diff --git a/backend/src/graphql/notifications.ts b/backend/src/graphql/notifications.ts deleted file mode 100644 index 233077372..000000000 --- a/backend/src/graphql/notifications.ts +++ /dev/null @@ -1,65 +0,0 @@ -import gql from 'graphql-tag' - -// ------ mutations - -export const markAsReadMutation = () => { - return gql` - mutation ($id: ID!) { - markAsRead(id: $id) { - from { - __typename - ... on Post { - content - } - ... on Comment { - content - } - } - read - createdAt - } - } - ` -} - -export const markAllAsReadMutation = () => { - return gql` - mutation { - markAllAsRead { - from { - __typename - ... on Post { - content - } - ... on Comment { - content - } - } - read - createdAt - } - } - ` -} - -// ------ queries - -export const notificationQuery = () => { - return gql` - query ($read: Boolean, $orderBy: NotificationOrdering) { - notifications(read: $read, orderBy: $orderBy) { - from { - __typename - ... on Post { - content - } - ... on Comment { - content - } - } - read - createdAt - } - } - ` -} diff --git a/backend/src/graphql/posts.ts b/backend/src/graphql/posts.ts deleted file mode 100644 index dcd75a4ff..000000000 --- a/backend/src/graphql/posts.ts +++ /dev/null @@ -1,113 +0,0 @@ -import gql from 'graphql-tag' - -// ------ mutations - -export const createPostMutation = () => { - return gql` - mutation ( - $id: ID - $title: String! - $slug: String - $content: String! - $categoryIds: [ID] - $groupId: ID - $postType: PostType - $eventInput: _EventInput - ) { - CreatePost( - id: $id - title: $title - slug: $slug - content: $content - categoryIds: $categoryIds - groupId: $groupId - postType: $postType - eventInput: $eventInput - ) { - id - slug - title - content - disabled - deleted - postType - author { - name - } - categories { - id - } - eventStart - eventEnd - eventLocationName - eventVenue - eventIsOnline - eventLocation { - lng - lat - } - isObservedByMe - observingUsersCount - } - } - ` -} - -// ------ queries - -export const postQuery = () => { - return gql` - query Post($id: ID!) { - Post(id: $id) { - id - title - content - } - } - ` -} - -export const filterPosts = () => { - return gql` - query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) { - Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { - id - title - content - eventStart - } - } - ` -} - -export const profilePagePosts = () => { - return gql` - query profilePagePosts( - $filter: _PostFilter - $first: Int - $offset: Int - $orderBy: [_PostOrdering] - ) { - profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { - id - title - content - } - } - ` -} - -export const searchPosts = () => { - return gql` - query ($query: String!, $firstPosts: Int, $postsOffset: Int) { - searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) { - postCount - posts { - id - title - content - } - } - } - ` -} diff --git a/backend/src/graphql/queries/changeGroupMemberRoleMutation.ts b/backend/src/graphql/queries/changeGroupMemberRoleMutation.ts new file mode 100644 index 000000000..a01c19cfb --- /dev/null +++ b/backend/src/graphql/queries/changeGroupMemberRoleMutation.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const changeGroupMemberRoleMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) { + ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/comments.ts b/backend/src/graphql/queries/createCommentMutation.ts similarity index 76% rename from backend/src/graphql/comments.ts rename to backend/src/graphql/queries/createCommentMutation.ts index b408c5e95..c3824e1d0 100644 --- a/backend/src/graphql/comments.ts +++ b/backend/src/graphql/queries/createCommentMutation.ts @@ -1,7 +1,5 @@ import gql from 'graphql-tag' -// ------ mutations - export const createCommentMutation = gql` mutation ($id: ID, $postId: ID!, $content: String!) { CreateComment(id: $id, postId: $postId, content: $content) { @@ -9,7 +7,3 @@ export const createCommentMutation = gql` } } ` - -// ------ queries - -// fill queries in here diff --git a/backend/src/graphql/queries/createGroupMutation.ts b/backend/src/graphql/queries/createGroupMutation.ts new file mode 100644 index 000000000..20cd93323 --- /dev/null +++ b/backend/src/graphql/queries/createGroupMutation.ts @@ -0,0 +1,55 @@ +import gql from 'graphql-tag' + +export const createGroupMutation = () => { + return gql` + mutation ( + $id: ID + $name: String! + $slug: String + $about: String + $description: String! + $groupType: GroupType! + $actionRadius: GroupActionRadius! + $categoryIds: [ID] + $locationName: String # empty string '' sets it to null + ) { + CreateGroup( + id: $id + name: $name + slug: $slug + about: $about + description: $description + groupType: $groupType + actionRadius: $actionRadius + categoryIds: $categoryIds + locationName: $locationName + ) { + id + name + slug + createdAt + updatedAt + disabled + deleted + about + description + descriptionExcerpt + groupType + actionRadius + categories { + id + slug + name + icon + } + locationName + location { + name + nameDE + nameEN + } + myRole + } + } + ` +} diff --git a/backend/src/graphql/queries/createMessageMutation.ts b/backend/src/graphql/queries/createMessageMutation.ts new file mode 100644 index 000000000..e8c6fc7b8 --- /dev/null +++ b/backend/src/graphql/queries/createMessageMutation.ts @@ -0,0 +1,19 @@ +import gql from 'graphql-tag' + +export const createMessageMutation = () => { + return gql` + mutation ($roomId: ID!, $content: String!) { + CreateMessage(roomId: $roomId, content: $content) { + id + content + senderId + username + avatar + date + saved + distributed + seen + } + } + ` +} diff --git a/backend/src/graphql/queries/createPostMutation.ts b/backend/src/graphql/queries/createPostMutation.ts new file mode 100644 index 000000000..f0a01b303 --- /dev/null +++ b/backend/src/graphql/queries/createPostMutation.ts @@ -0,0 +1,52 @@ +import gql from 'graphql-tag' + +export const createPostMutation = () => { + return gql` + mutation ( + $id: ID + $title: String! + $slug: String + $content: String! + $categoryIds: [ID] + $groupId: ID + $postType: PostType + $eventInput: _EventInput + ) { + CreatePost( + id: $id + title: $title + slug: $slug + content: $content + categoryIds: $categoryIds + groupId: $groupId + postType: $postType + eventInput: $eventInput + ) { + id + slug + title + content + disabled + deleted + postType + author { + name + } + categories { + id + } + eventStart + eventEnd + eventLocationName + eventVenue + eventIsOnline + eventLocation { + lng + lat + } + isObservedByMe + observingUsersCount + } + } + ` +} diff --git a/backend/src/graphql/queries/createRoomMutation.ts b/backend/src/graphql/queries/createRoomMutation.ts new file mode 100644 index 000000000..3a791d294 --- /dev/null +++ b/backend/src/graphql/queries/createRoomMutation.ts @@ -0,0 +1,24 @@ +import gql from 'graphql-tag' + +export const createRoomMutation = () => { + return gql` + mutation ($userId: ID!) { + CreateRoom(userId: $userId) { + id + roomId + roomName + lastMessageAt + unreadCount + #avatar + users { + _id + id + name + avatar { + url + } + } + } + } + ` +} diff --git a/backend/src/graphql/queries/filterPosts.ts b/backend/src/graphql/queries/filterPosts.ts new file mode 100644 index 000000000..7e6d5059f --- /dev/null +++ b/backend/src/graphql/queries/filterPosts.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const filterPosts = () => { + return gql` + query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) { + Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { + id + title + content + eventStart + } + } + ` +} diff --git a/backend/src/graphql/queries/groupMembersQuery.ts b/backend/src/graphql/queries/groupMembersQuery.ts new file mode 100644 index 000000000..b1b8cb313 --- /dev/null +++ b/backend/src/graphql/queries/groupMembersQuery.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const groupMembersQuery = () => { + return gql` + query ($id: ID!) { + GroupMembers(id: $id) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/queries/groupQuery.ts b/backend/src/graphql/queries/groupQuery.ts new file mode 100644 index 000000000..463e9e13e --- /dev/null +++ b/backend/src/graphql/queries/groupQuery.ts @@ -0,0 +1,38 @@ +import gql from 'graphql-tag' + +export const groupQuery = () => { + return gql` + query ($isMember: Boolean, $id: ID, $slug: String) { + Group(isMember: $isMember, id: $id, slug: $slug) { + id + name + slug + createdAt + updatedAt + disabled + deleted + about + description + descriptionExcerpt + groupType + actionRadius + categories { + id + slug + name + icon + } + avatar { + url + } + locationName + location { + name + nameDE + nameEN + } + myRole + } + } + ` +} diff --git a/backend/src/graphql/queries/joinGroupMutation.ts b/backend/src/graphql/queries/joinGroupMutation.ts new file mode 100644 index 000000000..ce627b1ef --- /dev/null +++ b/backend/src/graphql/queries/joinGroupMutation.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const joinGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + JoinGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/queries/leaveGroupMutation.ts b/backend/src/graphql/queries/leaveGroupMutation.ts new file mode 100644 index 000000000..470bd6a7a --- /dev/null +++ b/backend/src/graphql/queries/leaveGroupMutation.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const leaveGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + LeaveGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/userManagement.ts b/backend/src/graphql/queries/loginMutation.ts similarity index 72% rename from backend/src/graphql/userManagement.ts rename to backend/src/graphql/queries/loginMutation.ts index 3cb8a05f8..8c7b36f12 100644 --- a/backend/src/graphql/userManagement.ts +++ b/backend/src/graphql/queries/loginMutation.ts @@ -1,13 +1,7 @@ import gql from 'graphql-tag' -// ------ mutations - export const loginMutation = gql` mutation ($email: String!, $password: String!) { login(email: $email, password: $password) } ` - -// ------ queries - -// fill queries in here diff --git a/backend/src/graphql/queries/markAllAsReadMutation.ts b/backend/src/graphql/queries/markAllAsReadMutation.ts new file mode 100644 index 000000000..d1f19e369 --- /dev/null +++ b/backend/src/graphql/queries/markAllAsReadMutation.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag' + +export const markAllAsReadMutation = () => { + return gql` + mutation { + markAllAsRead { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` +} diff --git a/backend/src/graphql/queries/markAsReadMutation.ts b/backend/src/graphql/queries/markAsReadMutation.ts new file mode 100644 index 000000000..fd855665a --- /dev/null +++ b/backend/src/graphql/queries/markAsReadMutation.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag' + +export const markAsReadMutation = () => { + return gql` + mutation ($id: ID!) { + markAsRead(id: $id) { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` +} diff --git a/backend/src/graphql/queries/markMessagesAsSeen.ts b/backend/src/graphql/queries/markMessagesAsSeen.ts new file mode 100644 index 000000000..9081c5def --- /dev/null +++ b/backend/src/graphql/queries/markMessagesAsSeen.ts @@ -0,0 +1,9 @@ +import gql from 'graphql-tag' + +export const markMessagesAsSeen = () => { + return gql` + mutation ($messageIds: [String!]) { + MarkMessagesAsSeen(messageIds: $messageIds) + } + ` +} diff --git a/backend/src/graphql/queries/messageQuery.ts b/backend/src/graphql/queries/messageQuery.ts new file mode 100644 index 000000000..791851121 --- /dev/null +++ b/backend/src/graphql/queries/messageQuery.ts @@ -0,0 +1,24 @@ +import gql from 'graphql-tag' + +export const messageQuery = () => { + return gql` + query ($roomId: ID!, $first: Int, $offset: Int) { + Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) { + _id + id + indexId + content + senderId + author { + id + } + username + avatar + date + saved + distributed + seen + } + } + ` +} diff --git a/backend/src/graphql/queries/notificationQuery.ts b/backend/src/graphql/queries/notificationQuery.ts new file mode 100644 index 000000000..965fb9ce9 --- /dev/null +++ b/backend/src/graphql/queries/notificationQuery.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag' + +export const notificationQuery = () => { + return gql` + query ($read: Boolean, $orderBy: NotificationOrdering) { + notifications(read: $read, orderBy: $orderBy) { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` +} diff --git a/backend/src/graphql/queries/postQuery.ts b/backend/src/graphql/queries/postQuery.ts new file mode 100644 index 000000000..ff8faf311 --- /dev/null +++ b/backend/src/graphql/queries/postQuery.ts @@ -0,0 +1,13 @@ +import gql from 'graphql-tag' + +export const postQuery = () => { + return gql` + query Post($id: ID!) { + Post(id: $id) { + id + title + content + } + } + ` +} diff --git a/backend/src/graphql/queries/profilePagePosts.ts b/backend/src/graphql/queries/profilePagePosts.ts new file mode 100644 index 000000000..5d713a23c --- /dev/null +++ b/backend/src/graphql/queries/profilePagePosts.ts @@ -0,0 +1,18 @@ +import gql from 'graphql-tag' + +export const profilePagePosts = () => { + return gql` + query profilePagePosts( + $filter: _PostFilter + $first: Int + $offset: Int + $orderBy: [_PostOrdering] + ) { + profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { + id + title + content + } + } + ` +} diff --git a/backend/src/graphql/queries/removeUserFromGroupMutation.ts b/backend/src/graphql/queries/removeUserFromGroupMutation.ts new file mode 100644 index 000000000..bdb9792d9 --- /dev/null +++ b/backend/src/graphql/queries/removeUserFromGroupMutation.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const removeUserFromGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + RemoveUserFromGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/rooms.ts b/backend/src/graphql/queries/roomQuery.ts similarity index 58% rename from backend/src/graphql/rooms.ts rename to backend/src/graphql/queries/roomQuery.ts index 7612641f3..01b24654e 100644 --- a/backend/src/graphql/rooms.ts +++ b/backend/src/graphql/queries/roomQuery.ts @@ -1,28 +1,5 @@ import gql from 'graphql-tag' -export const createRoomMutation = () => { - return gql` - mutation ($userId: ID!) { - CreateRoom(userId: $userId) { - id - roomId - roomName - lastMessageAt - unreadCount - #avatar - users { - _id - id - name - avatar { - url - } - } - } - } - ` -} - export const roomQuery = () => { return gql` query Room($first: Int, $offset: Int, $id: ID) { @@ -57,11 +34,3 @@ export const roomQuery = () => { } ` } - -export const unreadRoomsQuery = () => { - return gql` - query { - UnreadRooms - } - ` -} diff --git a/backend/src/graphql/queries/searchPosts.ts b/backend/src/graphql/queries/searchPosts.ts new file mode 100644 index 000000000..ed9e9a641 --- /dev/null +++ b/backend/src/graphql/queries/searchPosts.ts @@ -0,0 +1,16 @@ +import gql from 'graphql-tag' + +export const searchPosts = () => { + return gql` + query ($query: String!, $firstPosts: Int, $postsOffset: Int) { + searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) { + postCount + posts { + id + title + content + } + } + } + ` +} diff --git a/backend/src/graphql/authentications.ts b/backend/src/graphql/queries/signupVerificationMutation.ts similarity index 88% rename from backend/src/graphql/authentications.ts rename to backend/src/graphql/queries/signupVerificationMutation.ts index 91605ec9f..f504da0ce 100644 --- a/backend/src/graphql/authentications.ts +++ b/backend/src/graphql/queries/signupVerificationMutation.ts @@ -1,7 +1,5 @@ import gql from 'graphql-tag' -// ------ mutations - export const signupVerificationMutation = gql` mutation ( $password: String! @@ -24,7 +22,3 @@ export const signupVerificationMutation = gql` } } ` - -// ------ queries - -// fill queries in here diff --git a/backend/src/graphql/queries/unreadRoomsQuery.ts b/backend/src/graphql/queries/unreadRoomsQuery.ts new file mode 100644 index 000000000..d5612dcad --- /dev/null +++ b/backend/src/graphql/queries/unreadRoomsQuery.ts @@ -0,0 +1,9 @@ +import gql from 'graphql-tag' + +export const unreadRoomsQuery = () => { + return gql` + query { + UnreadRooms + } + ` +} diff --git a/backend/src/graphql/queries/updateGroupMutation.ts b/backend/src/graphql/queries/updateGroupMutation.ts new file mode 100644 index 000000000..826a9c9d4 --- /dev/null +++ b/backend/src/graphql/queries/updateGroupMutation.ts @@ -0,0 +1,56 @@ +import gql from 'graphql-tag' + +export const updateGroupMutation = () => { + return gql` + mutation ( + $id: ID! + $name: String + $slug: String + $about: String + $description: String + $actionRadius: GroupActionRadius + $categoryIds: [ID] + $avatar: ImageInput + $locationName: String # empty string '' sets it to null + ) { + UpdateGroup( + id: $id + name: $name + slug: $slug + about: $about + description: $description + actionRadius: $actionRadius + categoryIds: $categoryIds + avatar: $avatar + locationName: $locationName + ) { + id + name + slug + createdAt + updatedAt + disabled + deleted + about + description + descriptionExcerpt + groupType + actionRadius + categories { + id + slug + name + icon + } + # avatar # test this as result + locationName + location { + name + nameDE + nameEN + } + myRole + } + } + ` +} diff --git a/backend/src/schema/types/enum/EmailNotificationSettingsName.gql b/backend/src/graphql/types/enum/EmailNotificationSettingsName.gql similarity index 100% rename from backend/src/schema/types/enum/EmailNotificationSettingsName.gql rename to backend/src/graphql/types/enum/EmailNotificationSettingsName.gql diff --git a/backend/src/schema/types/enum/EmailNotificationSettingsType.gql b/backend/src/graphql/types/enum/EmailNotificationSettingsType.gql similarity index 100% rename from backend/src/schema/types/enum/EmailNotificationSettingsType.gql rename to backend/src/graphql/types/enum/EmailNotificationSettingsType.gql diff --git a/backend/src/schema/types/enum/Emotion.gql b/backend/src/graphql/types/enum/Emotion.gql similarity index 100% rename from backend/src/schema/types/enum/Emotion.gql rename to backend/src/graphql/types/enum/Emotion.gql diff --git a/backend/src/schema/types/enum/GroupActionRadius.gql b/backend/src/graphql/types/enum/GroupActionRadius.gql similarity index 100% rename from backend/src/schema/types/enum/GroupActionRadius.gql rename to backend/src/graphql/types/enum/GroupActionRadius.gql diff --git a/backend/src/schema/types/enum/GroupMemberRole.gql b/backend/src/graphql/types/enum/GroupMemberRole.gql similarity index 100% rename from backend/src/schema/types/enum/GroupMemberRole.gql rename to backend/src/graphql/types/enum/GroupMemberRole.gql diff --git a/backend/src/schema/types/enum/GroupType.gql b/backend/src/graphql/types/enum/GroupType.gql similarity index 100% rename from backend/src/schema/types/enum/GroupType.gql rename to backend/src/graphql/types/enum/GroupType.gql diff --git a/backend/src/schema/types/enum/OnlineStatus.gql b/backend/src/graphql/types/enum/OnlineStatus.gql similarity index 100% rename from backend/src/schema/types/enum/OnlineStatus.gql rename to backend/src/graphql/types/enum/OnlineStatus.gql diff --git a/backend/src/schema/types/enum/PostType.gql b/backend/src/graphql/types/enum/PostType.gql similarity index 100% rename from backend/src/schema/types/enum/PostType.gql rename to backend/src/graphql/types/enum/PostType.gql diff --git a/backend/src/graphql/types/enum/ShoutTypeEnum.gql b/backend/src/graphql/types/enum/ShoutTypeEnum.gql new file mode 100644 index 000000000..87fcbc5ff --- /dev/null +++ b/backend/src/graphql/types/enum/ShoutTypeEnum.gql @@ -0,0 +1,3 @@ +enum ShoutTypeEnum { + Post +} \ No newline at end of file diff --git a/backend/src/schema/types/enum/UserRole.gql b/backend/src/graphql/types/enum/UserRole.gql similarity index 100% rename from backend/src/schema/types/enum/UserRole.gql rename to backend/src/graphql/types/enum/UserRole.gql diff --git a/backend/src/schema/types/enum/Visibility.gql b/backend/src/graphql/types/enum/Visibility.gql similarity index 100% rename from backend/src/schema/types/enum/Visibility.gql rename to backend/src/graphql/types/enum/Visibility.gql diff --git a/backend/src/schema/types/index.ts b/backend/src/graphql/types/index.ts similarity index 100% rename from backend/src/schema/types/index.ts rename to backend/src/graphql/types/index.ts diff --git a/backend/src/schema/types/scalar/Upload.gql b/backend/src/graphql/types/scalar/Upload.gql similarity index 100% rename from backend/src/schema/types/scalar/Upload.gql rename to backend/src/graphql/types/scalar/Upload.gql diff --git a/backend/src/graphql/types/type/Badge.gql b/backend/src/graphql/types/type/Badge.gql new file mode 100644 index 000000000..cbfe0193d --- /dev/null +++ b/backend/src/graphql/types/type/Badge.gql @@ -0,0 +1,25 @@ +type Badge { + id: ID! + type: BadgeType! + icon: String! + createdAt: String + description: String! + + rewarded: [User]! @relation(name: "REWARDED", direction: "OUT") + verifies: [User]! @relation(name: "VERIFIES", direction: "OUT") +} + +enum BadgeType { + verification + trophy +} + +type Query { + Badge: [Badge] +} + +type Mutation { + setVerificationBadge(badgeId: ID!, userId: ID!): User + rewardTrophyBadge(badgeId: ID!, userId: ID!): User + revokeBadge(badgeId: ID!, userId: ID!): User +} diff --git a/backend/src/schema/types/type/Category.gql b/backend/src/graphql/types/type/Category.gql similarity index 100% rename from backend/src/schema/types/type/Category.gql rename to backend/src/graphql/types/type/Category.gql diff --git a/backend/src/schema/types/type/Comment.gql b/backend/src/graphql/types/type/Comment.gql similarity index 100% rename from backend/src/schema/types/type/Comment.gql rename to backend/src/graphql/types/type/Comment.gql diff --git a/backend/src/schema/types/type/Donations.gql b/backend/src/graphql/types/type/Donations.gql similarity index 100% rename from backend/src/schema/types/type/Donations.gql rename to backend/src/graphql/types/type/Donations.gql diff --git a/backend/src/schema/types/type/EMOTED.gql b/backend/src/graphql/types/type/EMOTED.gql similarity index 100% rename from backend/src/schema/types/type/EMOTED.gql rename to backend/src/graphql/types/type/EMOTED.gql diff --git a/backend/src/schema/types/type/EmailAddress.gql b/backend/src/graphql/types/type/EmailAddress.gql similarity index 100% rename from backend/src/schema/types/type/EmailAddress.gql rename to backend/src/graphql/types/type/EmailAddress.gql diff --git a/backend/src/schema/types/embed.gql b/backend/src/graphql/types/type/Embed.gql similarity index 100% rename from backend/src/schema/types/embed.gql rename to backend/src/graphql/types/type/Embed.gql diff --git a/backend/src/schema/types/type/FILED.gql b/backend/src/graphql/types/type/FILED.gql similarity index 100% rename from backend/src/schema/types/type/FILED.gql rename to backend/src/graphql/types/type/FILED.gql diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/graphql/types/type/Group.gql similarity index 95% rename from backend/src/schema/types/type/Group.gql rename to backend/src/graphql/types/type/Group.gql index 0d399d287..9bcac5047 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/graphql/types/type/Group.gql @@ -42,9 +42,7 @@ type Group { posts: [Post] @relation(name: "IN", direction: "IN") - isMutedByMe: Boolean! - @cypher( - statement: "MATCH (this)<-[m:MUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(m) >= 1") + isMutedByMe: Boolean! @cypher(statement: "MATCH (this) RETURN EXISTS( (this)<-[:MUTED]-(:User {id: $cypherParams.currentUserId}) )") } diff --git a/backend/src/schema/types/type/Image.gql b/backend/src/graphql/types/type/Image.gql similarity index 100% rename from backend/src/schema/types/type/Image.gql rename to backend/src/graphql/types/type/Image.gql diff --git a/backend/src/schema/types/type/InviteCode.gql b/backend/src/graphql/types/type/InviteCode.gql similarity index 100% rename from backend/src/schema/types/type/InviteCode.gql rename to backend/src/graphql/types/type/InviteCode.gql diff --git a/backend/src/schema/types/type/Location.gql b/backend/src/graphql/types/type/Location.gql similarity index 100% rename from backend/src/schema/types/type/Location.gql rename to backend/src/graphql/types/type/Location.gql diff --git a/backend/src/schema/types/type/MEMBER_OF.gql b/backend/src/graphql/types/type/MEMBER_OF.gql similarity index 100% rename from backend/src/schema/types/type/MEMBER_OF.gql rename to backend/src/graphql/types/type/MEMBER_OF.gql diff --git a/backend/src/schema/types/type/Message.gql b/backend/src/graphql/types/type/Message.gql similarity index 100% rename from backend/src/schema/types/type/Message.gql rename to backend/src/graphql/types/type/Message.gql diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/graphql/types/type/NOTIFIED.gql similarity index 100% rename from backend/src/schema/types/type/NOTIFIED.gql rename to backend/src/graphql/types/type/NOTIFIED.gql diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/graphql/types/type/Post.gql similarity index 100% rename from backend/src/schema/types/type/Post.gql rename to backend/src/graphql/types/type/Post.gql diff --git a/backend/src/schema/types/type/REVIEWED.gql b/backend/src/graphql/types/type/REVIEWED.gql similarity index 100% rename from backend/src/schema/types/type/REVIEWED.gql rename to backend/src/graphql/types/type/REVIEWED.gql diff --git a/backend/src/schema/types/type/Report.gql b/backend/src/graphql/types/type/Report.gql similarity index 100% rename from backend/src/schema/types/type/Report.gql rename to backend/src/graphql/types/type/Report.gql diff --git a/backend/src/schema/types/schema.gql b/backend/src/graphql/types/type/Reward.gql.unused similarity index 68% rename from backend/src/schema/types/schema.gql rename to backend/src/graphql/types/type/Reward.gql.unused index 9e2d00bca..6691b34f1 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/graphql/types/type/Reward.gql.unused @@ -1,16 +1,7 @@ -enum ShoutTypeEnum { - Post -} - type Reward { id: ID! user: User @relation(name: "REWARDED", direction: "IN") rewarderId: ID createdAt: String badge: Badge @relation(name: "REWARDED", direction: "OUT") -} - -type SharedInboxEndpoint { - id: ID! - uri: String } \ No newline at end of file diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/graphql/types/type/Room.gql similarity index 100% rename from backend/src/schema/types/type/Room.gql rename to backend/src/graphql/types/type/Room.gql diff --git a/backend/src/schema/types/type/Search.gql b/backend/src/graphql/types/type/Search.gql similarity index 100% rename from backend/src/schema/types/type/Search.gql rename to backend/src/graphql/types/type/Search.gql diff --git a/backend/src/graphql/types/type/SharedInboxEndpoint.gql.old b/backend/src/graphql/types/type/SharedInboxEndpoint.gql.old new file mode 100644 index 000000000..b078af63b --- /dev/null +++ b/backend/src/graphql/types/type/SharedInboxEndpoint.gql.old @@ -0,0 +1,4 @@ +type SharedInboxEndpoint { + id: ID! + uri: String +} \ No newline at end of file diff --git a/backend/src/schema/types/type/SocialMedia.gql b/backend/src/graphql/types/type/SocialMedia.gql similarity index 100% rename from backend/src/schema/types/type/SocialMedia.gql rename to backend/src/graphql/types/type/SocialMedia.gql diff --git a/backend/src/schema/types/type/Statistics.gql b/backend/src/graphql/types/type/Statistics.gql similarity index 100% rename from backend/src/schema/types/type/Statistics.gql rename to backend/src/graphql/types/type/Statistics.gql diff --git a/backend/src/schema/types/type/Tag.gql b/backend/src/graphql/types/type/Tag.gql similarity index 100% rename from backend/src/schema/types/type/Tag.gql rename to backend/src/graphql/types/type/Tag.gql diff --git a/backend/src/schema/types/type/User.gql b/backend/src/graphql/types/type/User.gql similarity index 93% rename from backend/src/schema/types/type/User.gql rename to backend/src/graphql/types/type/User.gql index cac622316..f1a2bcc15 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/graphql/types/type/User.gql @@ -125,8 +125,12 @@ type User { categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT") - badges: [Badge]! @relation(name: "REWARDED", direction: "IN") - badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") + badgeVerification: Badge @relation(name: "VERIFIES", direction: "IN") + badgeTrophies: [Badge]! @relation(name: "REWARDED", direction: "IN") + badgeTrophiesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") + badgeTrophiesSelected: [Badge]! @neo4j_ignore + badgeTrophiesUnused: [Badge]! @neo4j_ignore + badgeTrophiesUnusedCount: Int! @neo4j_ignore emotions: [EMOTED] @@ -247,4 +251,7 @@ type Mutation { # Get a JWT Token for the given Email and password login(email: String!, password: String!): String! + + setTrophyBadgeSelected(slot: Int!, badgeId: ID!): User + resetTrophyBadgesSelected: User } diff --git a/backend/src/schema/types/type/UserData.gql b/backend/src/graphql/types/type/UserData.gql similarity index 100% rename from backend/src/schema/types/type/UserData.gql rename to backend/src/graphql/types/type/UserData.gql diff --git a/backend/src/helpers/asyncForEach.ts b/backend/src/helpers/asyncForEach.ts index 00b0f85a3..354f2cd07 100644 --- a/backend/src/helpers/asyncForEach.ts +++ b/backend/src/helpers/asyncForEach.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable promise/prefer-await-to-callbacks */ /* eslint-disable security/detect-object-injection */ /** diff --git a/backend/src/helpers/encryptPassword.ts b/backend/src/helpers/encryptPassword.ts index 657dee98a..d8d08c23c 100644 --- a/backend/src/helpers/encryptPassword.ts +++ b/backend/src/helpers/encryptPassword.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ import { hashSync } from 'bcryptjs' export default function (args) { diff --git a/backend/src/helpers/jest.ts b/backend/src/helpers/jest.ts index f1a0deb15..5594eb348 100644 --- a/backend/src/helpers/jest.ts +++ b/backend/src/helpers/jest.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable promise/avoid-new */ // sometime we have to wait to check a db state by having a look into the db in a certain moment // or we wait a bit to check if we missed to set an await somewhere diff --git a/backend/src/helpers/walkRecursive.ts b/backend/src/helpers/walkRecursive.ts index 4937f61bb..5874ca3af 100644 --- a/backend/src/helpers/walkRecursive.ts +++ b/backend/src/helpers/walkRecursive.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable promise/prefer-await-to-callbacks */ /* eslint-disable security/detect-object-injection */ /** diff --git a/backend/src/index.ts b/backend/src/index.ts index 35c215803..3da4ec7ae 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ import CONFIG from './config' import createServer from './server' diff --git a/backend/src/jwt/decode.spec.ts b/backend/src/jwt/decode.spec.ts index 29783bc6b..34dd86d68 100644 --- a/backend/src/jwt/decode.spec.ts +++ b/backend/src/jwt/decode.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import Factory, { cleanDatabase } from '@db/factories' import { getDriver, getNeode } from '@db/neo4j' diff --git a/backend/src/jwt/decode.ts b/backend/src/jwt/decode.ts index b4424e660..0a433d38f 100644 --- a/backend/src/jwt/decode.ts +++ b/backend/src/jwt/decode.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import jwt from 'jsonwebtoken' import CONFIG from '@config/index' diff --git a/backend/src/jwt/encode.spec.ts b/backend/src/jwt/encode.spec.ts index 55c74bf8d..8121118f3 100644 --- a/backend/src/jwt/encode.spec.ts +++ b/backend/src/jwt/encode.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import jwt from 'jsonwebtoken' import CONFIG from '@config/index' diff --git a/backend/src/jwt/encode.ts b/backend/src/jwt/encode.ts index 110111faf..742bf438b 100644 --- a/backend/src/jwt/encode.ts +++ b/backend/src/jwt/encode.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import jwt from 'jsonwebtoken' import CONFIG from '@config/index' diff --git a/backend/src/middleware/chatMiddleware.ts b/backend/src/middleware/chatMiddleware.ts index 8ae252e13..17d01fd95 100644 --- a/backend/src/middleware/chatMiddleware.ts +++ b/backend/src/middleware/chatMiddleware.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { isArray } from 'lodash' const setRoomProps = (room) => { diff --git a/backend/src/middleware/excerptMiddleware.ts b/backend/src/middleware/excerptMiddleware.ts index f903dd01c..7a865be90 100644 --- a/backend/src/middleware/excerptMiddleware.ts +++ b/backend/src/middleware/excerptMiddleware.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import trunc from 'trunc-html' import { DESCRIPTION_EXCERPT_HTML_LENGTH } from '@constants/groups' diff --git a/backend/src/middleware/hashtags/extractHashtags.spec.ts b/backend/src/middleware/hashtags/extractHashtags.spec.ts index 739c7de54..134ede761 100644 --- a/backend/src/middleware/hashtags/extractHashtags.spec.ts +++ b/backend/src/middleware/hashtags/extractHashtags.spec.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import extractHashtags from './extractHashtags' describe('extractHashtags', () => { diff --git a/backend/src/middleware/hashtags/extractHashtags.ts b/backend/src/middleware/hashtags/extractHashtags.ts index fc7a93d17..26b224a9a 100644 --- a/backend/src/middleware/hashtags/extractHashtags.ts +++ b/backend/src/middleware/hashtags/extractHashtags.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { load } from 'cheerio' // eslint-disable-next-line import/extensions import { exec, build } from 'xregexp/xregexp-all.js' @@ -19,6 +24,7 @@ export default function (content?) { return $(el).attr('data-hashtag-id') }) .get() + // eslint-disable-next-line @typescript-eslint/no-explicit-any const hashtags: any = [] ids.forEach((id) => { const match = exec(id, regX) diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts index 2bb617a3d..4f0d57303 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.ts b/backend/src/middleware/hashtags/hashtagsMiddleware.ts index 76939d59d..2f53ee1a5 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.ts +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import extractHashtags from './extractHashtags' const updateHashtagsOfPost = async (postId, hashtags, context) => { diff --git a/backend/src/middleware/helpers/cleanHtml.ts b/backend/src/middleware/helpers/cleanHtml.ts index e72746fcf..d429f8f9d 100644 --- a/backend/src/middleware/helpers/cleanHtml.ts +++ b/backend/src/middleware/helpers/cleanHtml.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable security/detect-unsafe-regex */ import linkifyHtml from 'linkify-html' import sanitizeHtml from 'sanitize-html' diff --git a/backend/src/middleware/helpers/email/sendMail.ts b/backend/src/middleware/helpers/email/sendMail.ts index 46d808742..a7d223f1c 100644 --- a/backend/src/middleware/helpers/email/sendMail.ts +++ b/backend/src/middleware/helpers/email/sendMail.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import nodemailer from 'nodemailer' import { htmlToText } from 'nodemailer-html-to-text' @@ -28,6 +34,7 @@ const transporter = nodemailer.createTransport({ }, }) +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-function let sendMailCallback: any = async () => {} if (!hasEmailConfig) { if (!CONFIG.TEST) { @@ -51,6 +58,7 @@ if (!hasEmailConfig) { cleanHtml(templateArgs.html, 'dummyKey', { allowedTags: ['a'], allowedAttributes: { a: ['href'] }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any).replace(/&/g, '&'), ) } diff --git a/backend/src/middleware/helpers/email/templateBuilder.spec.ts b/backend/src/middleware/helpers/email/templateBuilder.spec.ts index 9dbfca91f..85608b55a 100644 --- a/backend/src/middleware/helpers/email/templateBuilder.spec.ts +++ b/backend/src/middleware/helpers/email/templateBuilder.spec.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ import CONFIG from '@config/index' import logosWebapp from '@config/logos' diff --git a/backend/src/middleware/helpers/email/templateBuilder.ts b/backend/src/middleware/helpers/email/templateBuilder.ts index bd44716fe..ffceb49f6 100644 --- a/backend/src/middleware/helpers/email/templateBuilder.ts +++ b/backend/src/middleware/helpers/email/templateBuilder.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable import/no-namespace */ import mustache from 'mustache' diff --git a/backend/src/middleware/helpers/email/templates/de/index.ts b/backend/src/middleware/helpers/email/templates/de/index.ts index 6f0803bc7..408bbb34d 100644 --- a/backend/src/middleware/helpers/email/templates/de/index.ts +++ b/backend/src/middleware/helpers/email/templates/de/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable security/detect-non-literal-fs-filename */ import fs from 'node:fs' import path from 'node:path' diff --git a/backend/src/middleware/helpers/email/templates/en/index.ts b/backend/src/middleware/helpers/email/templates/en/index.ts index 6f0803bc7..408bbb34d 100644 --- a/backend/src/middleware/helpers/email/templates/en/index.ts +++ b/backend/src/middleware/helpers/email/templates/en/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable security/detect-non-literal-fs-filename */ import fs from 'node:fs' import path from 'node:path' diff --git a/backend/src/middleware/helpers/email/templates/index.ts b/backend/src/middleware/helpers/email/templates/index.ts index 79de6b8ae..f481516db 100644 --- a/backend/src/middleware/helpers/email/templates/index.ts +++ b/backend/src/middleware/helpers/email/templates/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable security/detect-non-literal-fs-filename */ import fs from 'node:fs' import path from 'node:path' diff --git a/backend/src/middleware/helpers/isUserOnline.spec.ts b/backend/src/middleware/helpers/isUserOnline.spec.ts index 62ed17f79..de4c840e3 100644 --- a/backend/src/middleware/helpers/isUserOnline.spec.ts +++ b/backend/src/middleware/helpers/isUserOnline.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { isUserOnline } from './isUserOnline' let user diff --git a/backend/src/middleware/helpers/isUserOnline.ts b/backend/src/middleware/helpers/isUserOnline.ts index 23ddeb0dc..b2d0b601f 100644 --- a/backend/src/middleware/helpers/isUserOnline.ts +++ b/backend/src/middleware/helpers/isUserOnline.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export const isUserOnline = (user) => { // Is Recipient considered online const lastActive = new Date(user.lastActiveAt).getTime() diff --git a/backend/src/middleware/includedFieldsMiddleware.ts b/backend/src/middleware/includedFieldsMiddleware.ts index fd95029b0..4b0ab1b1e 100644 --- a/backend/src/middleware/includedFieldsMiddleware.ts +++ b/backend/src/middleware/includedFieldsMiddleware.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import cloneDeep from 'lodash/cloneDeep' const _includeFieldsRecursively = (selectionSet, includedFields) => { diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 225e02209..7ce53663c 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable security/detect-object-injection */ import { applyMiddleware } from 'graphql-middleware' diff --git a/backend/src/middleware/languages/languages.spec.ts b/backend/src/middleware/languages/languages.spec.ts index ca77acac8..11ebf3a41 100644 --- a/backend/src/middleware/languages/languages.spec.ts +++ b/backend/src/middleware/languages/languages.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/middleware/languages/languages.ts b/backend/src/middleware/languages/languages.ts index 6149b90d5..fb8c51a1f 100644 --- a/backend/src/middleware/languages/languages.ts +++ b/backend/src/middleware/languages/languages.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import LanguageDetect from 'languagedetect' import { removeHtmlTags } from '@middleware/helpers/cleanHtml' diff --git a/backend/src/middleware/login/loginMiddleware.ts b/backend/src/middleware/login/loginMiddleware.ts index 04d189b4b..b67e5f60a 100644 --- a/backend/src/middleware/login/loginMiddleware.ts +++ b/backend/src/middleware/login/loginMiddleware.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { sendMail } from '@middleware/helpers/email/sendMail' import { signupTemplate, diff --git a/backend/src/middleware/notifications/mentions/extractMentionedUsers.ts b/backend/src/middleware/notifications/mentions/extractMentionedUsers.ts index b7dc0fed1..ccb7dd1be 100644 --- a/backend/src/middleware/notifications/mentions/extractMentionedUsers.ts +++ b/backend/src/middleware/notifications/mentions/extractMentionedUsers.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ import { load } from 'cheerio' export default (content?) => { diff --git a/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts new file mode 100644 index 000000000..78c95b454 --- /dev/null +++ b/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts @@ -0,0 +1,723 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import CONFIG from '@src/config' +import createServer from '@src/server' + +CONFIG.CATEGORIES_ACTIVE = false + +const sendMailMock = jest.fn() +jest.mock('../helpers/email/sendMail', () => ({ + sendMail: () => sendMailMock(), +})) + +let server, query, mutate, authenticatedUser + +let postAuthor, groupMember + +const driver = getDriver() +const neode = getNeode() + +const mentionString = + '@group-member' + +const createPostMutation = gql` + mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) { + CreatePost(id: $id, title: $title, content: $content, groupId: $groupId) { + id + title + content + } + } +` + +const createCommentMutation = gql` + mutation ($id: ID, $postId: ID!, $content: String!) { + CreateComment(id: $id, postId: $postId, content: $content) { + id + content + } + } +` + +const notificationQuery = gql` + query ($read: Boolean) { + notifications(read: $read, orderBy: updatedAt_desc) { + read + reason + createdAt + relatedUser { + id + } + from { + __typename + ... on Post { + id + content + } + ... on Comment { + id + content + } + ... on Group { + id + } + } + } + } +` + +const followUserMutation = gql` + mutation ($id: ID!) { + followUser(id: $id) { + id + } + } +` + +const markAllAsRead = async () => + mutate({ + mutation: gql` + mutation { + markAllAsRead { + id + } + } + `, + }) + +beforeAll(async () => { + await cleanDatabase() + + const createServerResult = createServer({ + context: () => { + return { + user: authenticatedUser, + neode, + driver, + cypherParams: { + currentUserId: authenticatedUser ? authenticatedUser.id : null, + }, + } + }, + }) + server = createServerResult.server + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +afterAll(async () => { + await cleanDatabase() + driver.close() +}) + +describe('emails sent for notifications', () => { + beforeEach(async () => { + postAuthor = await Factory.build( + 'user', + { + id: 'post-author', + name: 'Post Author', + slug: 'post-author', + }, + { + email: 'test@example.org', + password: '1234', + }, + ) + groupMember = await Factory.build( + 'user', + { + id: 'group-member', + name: 'Group Member', + slug: 'group-member', + }, + { + email: 'group.member@example.org', + password: '1234', + }, + ) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'public-group', + name: 'A public group', + description: 'A public group to test the notifications of mentions', + groupType: 'public', + actionRadius: 'national', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) + }) + + afterEach(async () => { + await cleanDatabase() + }) + + describe('handleContentDataOfPost', () => { + describe('post-author posts into group and mentions following group-member', () => { + describe('all email notification settings are true', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty follower.`, + groupId: 'public-group', + }, + }) + }) + + it('sends only one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + + it('sends 3 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello, @group-member, my trusty follower.', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello, @group-member, my trusty follower.', + }, + read: false, + reason: 'followed_user_posted', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello, @group-member, my trusty follower.', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('email notification for mention in post is false', () => { + beforeEach(async () => { + jest.clearAllMocks() + await groupMember.update({ emailNotificationsMention: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty follower.`, + groupId: 'public-group', + }, + }) + }) + + it('sends only one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + + it('sends 3 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello, @group-member, my trusty follower.', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello, @group-member, my trusty follower.', + }, + read: false, + reason: 'followed_user_posted', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello, @group-member, my trusty follower.', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('email notification for mention in post and followed users is false', () => { + beforeEach(async () => { + jest.clearAllMocks() + await groupMember.update({ emailNotificationsMention: false }) + await groupMember.update({ emailNotificationsFollowingUsers: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty follower.`, + groupId: 'public-group', + }, + }) + }) + + it('sends only one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + + it('sends 3 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello, @group-member, my trusty follower.', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello, @group-member, my trusty follower.', + }, + read: false, + reason: 'followed_user_posted', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello, @group-member, my trusty follower.', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('all relevant email notifications are false', () => { + beforeEach(async () => { + jest.clearAllMocks() + await groupMember.update({ emailNotificationsMention: false }) + await groupMember.update({ emailNotificationsFollowingUsers: false }) + await groupMember.update({ emailNotificationsPostInGroup: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty follower.`, + groupId: 'public-group', + }, + }) + }) + + it('sends NO email', () => { + expect(sendMailMock).not.toHaveBeenCalled() + }) + + it('sends 3 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello, @group-member, my trusty follower.', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello, @group-member, my trusty follower.', + }, + read: false, + reason: 'followed_user_posted', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello, @group-member, my trusty follower.', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) + }) + + describe('handleContentDataOfComment', () => { + describe('user comments post and author responds with in a comment and mentions the user', () => { + describe('all email notification settings are true', () => { + beforeEach(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty follower.`, + groupId: 'public-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment', + content: `Hello, my beloved author.`, + postId: 'post', + }, + }) + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment-2', + content: `Hello, ${mentionString}, my beloved follower.`, + postId: 'post', + }, + }) + }) + + it('sends only one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + + it('sends 2 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello, @group-member, my beloved follower.', + }, + read: false, + reason: 'commented_on_post', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello, @group-member, my beloved follower.', + }, + read: false, + reason: 'mentioned_in_comment', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('email notification commented on post is false', () => { + beforeEach(async () => { + await groupMember.update({ emailNotificationsCommentOnObservedPost: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty follower.`, + groupId: 'public-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment', + content: `Hello, my beloved author.`, + postId: 'post', + }, + }) + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment-2', + content: `Hello, ${mentionString}, my beloved follower.`, + postId: 'post', + }, + }) + }) + + it('sends only one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + + it('sends 2 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello, @group-member, my beloved follower.', + }, + read: false, + reason: 'commented_on_post', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello, @group-member, my beloved follower.', + }, + read: false, + reason: 'mentioned_in_comment', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('all relevant email notifications are false', () => { + beforeEach(async () => { + await groupMember.update({ emailNotificationsCommentOnObservedPost: false }) + await groupMember.update({ emailNotificationsMention: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty follower.`, + groupId: 'public-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment', + content: `Hello, my beloved author.`, + postId: 'post', + }, + }) + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment-2', + content: `Hello, ${mentionString}, my beloved follower.`, + postId: 'post', + }, + }) + }) + + it('sends NO email', () => { + expect(sendMailMock).not.toHaveBeenCalled() + }) + + it('sends 2 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello, @group-member, my beloved follower.', + }, + read: false, + reason: 'commented_on_post', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello, @group-member, my beloved follower.', + }, + read: false, + reason: 'mentioned_in_comment', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/followed-users.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts similarity index 96% rename from backend/src/middleware/notifications/followed-users.spec.ts rename to backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts index 4d4b0e872..5be4ea5b5 100644 --- a/backend/src/middleware/notifications/followed-users.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts @@ -1,9 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { createGroupMutation } from '@graphql/groups' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' import CONFIG from '@src/config' import createServer from '@src/server' diff --git a/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts new file mode 100644 index 000000000..7058efd25 --- /dev/null +++ b/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts @@ -0,0 +1,817 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import CONFIG from '@src/config' +import createServer from '@src/server' + +CONFIG.CATEGORIES_ACTIVE = false + +const sendMailMock = jest.fn() +jest.mock('../helpers/email/sendMail', () => ({ + sendMail: () => sendMailMock(), +})) + +let server, query, mutate, authenticatedUser + +let postAuthor, groupMember, pendingMember, noMember + +const driver = getDriver() +const neode = getNeode() + +const mentionString = ` + @no-meber + @pending-member + @group-member. +` + +const createPostMutation = gql` + mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) { + CreatePost(id: $id, title: $title, content: $content, groupId: $groupId) { + id + title + content + } + } +` + +const createCommentMutation = gql` + mutation ($id: ID, $postId: ID!, $commentContent: String!) { + CreateComment(id: $id, postId: $postId, content: $commentContent) { + id + content + } + } +` + +const notificationQuery = gql` + query ($read: Boolean) { + notifications(read: $read, orderBy: updatedAt_desc) { + read + reason + createdAt + relatedUser { + id + } + from { + __typename + ... on Post { + id + content + } + ... on Comment { + id + content + } + ... on Group { + id + } + } + } + } +` + +const markAllAsRead = async () => + mutate({ + mutation: gql` + mutation { + markAllAsRead { + id + } + } + `, + }) + +beforeAll(async () => { + await cleanDatabase() + + const createServerResult = createServer({ + context: () => { + return { + user: authenticatedUser, + neode, + driver, + cypherParams: { + currentUserId: authenticatedUser ? authenticatedUser.id : null, + }, + } + }, + }) + server = createServerResult.server + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +afterAll(async () => { + await cleanDatabase() + driver.close() +}) + +describe('mentions in groups', () => { + beforeEach(async () => { + postAuthor = await Factory.build( + 'user', + { + id: 'post-author', + name: 'Post Author', + slug: 'post-author', + }, + { + email: 'post.author@example.org', + password: '1234', + }, + ) + groupMember = await Factory.build( + 'user', + { + id: 'group-member', + name: 'Group Member', + slug: 'group-member', + }, + { + email: 'group.member@example.org', + password: '1234', + }, + ) + pendingMember = await Factory.build( + 'user', + { + id: 'pending-member', + name: 'Pending Member', + slug: 'pending-member', + }, + { + email: 'pending.member@example.org', + password: '1234', + }, + ) + noMember = await Factory.build( + 'user', + { + id: 'no-member', + name: 'No Member', + slug: 'no-member', + }, + { + email: 'no.member@example.org', + password: '1234', + }, + ) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'public-group', + name: 'A public group', + description: 'A public group to test the notifications of mentions', + groupType: 'public', + actionRadius: 'national', + }, + }) + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'closed-group', + name: 'A closed group', + description: 'A closed group to test the notifications of mentions', + groupType: 'closed', + actionRadius: 'national', + }, + }) + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'hidden-group', + name: 'A hidden group', + description: 'A hidden group to test the notifications of mentions', + groupType: 'hidden', + actionRadius: 'national', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'group-member', + }, + }) + authenticatedUser = await pendingMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'pending-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: 'pending-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'pending-member', + }, + }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'closed-group', + userId: 'group-member', + roleInGroup: 'usual', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'hidden-group', + userId: 'group-member', + roleInGroup: 'usual', + }, + }) + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + }) + + afterEach(async () => { + await cleanDatabase() + }) + + describe('post in public group', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'public-post', + title: 'This is the post in the public group', + content: `Hey ${mentionString}! Please read this`, + groupId: 'public-group', + }, + }) + }) + + it('sends a notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Post', + id: 'public-post', + }, + read: false, + reason: 'mentioned_in_post', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'public-post', + content: + 'Hey
@no-meber
@pending-member
@group-member.
! Please read this', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'public-post', + content: + 'Hey
@no-meber
@pending-member
@group-member.
! Please read this', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + + it('sends 3 emails, one for each user', () => { + expect(sendMailMock).toHaveBeenCalledTimes(3) + }) + }) + + describe('post in closed group', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'closed-post', + title: 'This is the post in the closed group', + content: `Hey members ${mentionString}! Please read this`, + groupId: 'closed-group', + }, + }) + }) + + it('sends NO notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the pending member', async () => { + authenticatedUser = await pendingMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'closed-post', + content: + 'Hey members
@no-meber
@pending-member
@group-member.
! Please read this', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'closed-post', + content: + 'Hey members
@no-meber
@pending-member
@group-member.
! Please read this', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + + it('sends only 1 email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + }) + + describe('post in hidden group', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'hidden-post', + title: 'This is the post in the hidden group', + content: `Hey hiders ${mentionString}! Please read this`, + groupId: 'hidden-group', + }, + }) + }) + + it('sends NO notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the pending member', async () => { + authenticatedUser = await pendingMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'hidden-post', + content: + 'Hey hiders
@no-meber
@pending-member
@group-member.
! Please read this', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'hidden-post', + content: + 'Hey hiders
@no-meber
@pending-member
@group-member.
! Please read this', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + + it('sends only 1 email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + }) + + describe('comments on group posts', () => { + describe('public group', () => { + beforeEach(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'public-post', + title: 'This is the post in the public group', + content: `Some public content`, + groupId: 'public-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + authenticatedUser = await postAuthor.toJson() + jest.clearAllMocks() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'public-comment', + postId: 'public-post', + commentContent: `Hey everyone ${mentionString}! Please read this`, + }, + }) + }) + + it('sends a notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + id: 'public-comment', + }, + read: false, + reason: 'mentioned_in_comment', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + id: 'public-comment', + }, + read: false, + reason: 'mentioned_in_comment', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends 2 emails', () => { + expect(sendMailMock).toHaveBeenCalledTimes(3) + }) + }) + + describe('closed group', () => { + beforeEach(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'closed-post', + title: 'This is the post in the closed group', + content: `Some closed content`, + groupId: 'closed-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + authenticatedUser = await postAuthor.toJson() + jest.clearAllMocks() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'closed-comment', + postId: 'closed-post', + commentContent: `Hey members ${mentionString}! Please read this`, + }, + }) + }) + + it('sends NO notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the pending member', async () => { + authenticatedUser = await pendingMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + id: 'closed-comment', + }, + read: false, + reason: 'mentioned_in_comment', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends 1 email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + }) + + describe('hidden group', () => { + beforeEach(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'hidden-post', + title: 'This is the post in the hidden group', + content: `Some hidden content`, + groupId: 'hidden-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + authenticatedUser = await postAuthor.toJson() + jest.clearAllMocks() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'hidden-comment', + postId: 'hidden-post', + commentContent: `Hey hiders ${mentionString}! Please read this`, + }, + }) + }) + + it('sends NO notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the pending member', async () => { + authenticatedUser = await pendingMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + id: 'hidden-comment', + }, + read: false, + reason: 'mentioned_in_comment', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends 1 email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/observing-posts.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts similarity index 97% rename from backend/src/middleware/notifications/observing-posts.spec.ts rename to backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts index e10d61d9f..2c73d2beb 100644 --- a/backend/src/middleware/notifications/observing-posts.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts new file mode 100644 index 000000000..da9a6b250 --- /dev/null +++ b/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts @@ -0,0 +1,151 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import CONFIG from '@src/config' +import createServer from '@src/server' + +CONFIG.CATEGORIES_ACTIVE = false + +const sendMailMock = jest.fn() +jest.mock('../helpers/email/sendMail', () => ({ + sendMail: () => sendMailMock(), +})) + +let isUserOnlineMock = jest.fn().mockReturnValue(false) +jest.mock('../helpers/isUserOnline', () => ({ + isUserOnline: () => isUserOnlineMock(), +})) + +let server, mutate, authenticatedUser + +let postAuthor + +const driver = getDriver() +const neode = getNeode() + +const createPostMutation = gql` + mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) { + CreatePost(id: $id, title: $title, content: $content, groupId: $groupId) { + id + title + content + } + } +` + +beforeAll(async () => { + await cleanDatabase() + + const createServerResult = createServer({ + context: () => { + return { + user: authenticatedUser, + neode, + driver, + cypherParams: { + currentUserId: authenticatedUser ? authenticatedUser.id : null, + }, + } + }, + }) + server = createServerResult.server + const createTestClientResult = createTestClient(server) + mutate = createTestClientResult.mutate +}) + +afterAll(async () => { + await cleanDatabase() + driver.close() +}) + +afterEach(async () => { + await cleanDatabase() +}) + +describe('online status and sending emails', () => { + beforeEach(async () => { + postAuthor = await Factory.build( + 'user', + { + id: 'post-author', + name: 'Post Author', + slug: 'post-author', + }, + { + email: 'test@example.org', + password: '1234', + }, + ) + await Factory.build( + 'user', + { + id: 'other-user', + name: 'Other User', + slug: 'other-user', + }, + { + email: 'test2@example.org', + password: '1234', + }, + ) + }) + + describe('user is online', () => { + beforeAll(() => { + isUserOnlineMock = jest.fn().mockReturnValue(true) + }) + + describe('mentioned in post', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-online-1', + title: 'This post mentions the other user', + content: + 'Hello @other-user, are you fine?', + }, + }) + }) + + it('sends NO email to the other user', () => { + expect(sendMailMock).not.toBeCalled() + }) + }) + }) + + describe('user is offline', () => { + beforeAll(() => { + isUserOnlineMock = jest.fn().mockReturnValue(false) + }) + + describe('mentioned in post', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-offline-1', + title: 'This post mentions the other user', + content: + 'Hello @other-user, are you fine?', + }, + }) + }) + + it('sends email to the other user', () => { + expect(sendMailMock).toBeCalledTimes(1) + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/posts-in-groups.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts similarity index 75% rename from backend/src/middleware/notifications/posts-in-groups.spec.ts rename to backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts index a46de2830..9ca4ae7ab 100644 --- a/backend/src/middleware/notifications/posts-in-groups.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts @@ -1,13 +1,17 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { - createGroupMutation, - joinGroupMutation, - changeGroupMemberRoleMutation, -} from '@graphql/groups' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' import CONFIG from '@src/config' import createServer from '@src/server' @@ -118,7 +122,7 @@ afterAll(async () => { }) describe('notify group members of new posts in group', () => { - beforeAll(async () => { + beforeEach(async () => { postAuthor = await Factory.build( 'user', { @@ -193,8 +197,12 @@ describe('notify group members of new posts in group', () => { }) }) + afterEach(async () => { + await cleanDatabase() + }) + describe('group owner posts in group', () => { - beforeAll(async () => { + beforeEach(async () => { jest.clearAllMocks() authenticatedUser = await groupMember.toJson() await markAllAsRead() @@ -275,29 +283,15 @@ describe('notify group members of new posts in group', () => { }) describe('group member mutes group', () => { - it('sets the muted status correctly', async () => { + beforeEach(async () => { authenticatedUser = await groupMember.toJson() - await expect( - mutate({ - mutation: muteGroupMutation, - variables: { - groupId: 'g-1', - }, - }), - ).resolves.toMatchObject({ - data: { - muteGroup: { - isMutedByMe: true, - }, + await mutate({ + mutation: muteGroupMutation, + variables: { + groupId: 'g-1', }, - errors: undefined, }) - }) - - it('sends NO notification when another post is posted', async () => { jest.clearAllMocks() - authenticatedUser = await groupMember.toJson() - await markAllAsRead() authenticatedUser = await postAuthor.toJson() await mutate({ mutation: createPostMutation, @@ -308,7 +302,9 @@ describe('notify group members of new posts in group', () => { groupId: 'g-1', }, }) - authenticatedUser = await groupMember.toJson() + }) + + it('sends NO notification when another post is posted', async () => { await expect( query({ query: notificationQuery, @@ -329,30 +325,18 @@ describe('notify group members of new posts in group', () => { }) describe('group member unmutes group again but disables email', () => { - beforeAll(async () => { + beforeEach(async () => { + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: unmuteGroupMutation, + variables: { + groupId: 'g-1', + }, + }) jest.clearAllMocks() await groupMember.update({ emailNotificationsPostInGroup: false }) }) - it('sets the muted status correctly', async () => { - authenticatedUser = await groupMember.toJson() - await expect( - mutate({ - mutation: unmuteGroupMutation, - variables: { - groupId: 'g-1', - }, - }), - ).resolves.toMatchObject({ - data: { - unmuteGroup: { - isMutedByMe: false, - }, - }, - errors: undefined, - }) - }) - it('sends notification when another post is posted', async () => { authenticatedUser = await groupMember.toJson() await markAllAsRead() @@ -396,5 +380,85 @@ describe('notify group members of new posts in group', () => { }) }) }) + + describe('group member blocks author', () => { + beforeEach(async () => { + await groupMember.relateTo(postAuthor, 'blocked') + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-1', + title: 'This is another post in the group', + content: 'This is the content of another post in the group', + groupId: 'g-1', + }, + }) + }) + + it('sends no notification to the user', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO email', () => { + expect(sendMailMock).not.toHaveBeenCalled() + }) + }) + + describe('group member mutes author', () => { + beforeEach(async () => { + await groupMember.relateTo(postAuthor, 'muted') + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-1', + title: 'This is another post in the group', + content: 'This is the content of another post in the group', + groupId: 'g-1', + }, + }) + }) + + it('sends no notification to the user', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO email', () => { + expect(sendMailMock).not.toHaveBeenCalled() + }) + }) }) }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts index c636a7c87..908ccac22 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts @@ -1,17 +1,21 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { - createGroupMutation, - joinGroupMutation, - leaveGroupMutation, - changeGroupMemberRoleMutation, - removeUserFromGroupMutation, -} from '@graphql/groups' -import { createMessageMutation } from '@graphql/messages' -import { createRoomMutation } from '@graphql/rooms' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { createMessageMutation } from '@graphql/queries/createMessageMutation' +import { createRoomMutation } from '@graphql/queries/createRoomMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation' +import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation' import createServer, { pubsub } from '@src/server' const sendMailMock = jest.fn() @@ -31,8 +35,10 @@ jest.mock('../helpers/isUserOnline', () => ({ isUserOnline: () => isUserOnlineMock(), })) +const pubsubSpy = jest.spyOn(pubsub, 'publish') + let server, query, mutate, notifiedUser, authenticatedUser -let publishSpy + const driver = getDriver() const neode = getNeode() const categoryIds = ['cat9'] @@ -65,7 +71,6 @@ const createCommentMutation = gql` beforeAll(async () => { await cleanDatabase() - publishSpy = jest.spyOn(pubsub, 'publish') const createServerResult = createServer({ context: () => { return { @@ -87,7 +92,6 @@ afterAll(async () => { }) beforeEach(async () => { - publishSpy.mockClear() notifiedUser = await Factory.build( 'user', { @@ -294,6 +298,25 @@ describe('notifications', () => { ).resolves.toEqual(expected) }) }) + + describe('if I have muted the comment author', () => { + it('sends me no notification', async () => { + await notifiedUser.relateTo(commentAuthor, 'muted') + await createCommentOnPostAction() + const expected = expect.objectContaining({ + data: { notifications: [] }, + }) + + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) + }) }) describe('commenter is me', () => { @@ -417,7 +440,7 @@ describe('notifications', () => { it('publishes `NOTIFICATION_ADDED` to me', async () => { await createPostAction() - expect(publishSpy).toHaveBeenCalledWith( + expect(pubsubSpy).toHaveBeenCalledWith( 'NOTIFICATION_ADDED', expect.objectContaining({ notificationAdded: expect.objectContaining({ @@ -428,7 +451,7 @@ describe('notifications', () => { }), }), ) - expect(publishSpy).toHaveBeenCalledTimes(1) + expect(pubsubSpy).toHaveBeenCalledTimes(1) }) describe('updates the post and mentions me again', () => { @@ -578,7 +601,49 @@ describe('notifications', () => { it('does not publish `NOTIFICATION_ADDED`', async () => { await createPostAction() - expect(publishSpy).not.toHaveBeenCalled() + expect(pubsubSpy).not.toHaveBeenCalled() + }) + }) + + describe('but the author of the post muted me', () => { + beforeEach(async () => { + await postAuthor.relateTo(notifiedUser, 'muted') + }) + + it('sends me a notification', async () => { + await createPostAction() + const expected = expect.objectContaining({ + data: { + notifications: [ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + content: + 'Hey @al-capone how do you do?', + id: 'p47', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ], + }, + }) + + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) + + it('publishes `NOTIFICATION_ADDED`', async () => { + await createPostAction() + expect(pubsubSpy).toHaveBeenCalled() }) }) }) @@ -722,7 +787,7 @@ describe('notifications', () => { it('does not publish `NOTIFICATION_ADDED` to authenticated user', async () => { await createCommentOnPostAction() - expect(publishSpy).toHaveBeenCalledWith( + expect(pubsubSpy).toHaveBeenCalledWith( 'NOTIFICATION_ADDED', expect.objectContaining({ notificationAdded: expect.objectContaining({ @@ -733,14 +798,80 @@ describe('notifications', () => { }), }), ) - expect(publishSpy).toHaveBeenCalledTimes(1) + expect(pubsubSpy).toHaveBeenCalledTimes(1) + }) + }) + + describe('but the author of the post muted me', () => { + beforeEach(async () => { + await postAuthor.relateTo(notifiedUser, 'muted') + commentContent = + 'One mention about me with @al-capone.' + commentAuthor = await neode.create( + 'User', + { + id: 'commentAuthor', + name: 'Mrs Comment', + slug: 'mrs-comment', + }, + { + email: 'comment-author@example.org', + password: '1234', + }, + ) + }) + + it('sends me a notification', async () => { + await createCommentOnPostAction() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + content: + 'One mention about me with @al-capone.', + id: 'c47', + }, + read: false, + reason: 'mentioned_in_comment', + relatedUser: null, + }, + ], + }, + errors: undefined, + }) + }) + + it('publishes `NOTIFICATION_ADDED` to authenticated user and me', async () => { + await createCommentOnPostAction() + expect(pubsubSpy).toHaveBeenCalledWith( + 'NOTIFICATION_ADDED', + expect.objectContaining({ + notificationAdded: expect.objectContaining({ + reason: 'commented_on_post', + to: expect.objectContaining({ + id: 'postAuthor', // that's expected, it's not me but the post author + }), + }), + }), + ) + expect(pubsubSpy).toHaveBeenCalledTimes(2) }) }) }) }) }) - describe('chat email notifications', () => { + describe('chat notifications', () => { let chatSender let chatReceiver let roomId @@ -779,7 +910,7 @@ describe('notifications', () => { }) describe('if the chatReceiver is online', () => { - it('sends no email', async () => { + it('publishes subscriptions but sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(true) await mutate({ @@ -790,13 +921,32 @@ describe('notifications', () => { }, }) + expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { + roomCountUpdated: '1', + userId: 'chatReceiver', + }) + expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { + chatMessageAdded: expect.objectContaining({ + id: expect.any(String), + content: 'Some nice message to chatReceiver', + senderId: 'chatSender', + username: 'chatSender', + avatar: null, + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + userId: 'chatReceiver', + }) + expect(sendMailMock).not.toHaveBeenCalled() expect(chatMessageTemplateMock).not.toHaveBeenCalled() }) }) describe('if the chatReceiver is offline', () => { - it('sends an email', async () => { + it('publishes subscriptions and sends an email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await mutate({ @@ -807,13 +957,32 @@ describe('notifications', () => { }, }) + expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { + roomCountUpdated: '1', + userId: 'chatReceiver', + }) + expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { + chatMessageAdded: expect.objectContaining({ + id: expect.any(String), + content: 'Some nice message to chatReceiver', + senderId: 'chatSender', + username: 'chatSender', + avatar: null, + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + userId: 'chatReceiver', + }) + expect(sendMailMock).toHaveBeenCalledTimes(1) expect(chatMessageTemplateMock).toHaveBeenCalledTimes(1) }) }) describe('if the chatReceiver has blocked chatSender', () => { - it('sends no email', async () => { + it('publishes no subscriptions and sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await chatReceiver.relateTo(chatSender, 'blocked') @@ -825,13 +994,37 @@ describe('notifications', () => { }, }) + expect(pubsubSpy).not.toHaveBeenCalled() + expect(pubsubSpy).not.toHaveBeenCalled() + + expect(sendMailMock).not.toHaveBeenCalled() + expect(chatMessageTemplateMock).not.toHaveBeenCalled() + }) + }) + + describe('if the chatReceiver has muted chatSender', () => { + it('publishes no subscriptions and sends no email', async () => { + isUserOnlineMock = jest.fn().mockReturnValue(false) + await chatReceiver.relateTo(chatSender, 'muted') + + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'Some nice message to chatReceiver', + }, + }) + + expect(pubsubSpy).not.toHaveBeenCalled() + expect(pubsubSpy).not.toHaveBeenCalled() + expect(sendMailMock).not.toHaveBeenCalled() expect(chatMessageTemplateMock).not.toHaveBeenCalled() }) }) describe('if the chatReceiver has disabled `emailNotificationsChatMessage`', () => { - it('sends no email', async () => { + it('publishes subscriptions but sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await chatReceiver.update({ emailNotificationsChatMessage: false }) @@ -843,6 +1036,25 @@ describe('notifications', () => { }, }) + expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { + roomCountUpdated: '1', + userId: 'chatReceiver', + }) + expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { + chatMessageAdded: expect.objectContaining({ + id: expect.any(String), + content: 'Some nice message to chatReceiver', + senderId: 'chatSender', + username: 'chatSender', + avatar: null, + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + userId: 'chatReceiver', + }) + expect(sendMailMock).not.toHaveBeenCalled() expect(chatMessageTemplateMock).not.toHaveBeenCalled() }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.ts b/backend/src/middleware/notifications/notificationsMiddleware.ts index ebbcd7886..f7be031c8 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable security/detect-object-injection */ import { sendMail } from '@middleware/helpers/email/sendMail' import { @@ -7,12 +12,14 @@ import { import { isUserOnline } from '@middleware/helpers/isUserOnline' import { validateNotifyUsers } from '@middleware/validation/validationMiddleware' // eslint-disable-next-line import/no-cycle -import { pubsub, NOTIFICATION_ADDED } from '@src/server' +import { getUnreadRoomsCount } from '@schema/resolvers/rooms' +// eslint-disable-next-line import/no-cycle +import { pubsub, NOTIFICATION_ADDED, ROOM_COUNT_UPDATED, CHAT_MESSAGE_ADDED } from '@src/server' import extractMentionedUsers from './mentions/extractMentionedUsers' const queryNotificationEmails = async (context, notificationUserIds) => { - if (!(notificationUserIds && notificationUserIds.length)) return [] + if (!notificationUserIds?.length) return [] const userEmailCypher = ` MATCH (user: User) // blocked users are filtered out from notifications already @@ -38,7 +45,12 @@ const queryNotificationEmails = async (context, notificationUserIds) => { } } -const publishNotifications = async (context, promises, emailNotificationSetting: string) => { +const publishNotifications = async ( + context, + promises, + emailNotificationSetting: string, + emailsSent: string[] = [], +): Promise => { let notifications = await Promise.all(promises) notifications = notifications.flat() const notificationsEmailAddresses = await queryNotificationEmails( @@ -47,15 +59,21 @@ const publishNotifications = async (context, promises, emailNotificationSetting: ) notifications.forEach((notificationAdded, index) => { pubsub.publish(NOTIFICATION_ADDED, { notificationAdded }) - if (notificationAdded.to[emailNotificationSetting] ?? true) { + if ( + (notificationAdded.to[emailNotificationSetting] ?? true) && + !isUserOnline(notificationAdded.to) && + !emailsSent.includes(notificationsEmailAddresses[index].email) + ) { sendMail( notificationTemplate({ email: notificationsEmailAddresses[index].email, variables: { notification: notificationAdded }, }), ) + emailsSent.push(notificationsEmailAddresses[index].email) } }) + return emailsSent } const handleJoinGroup = async (resolve, root, args, context, resolveInfo) => { @@ -115,20 +133,24 @@ const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo const idsOfUsers = extractMentionedUsers(args.content) const post = await resolve(root, args, context, resolveInfo) if (post) { - await publishNotifications( + const sentEmails: string[] = await publishNotifications( context, [notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context)], 'emailNotificationsMention', ) - await publishNotifications( - context, - [notifyFollowingUsers(post.id, groupId, context)], - 'emailNotificationsFollowingUsers', + sentEmails.concat( + await publishNotifications( + context, + [notifyFollowingUsers(post.id, groupId, context)], + 'emailNotificationsFollowingUsers', + sentEmails, + ), ) await publishNotifications( context, [notifyGroupMembersOfNewPost(post.id, groupId, context)], 'emailNotificationsPostInGroup', + sentEmails, ) } return post @@ -140,7 +162,7 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI const comment = await resolve(root, args, context, resolveInfo) const [postAuthor] = await postAuthorOfComment(comment.id, { context }) idsOfMentionedUsers = idsOfMentionedUsers.filter((id) => id !== postAuthor.id) - await publishNotifications( + const sentEmails: string[] = await publishNotifications( context, [ notifyUsersOfMention( @@ -153,13 +175,12 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI ], 'emailNotificationsMention', ) - await publishNotifications( context, [notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context)], 'emailNotificationsCommentOnObservedPost', + sentEmails, ) - return comment } @@ -229,6 +250,8 @@ const notifyGroupMembersOfNewPost = async (postId, groupId, context) => { MATCH (post)-[:IN]->(group:Group { id: $groupId })<-[membership:MEMBER_OF]-(user:User) WHERE NOT membership.role = 'pending' AND NOT (user)-[:MUTED]->(group) + AND NOT (user)-[:MUTED]->(author) + AND NOT (user)-[:BLOCKED]-(author) AND NOT user.id = $userId WITH post, author, user MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) @@ -337,7 +360,7 @@ const notifyMemberOfGroup = async (groupId, userId, reason, context) => { } const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { - if (!(idsOfUsers && idsOfUsers.length)) return [] + if (!idsOfUsers?.length) return [] await validateNotifyUsers(label, reason) let mentionedCypher switch (reason) { @@ -345,8 +368,12 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { mentionedCypher = ` MATCH (post: Post { id: $id })<-[:WROTE]-(author: User) MATCH (user: User) - WHERE user.id in $idsOfUsers - AND NOT (user)-[:BLOCKED]-(author) + WHERE user.id in $idsOfUsers + AND NOT (user)-[:BLOCKED]-(author) + AND NOT (user)-[:MUTED]->(author) + OPTIONAL MATCH (post)-[:IN]->(group:Group) + OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(user) + WITH post, author, user, group WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner'] MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) WITH post AS resource, notification, user ` @@ -356,9 +383,14 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { mentionedCypher = ` MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(commenter: User) MATCH (user: User) - WHERE user.id in $idsOfUsers - AND NOT (user)-[:BLOCKED]-(commenter) - AND NOT (user)-[:BLOCKED]-(postAuthor) + WHERE user.id in $idsOfUsers + AND NOT (user)-[:BLOCKED]-(commenter) + AND NOT (user)-[:BLOCKED]-(postAuthor) + AND NOT (user)-[:MUTED]->(commenter) + AND NOT (user)-[:MUTED]->(postAuthor) + OPTIONAL MATCH (post)-[:IN]->(group:Group) + OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(user) + WITH comment, user, group WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner'] MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user) WITH comment AS resource, notification, user ` @@ -402,7 +434,9 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => { const notificationTransactionResponse = await transaction.run( ` MATCH (observingUser:User)-[:OBSERVES { active: true }]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User) - WHERE NOT (observingUser)-[:BLOCKED]-(commenter) AND NOT observingUser.id = $userId + WHERE NOT (observingUser)-[:BLOCKED]-(commenter) + AND NOT (observingUser)-[:MUTED]->(commenter) + AND NOT observingUser.id = $userId WITH observingUser, post, comment, commenter MATCH (postAuthor:User)-[:WROTE]->(post) MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(observingUser) @@ -436,7 +470,7 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => { const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => { // Execute resolver - const result = await resolve(root, args, context, resolveInfo) + const message = await resolve(root, args, context, resolveInfo) // Query Parameters const { roomId } = args @@ -452,7 +486,7 @@ const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => MATCH (room)<-[:CHATS_IN]-(recipientUser:User)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) WHERE NOT recipientUser.id = $currentUserId AND NOT (recipientUser)-[:BLOCKED]-(senderUser) - AND NOT recipientUser.emailNotificationsChatMessage = false + AND NOT (recipientUser)-[:MUTED]->(senderUser) RETURN senderUser {.*}, recipientUser {.*}, emailAddress {.email} ` const txResponse = await transaction.run(messageRecipientCypher, { @@ -471,13 +505,27 @@ const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => // Execute Query const { senderUser, recipientUser, email } = await messageRecipient - // Send EMail if we found a user(not blocked) and he is not considered online - if (recipientUser && !isUserOnline(recipientUser)) { - void sendMail(chatMessageTemplate({ email, variables: { senderUser, recipientUser } })) + if (recipientUser) { + // send subscriptions + const roomCountUpdated = await getUnreadRoomsCount(recipientUser.id, session) + + void pubsub.publish(ROOM_COUNT_UPDATED, { + roomCountUpdated, + userId: recipientUser.id, + }) + void pubsub.publish(CHAT_MESSAGE_ADDED, { + chatMessageAdded: message, + userId: recipientUser.id, + }) + + // Send EMail if we found a user(not blocked) and he is not considered online + if (recipientUser.emailNotificationsChatMessage !== false && !isUserOnline(recipientUser)) { + void sendMail(chatMessageTemplate({ email, variables: { senderUser, recipientUser } })) + } } // Return resolver result to client - return result + return message } catch (error) { throw new Error(error) } finally { diff --git a/backend/src/middleware/orderByMiddleware.spec.ts b/backend/src/middleware/orderByMiddleware.spec.ts index 9534af76d..1ae15b26a 100644 --- a/backend/src/middleware/orderByMiddleware.spec.ts +++ b/backend/src/middleware/orderByMiddleware.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/middleware/orderByMiddleware.ts b/backend/src/middleware/orderByMiddleware.ts index 64eac8b74..9b437a5e9 100644 --- a/backend/src/middleware/orderByMiddleware.ts +++ b/backend/src/middleware/orderByMiddleware.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import cloneDeep from 'lodash/cloneDeep' const defaultOrderBy = (resolve, root, args, context, resolveInfo) => { diff --git a/backend/src/middleware/permissionsMiddleware.spec.ts b/backend/src/middleware/permissionsMiddleware.spec.ts index 81d73bae8..834e9888a 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.ts +++ b/backend/src/middleware/permissionsMiddleware.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index 0f2b71678..20063de11 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { rule, shield, deny, allow, or, and } from 'graphql-shield' import CONFIG from '@config/index' @@ -12,26 +18,26 @@ const neode = getNeode() const isAuthenticated = rule({ cache: 'contextual', })(async (_parent, _args, ctx, _info) => { - return !!(ctx && ctx.user && ctx.user.id) + return !!ctx?.user?.id }) -const isModerator = rule()(async (parent, args, { user }, info) => { +const isModerator = rule()(async (_parent, _args, { user }, _info) => { return user && (user.role === 'moderator' || user.role === 'admin') }) -const isAdmin = rule()(async (parent, args, { user }, info) => { +const isAdmin = rule()(async (_parent, _args, { user }, _info) => { return user && user.role === 'admin' }) const onlyYourself = rule({ cache: 'no_cache', -})(async (parent, args, context, info) => { +})(async (_parent, args, context, _info) => { return context.user.id === args.id }) const isMyOwn = rule({ cache: 'no_cache', -})(async (parent, args, { user }, info) => { +})(async (parent, _args, { user }, _info) => { return user && user.id === parent.id }) @@ -56,7 +62,7 @@ const isMySocialMedia = rule({ const isAllowedToChangeGroupSettings = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const ownerId = user.id const { id: groupId } = args const session = driver.session() @@ -86,7 +92,7 @@ const isAllowedToChangeGroupSettings = rule({ const isAllowedSeeingGroupMembers = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { id: groupId } = args const session = driver.session() const readTxPromise = session.readTransaction(async (transaction) => { @@ -122,7 +128,7 @@ const isAllowedSeeingGroupMembers = rule({ const isAllowedToChangeGroupMemberRole = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const currentUserId = user.id const { groupId, userId, roleInGroup } = args if (currentUserId === userId) return false @@ -169,7 +175,7 @@ const isAllowedToChangeGroupMemberRole = rule({ const isAllowedToJoinGroup = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { groupId, userId } = args const session = driver.session() const readTxPromise = session.readTransaction(async (transaction) => { @@ -199,7 +205,7 @@ const isAllowedToJoinGroup = rule({ const isAllowedToLeaveGroup = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { groupId, userId } = args if (user.id !== userId) return false const session = driver.session() @@ -229,7 +235,7 @@ const isAllowedToLeaveGroup = rule({ const isMemberOfGroup = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { groupId } = args if (!groupId) return true const userId = user.id @@ -257,7 +263,7 @@ const isMemberOfGroup = rule({ const canRemoveUserFromGroup = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { groupId, userId } = args const currentUserId = user.id if (currentUserId === userId) return false @@ -293,7 +299,7 @@ const canRemoveUserFromGroup = rule({ const canCommentPost = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { postId } = args const userId = user.id const session = driver.session() @@ -350,7 +356,7 @@ const isAuthor = rule({ const isDeletingOwnAccount = rule({ cache: 'no_cache', -})(async (parent, args, context, _info) => { +})(async (_parent, args, context, _info) => { return context.user.id === args.id }) @@ -362,7 +368,7 @@ const noEmailFilter = rule({ const publicRegistration = rule()(() => CONFIG.PUBLIC_REGISTRATION) -const inviteRegistration = rule()(async (_parent, args, { user, driver }) => { +const inviteRegistration = rule()(async (_parent, args, { _user, driver }) => { if (!CONFIG.INVITE_REGISTRATION) return false const { inviteCode } = args const session = driver.session() @@ -429,10 +435,9 @@ export default shield( CreateSocialMedia: isAuthenticated, UpdateSocialMedia: isMySocialMedia, DeleteSocialMedia: isMySocialMedia, - // AddBadgeRewarded: isAdmin, - // RemoveBadgeRewarded: isAdmin, - reward: isAdmin, - unreward: isAdmin, + setVerificationBadge: isAdmin, + rewardTrophyBadge: isAdmin, + revokeBadge: isAdmin, followUser: isAuthenticated, unfollowUser: isAuthenticated, shout: isAuthenticated, @@ -469,6 +474,8 @@ export default shield( toggleObservePost: isAuthenticated, muteGroup: and(isAuthenticated, isMemberOfGroup), unmuteGroup: and(isAuthenticated, isMemberOfGroup), + setTrophyBadgeSelected: isAuthenticated, + resetTrophyBadgesSelected: isAuthenticated, }, User: { email: or(isMyOwn, isAdmin), diff --git a/backend/src/middleware/sentryMiddleware.ts b/backend/src/middleware/sentryMiddleware.ts index b77f680d6..743ec32df 100644 --- a/backend/src/middleware/sentryMiddleware.ts +++ b/backend/src/middleware/sentryMiddleware.ts @@ -1,8 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { sentry } from 'graphql-middleware-sentry' import CONFIG from '@config/index' -// eslint-disable-next-line import/no-mutable-exports +// eslint-disable-next-line import/no-mutable-exports, @typescript-eslint/no-explicit-any let sentryMiddleware: any = (resolve, root, args, context, resolveInfo) => resolve(root, args, context, resolveInfo) @@ -14,9 +18,10 @@ if (CONFIG.SENTRY_DSN_BACKEND) { release: CONFIG.COMMIT, environment: CONFIG.NODE_ENV, }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any withScope: (scope, error, context: any) => { scope.setUser({ - id: context.user && context.user.id, + id: context.user?.id, }) scope.setExtra('body', context.req.body) scope.setExtra('origin', context.req.headers.origin) diff --git a/backend/src/middleware/sluggifyMiddleware.ts b/backend/src/middleware/sluggifyMiddleware.ts index bbe47c9aa..92c2c1367 100644 --- a/backend/src/middleware/sluggifyMiddleware.ts +++ b/backend/src/middleware/sluggifyMiddleware.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import uniqueSlug from './slugify/uniqueSlug' const isUniqueFor = (context, type) => { diff --git a/backend/src/middleware/slugify/uniqueSlug.ts b/backend/src/middleware/slugify/uniqueSlug.ts index 41d58ece3..e24b15eb3 100644 --- a/backend/src/middleware/slugify/uniqueSlug.ts +++ b/backend/src/middleware/slugify/uniqueSlug.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import slugify from 'slug' export default async function uniqueSlug(string, isUnique) { diff --git a/backend/src/middleware/slugifyMiddleware.spec.ts b/backend/src/middleware/slugifyMiddleware.spec.ts index 9e55d54b1..35247471c 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.ts +++ b/backend/src/middleware/slugifyMiddleware.spec.ts @@ -1,10 +1,15 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { signupVerificationMutation } from '@graphql/authentications' -import { createGroupMutation, updateGroupMutation } from '@graphql/groups' -import { createPostMutation } from '@graphql/posts' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import { signupVerificationMutation } from '@graphql/queries/signupVerificationMutation' +import { updateGroupMutation } from '@graphql/queries/updateGroupMutation' import createServer from '@src/server' let authenticatedUser diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts index fa62ed101..0264dedb9 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -42,7 +48,7 @@ beforeAll(async () => { }, { avatar: Factory.build('image', { - url: '/some/offensive/avatar.jpg', + url: 'http://localhost/some/offensive/avatar.jpg', }), }, ), @@ -110,7 +116,7 @@ beforeAll(async () => { }, { image: Factory.build('image', { - url: '/some/offensive/image.jpg', + url: 'http://localhost/some/offensive/image.jpg', }), author: troll, categoryIds, @@ -272,7 +278,7 @@ describe('softDeleteMiddleware', () => { expect(subject.about).toEqual('This self description is very offensive')) it('displays avatar', () => expect(subject.avatar).toEqual({ - url: expect.stringContaining('/some/offensive/avatar.jpg'), + url: expect.stringMatching('http://localhost/some/offensive/avatar.jpg'), })) }) @@ -287,7 +293,7 @@ describe('softDeleteMiddleware', () => { expect(subject.contentExcerpt).toEqual('This is an offensive post content')) it('displays image', () => expect(subject.image).toEqual({ - url: expect.stringContaining('/some/offensive/image.jpg'), + url: expect.stringMatching('http://localhost/some/offensive/image.jpg'), })) }) diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.ts b/backend/src/middleware/softDelete/softDeleteMiddleware.ts index 2e1f60251..4120733ff 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.ts +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ const isModerator = ({ user }) => { return user && (user.role === 'moderator' || user.role === 'admin') } diff --git a/backend/src/middleware/userInteractions.spec.ts b/backend/src/middleware/userInteractions.spec.ts index 37b5401e3..fc096c2b7 100644 --- a/backend/src/middleware/userInteractions.spec.ts +++ b/backend/src/middleware/userInteractions.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/middleware/userInteractions.ts b/backend/src/middleware/userInteractions.ts index 62e8e47f7..bb850a650 100644 --- a/backend/src/middleware/userInteractions.ts +++ b/backend/src/middleware/userInteractions.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ const createRelatedCypher = (relation) => ` MATCH (user:User { id: $currentUser}) MATCH (post:Post { id: $postId}) diff --git a/backend/src/middleware/validation/validationMiddleware.spec.ts b/backend/src/middleware/validation/validationMiddleware.spec.ts index 8e4b4329f..3d3cd9bda 100644 --- a/backend/src/middleware/validation/validationMiddleware.spec.ts +++ b/backend/src/middleware/validation/validationMiddleware.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/middleware/validation/validationMiddleware.ts b/backend/src/middleware/validation/validationMiddleware.ts index 072f2d7b9..75f8f5d09 100644 --- a/backend/src/middleware/validation/validationMiddleware.ts +++ b/backend/src/middleware/validation/validationMiddleware.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' const COMMENT_MIN_LENGTH = 1 @@ -80,7 +87,7 @@ const validateReview = async (resolve, root, args, context, info) => { try { const txResult = await reportReadTxPromise existingReportedResource = txResult - if (!existingReportedResource || !existingReportedResource.length) + if (!existingReportedResource?.length) throw new Error(`Resource not found or is not a Post|Comment|User!`) existingReportedResource = existingReportedResource[0] if (!existingReportedResource.filed) diff --git a/backend/src/middleware/xssMiddleware.ts b/backend/src/middleware/xssMiddleware.ts index 3ed310b40..31ded633c 100644 --- a/backend/src/middleware/xssMiddleware.ts +++ b/backend/src/middleware/xssMiddleware.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import walkRecursive from '@helpers/walkRecursive' import { cleanHtml } from './helpers/cleanHtml' diff --git a/backend/src/models/Badge.ts b/backend/src/models/Badge.ts index 9c4831041..e8d61cb42 100644 --- a/backend/src/models/Badge.ts +++ b/backend/src/models/Badge.ts @@ -1,7 +1,7 @@ export default { id: { type: 'string', primary: true, lowercase: true }, - status: { type: 'string', valid: ['permanent', 'temporary'] }, - type: { type: 'string', valid: ['role', 'crowdfunding'] }, + type: { type: 'string', valid: ['verification', 'trophy'] }, icon: { type: 'string', required: true }, + description: { type: 'string', required: true }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, } diff --git a/backend/src/models/Category.ts b/backend/src/models/Category.ts index 9a3f47fd0..f61d5aaab 100644 --- a/backend/src/models/Category.ts +++ b/backend/src/models/Category.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/Comment.ts b/backend/src/models/Comment.ts index f4548f0c2..f05cb7ccc 100644 --- a/backend/src/models/Comment.ts +++ b/backend/src/models/Comment.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/Donations.ts b/backend/src/models/Donations.ts index 742bfb569..61113702d 100644 --- a/backend/src/models/Donations.ts +++ b/backend/src/models/Donations.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/Group.ts b/backend/src/models/Group.ts index a75ad518f..cff388a0a 100644 --- a/backend/src/models/Group.ts +++ b/backend/src/models/Group.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/Post.ts b/backend/src/models/Post.ts index 75081b728..466e8a21d 100644 --- a/backend/src/models/Post.ts +++ b/backend/src/models/Post.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/Report.ts b/backend/src/models/Report.ts index 3e001746b..07e8a79c1 100644 --- a/backend/src/models/Report.ts +++ b/backend/src/models/Report.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/SocialMedia.ts b/backend/src/models/SocialMedia.ts index 6010c97bb..86f2f90be 100644 --- a/backend/src/models/SocialMedia.ts +++ b/backend/src/models/SocialMedia.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/User.spec.ts b/backend/src/models/User.spec.ts index 3fde03462..7d2c584b5 100644 --- a/backend/src/models/User.spec.ts +++ b/backend/src/models/User.spec.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 754f879a4..611b6a984 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { @@ -52,6 +53,24 @@ export default { target: 'Badge', direction: 'in', }, + selected: { + type: 'relationship', + relationship: 'SELECTED', + target: 'Badge', + direction: 'out', + properties: { + slot: { + type: 'int', + required: true, + }, + }, + }, + verifies: { + type: 'relationship', + relationship: 'VERIFIES', + target: 'Badge', + direction: 'in', + }, invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' }, lastActiveAt: { type: 'string', isoDate: true }, lastOnlineStatus: { type: 'string' }, diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index e02cbc242..6bbdab338 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -1,7 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable n/no-missing-require */ /* eslint-disable n/global-require */ // NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm // module that is not browser-compatible. Node's `fs` module is server-side only +// eslint-disable-next-line @typescript-eslint/no-explicit-any declare let Cypress: any | undefined export default { Image: typeof Cypress !== 'undefined' ? require('./Image') : require('./Image').default, diff --git a/backend/src/schema/index.ts b/backend/src/schema/index.ts index e043bc243..55eccb4dc 100644 --- a/backend/src/schema/index.ts +++ b/backend/src/schema/index.ts @@ -1,7 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { makeAugmentedSchema } from 'neo4j-graphql-js' +import typeDefs from '@graphql/types/index' + import resolvers from './resolvers' -import typeDefs from './types' export default makeAugmentedSchema({ typeDefs, diff --git a/backend/src/schema/resolvers/badges.spec.ts b/backend/src/schema/resolvers/badges.spec.ts new file mode 100644 index 000000000..17fe46c99 --- /dev/null +++ b/backend/src/schema/resolvers/badges.spec.ts @@ -0,0 +1,671 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' + +const driver = getDriver() +const instance = getNeode() + +let authenticatedUser, regularUser, administrator, moderator, badge, verification, query, mutate + +describe('Badges', () => { + beforeAll(async () => { + await cleanDatabase() + + const { server } = createServer({ + context: () => { + return { + driver, + neode: instance, + user: authenticatedUser, + } + }, + }) + query = createTestClient(server).query + mutate = createTestClient(server).mutate + }) + + afterAll(async () => { + await cleanDatabase() + driver.close() + }) + + beforeEach(async () => { + regularUser = await Factory.build( + 'user', + { + id: 'regular-user-id', + role: 'user', + }, + { + email: 'user@example.org', + password: '1234', + }, + ) + moderator = await Factory.build( + 'user', + { + id: 'moderator-id', + role: 'moderator', + }, + { + email: 'moderator@example.org', + }, + ) + administrator = await Factory.build( + 'user', + { + id: 'admin-id', + role: 'admin', + }, + { + email: 'admin@example.org', + }, + ) + badge = await Factory.build('badge', { + id: 'trophy_rhino', + type: 'trophy', + description: 'You earned a rhino', + icon: '/img/badges/trophy_blue_rhino.svg', + }) + + verification = await Factory.build('badge', { + id: 'verification_moderator', + type: 'verification', + description: 'You are a moderator', + icon: '/img/badges/verification_red_moderator.svg', + }) + }) + + // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 + afterEach(async () => { + await cleanDatabase() + }) + + describe('setVerificationBadge', () => { + const variables = { + badgeId: 'verification_moderator', + userId: 'regular-user-id', + } + + const setVerificationBadgeMutation = gql` + mutation ($badgeId: ID!, $userId: ID!) { + setVerificationBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + } + badgeTrophies { + id + } + } + } + ` + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + await expect( + mutate({ mutation: setVerificationBadgeMutation, variables }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated as moderator', () => { + beforeEach(async () => { + authenticatedUser = moderator.toJson() + }) + + describe('rewards badge to user', () => { + it('throws authorization error', async () => { + await expect( + mutate({ mutation: setVerificationBadgeMutation, variables }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + }) + + describe('authenticated as admin', () => { + beforeEach(async () => { + authenticatedUser = await administrator.toJson() + }) + + describe('badge for id does not exist', () => { + it('rejects with an informative error message', async () => { + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { userId: 'regular-user-id', badgeId: 'non-existent-badge-id' }, + }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + describe('non-existent user', () => { + it('rejects with a telling error message', async () => { + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { userId: 'non-existent-user-id', badgeId: 'verification_moderator' }, + }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + describe('badge is not a verification badge', () => { + it('rejects with a telling error message', async () => { + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { userId: 'regular-user-id', badgeId: 'trophy_rhino' }, + }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + it('rewards a verification badge to the user', async () => { + const expected = { + data: { + setVerificationBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'verification_moderator' }, + badgeTrophies: [], + }, + }, + errors: undefined, + } + await expect( + mutate({ mutation: setVerificationBadgeMutation, variables }), + ).resolves.toMatchObject(expected) + }) + + it('overrides the existing verification if a second verification badge is rewarded to the same user', async () => { + await Factory.build('badge', { + id: 'verification_admin', + type: 'verification', + description: 'You are an admin', + icon: '/img/badges/verification_red_admin.svg', + }) + const expected = { + data: { + setVerificationBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'verification_admin' }, + badgeTrophies: [], + }, + }, + errors: undefined, + } + await mutate({ + mutation: setVerificationBadgeMutation, + variables: { + userId: 'regular-user-id', + badgeId: 'verification_moderator', + }, + }) + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { + userId: 'regular-user-id', + badgeId: 'verification_admin', + }, + }), + ).resolves.toMatchObject(expected) + }) + + it('rewards the same verification badge as well to another user', async () => { + const expected = { + data: { + setVerificationBadge: { + id: 'regular-user-2-id', + badgeVerification: { id: 'verification_moderator' }, + badgeTrophies: [], + }, + }, + errors: undefined, + } + await Factory.build( + 'user', + { + id: 'regular-user-2-id', + }, + { + email: 'regular2@email.com', + }, + ) + await mutate({ + mutation: setVerificationBadgeMutation, + variables, + }) + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { + userId: 'regular-user-2-id', + badgeId: 'verification_moderator', + }, + }), + ).resolves.toMatchObject(expected) + }) + }) + }) + + describe('rewardTrophyBadge', () => { + const variables = { + badgeId: 'trophy_rhino', + userId: 'regular-user-id', + } + + const rewardTrophyBadgeMutation = gql` + mutation ($badgeId: ID!, $userId: ID!) { + rewardTrophyBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + } + badgeTrophies { + id + } + } + } + ` + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + await expect( + mutate({ mutation: rewardTrophyBadgeMutation, variables }), + ).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated as moderator', () => { + beforeEach(async () => { + authenticatedUser = moderator.toJson() + }) + + describe('rewards badge to user', () => { + it('throws authorization error', async () => { + await expect( + mutate({ mutation: rewardTrophyBadgeMutation, variables }), + ).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + }) + + describe('authenticated as admin', () => { + beforeEach(async () => { + authenticatedUser = await administrator.toJson() + }) + + describe('badge for id does not exist', () => { + it('rejects with an informative error message', async () => { + await expect( + mutate({ + mutation: rewardTrophyBadgeMutation, + variables: { userId: 'regular-user-id', badgeId: 'non-existent-badge-id' }, + }), + ).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + describe('non-existent user', () => { + it('rejects with a telling error message', async () => { + await expect( + mutate({ + mutation: rewardTrophyBadgeMutation, + variables: { userId: 'non-existent-user-id', badgeId: 'trophy_rhino' }, + }), + ).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + describe('badge is a verification Badge', () => { + it('rejects with a telling error message', async () => { + await expect( + mutate({ + mutation: rewardTrophyBadgeMutation, + variables: { userId: 'regular-user-id', badgeId: 'verification_moderator' }, + }), + ).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + it('rewards a badge to the user', async () => { + const expected = { + data: { + rewardTrophyBadge: { + id: 'regular-user-id', + badgeVerification: null, + badgeTrophies: [{ id: 'trophy_rhino' }], + }, + }, + errors: undefined, + } + await expect( + mutate({ mutation: rewardTrophyBadgeMutation, variables }), + ).resolves.toMatchObject(expected) + }) + + it('rewards a second different badge to the same user', async () => { + await Factory.build('badge', { + id: 'trophy_racoon', + type: 'trophy', + description: 'You earned a racoon', + icon: '/img/badges/trophy_blue_racoon.svg', + }) + const trophies = [{ id: 'trophy_racoon' }, { id: 'trophy_rhino' }] + const expected = { + data: { + rewardTrophyBadge: { + id: 'regular-user-id', + badgeTrophies: expect.arrayContaining(trophies), + }, + }, + errors: undefined, + } + await mutate({ + mutation: rewardTrophyBadgeMutation, + variables: { + userId: 'regular-user-id', + badgeId: 'trophy_rhino', + }, + }) + await expect( + mutate({ + mutation: rewardTrophyBadgeMutation, + variables: { + userId: 'regular-user-id', + badgeId: 'trophy_racoon', + }, + }), + ).resolves.toMatchObject(expected) + }) + + it('rewards the same badge as well to another user', async () => { + const expected = { + data: { + rewardTrophyBadge: { + id: 'regular-user-2-id', + badgeTrophies: [{ id: 'trophy_rhino' }], + }, + }, + errors: undefined, + } + await Factory.build( + 'user', + { + id: 'regular-user-2-id', + }, + { + email: 'regular2@email.com', + }, + ) + await mutate({ + mutation: rewardTrophyBadgeMutation, + variables, + }) + await expect( + mutate({ + mutation: rewardTrophyBadgeMutation, + variables: { + userId: 'regular-user-2-id', + badgeId: 'trophy_rhino', + }, + }), + ).resolves.toMatchObject(expected) + }) + + it('creates no duplicate reward relationships', async () => { + await mutate({ + mutation: rewardTrophyBadgeMutation, + variables, + }) + await mutate({ + mutation: rewardTrophyBadgeMutation, + variables, + }) + + const userQuery = gql` + { + User(id: "regular-user-id") { + badgeTrophiesCount + badgeTrophies { + id + } + } + } + ` + const expected = { + data: { User: [{ badgeTrophiesCount: 1, badgeTrophies: [{ id: 'trophy_rhino' }] }] }, + errors: undefined, + } + + await expect(query({ query: userQuery })).resolves.toMatchObject(expected) + }) + }) + }) + + describe('revokeBadge', () => { + const variables = { + badgeId: 'trophy_rhino', + userId: 'regular-user-id', + } + + beforeEach(async () => { + await regularUser.relateTo(badge, 'rewarded') + await regularUser.relateTo(verification, 'verifies') + }) + + const revokeBadgeMutation = gql` + mutation ($badgeId: ID!, $userId: ID!) { + revokeBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + } + badgeTrophies { + id + } + } + } + ` + + describe('check test setup', () => { + it('user has one badge', async () => { + authenticatedUser = regularUser.toJson() + const userQuery = gql` + { + User(id: "regular-user-id") { + badgeTrophiesCount + badgeTrophies { + id + } + } + } + ` + const expected = { + data: { User: [{ badgeTrophiesCount: 1, badgeTrophies: [{ id: 'trophy_rhino' }] }] }, + errors: undefined, + } + await expect(query({ query: userQuery })).resolves.toMatchObject(expected) + }) + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject({ + data: { revokeBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated moderator', () => { + beforeEach(async () => { + authenticatedUser = await moderator.toJson() + }) + + describe('removes badge from user', () => { + it('throws authorization error', async () => { + await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject( + { + data: { revokeBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }, + ) + }) + }) + }) + + describe('authenticated admin', () => { + beforeEach(async () => { + authenticatedUser = await administrator.toJson() + }) + + it('removes a badge from user', async () => { + await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject({ + data: { + revokeBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'verification_moderator' }, + badgeTrophies: [], + }, + }, + errors: undefined, + }) + }) + + it('does not crash when revoking multiple times', async () => { + await mutate({ mutation: revokeBadgeMutation, variables }) + await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject({ + data: { + revokeBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'verification_moderator' }, + badgeTrophies: [], + }, + }, + errors: undefined, + }) + }) + + it('removes a verification from user', async () => { + await expect( + mutate({ + mutation: revokeBadgeMutation, + variables: { + badgeId: 'verification_moderator', + userId: 'regular-user-id', + }, + }), + ).resolves.toMatchObject({ + data: { + revokeBadge: { + id: 'regular-user-id', + badgeVerification: null, + badgeTrophies: [{ id: 'trophy_rhino' }], + }, + }, + errors: undefined, + }) + }) + + it('does not crash when removing verification multiple times', async () => { + await mutate({ + mutation: revokeBadgeMutation, + variables: { + badgeId: 'verification_moderator', + userId: 'regular-user-id', + }, + }) + await expect( + mutate({ + mutation: revokeBadgeMutation, + variables: { + badgeId: 'verification_moderator', + userId: 'regular-user-id', + }, + }), + ).resolves.toMatchObject({ + data: { + revokeBadge: { + id: 'regular-user-id', + badgeVerification: null, + badgeTrophies: [{ id: 'trophy_rhino' }], + }, + }, + errors: undefined, + }) + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/badges.ts b/backend/src/schema/resolvers/badges.ts index d10d6b482..430e3bf75 100644 --- a/backend/src/schema/resolvers/badges.ts +++ b/backend/src/schema/resolvers/badges.ts @@ -1,9 +1,126 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { neo4jgraphql } from 'neo4j-graphql-js' export default { Query: { - Badge: async (object, args, context, resolveInfo) => { - return neo4jgraphql(object, args, context, resolveInfo) + Badge: async (object, args, context, resolveInfo) => + neo4jgraphql(object, args, context, resolveInfo), + }, + + Mutation: { + setVerificationBadge: async (_object, args, context, _resolveInfo) => { + const { + user: { id: currentUserId }, + } = context + const { badgeId, userId } = args + const session = context.driver.session() + + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const response = await transaction.run( + ` + MATCH (badge:Badge {id: $badgeId, type: 'verification'}), (user:User {id: $userId}) + OPTIONAL MATCH (:Badge {type: 'verification'})-[verify:VERIFIES]->(user) + DELETE verify + MERGE (badge)-[relation:VERIFIES {by: $currentUserId}]->(user) + RETURN relation, user {.*} + `, + { + badgeId, + userId, + currentUserId, + }, + ) + return { + relation: response.records.map((record) => record.get('relation'))[0], + user: response.records.map((record) => record.get('user'))[0], + } + }) + try { + const { relation, user } = await writeTxResultPromise + if (!relation) { + throw new Error( + 'Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + ) + } + return user + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + + rewardTrophyBadge: async (_object, args, context, _resolveInfo) => { + const { + user: { id: currentUserId }, + } = context + const { badgeId, userId } = args + const session = context.driver.session() + + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const response = await transaction.run( + ` + MATCH (badge:Badge {id: $badgeId, type: 'trophy'}), (user:User {id: $userId}) + MERGE (badge)-[relation:REWARDED {by: $currentUserId}]->(user) + RETURN relation, user {.*} + `, + { + badgeId, + userId, + currentUserId, + }, + ) + return { + relation: response.records.map((record) => record.get('relation'))[0], + user: response.records.map((record) => record.get('user'))[0], + } + }) + try { + const { relation, user } = await writeTxResultPromise + if (!relation) { + throw new Error( + 'Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + ) + } + return user + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + + revokeBadge: async (_object, args, context, _resolveInfo) => { + const { badgeId, userId } = args + const session = context.driver.session() + + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const response = await transaction.run( + ` + MATCH (user:User {id: $userId}) + OPTIONAL MATCH (badge:Badge {id: $badgeId})-[relation:REWARDED|VERIFIES]->(user) + DELETE relation + RETURN user {.*} + `, + { + badgeId, + userId, + }, + ) + return response.records.map((record) => record.get('user'))[0] + }) + try { + return await writeTxResultPromise + } catch (error) { + throw new Error(error) + } finally { + session.close() + } }, }, } diff --git a/backend/src/schema/resolvers/comments.spec.ts b/backend/src/schema/resolvers/comments.spec.ts index e92daf86e..432723bae 100644 --- a/backend/src/schema/resolvers/comments.spec.ts +++ b/backend/src/schema/resolvers/comments.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/comments.ts b/backend/src/schema/resolvers/comments.ts index 897c71d6f..3400b1d23 100644 --- a/backend/src/schema/resolvers/comments.ts +++ b/backend/src/schema/resolvers/comments.ts @@ -1,10 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' import Resolver from './helpers/Resolver' export default { Mutation: { - CreateComment: async (object, params, context, resolveInfo) => { + CreateComment: async (_object, params, context, _resolveInfo) => { const { postId } = params const { user, driver } = context // Adding relationship from comment to post by passing in the postId, diff --git a/backend/src/schema/resolvers/donations.spec.ts b/backend/src/schema/resolvers/donations.spec.ts index ef2070d4e..8e58153c1 100644 --- a/backend/src/schema/resolvers/donations.spec.ts +++ b/backend/src/schema/resolvers/donations.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/donations.ts b/backend/src/schema/resolvers/donations.ts index d077e7bed..017a97f5f 100644 --- a/backend/src/schema/resolvers/donations.ts +++ b/backend/src/schema/resolvers/donations.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export default { Query: { Donations: async (_parent, _params, context, _resolveInfo) => { diff --git a/backend/src/schema/resolvers/emails.spec.ts b/backend/src/schema/resolvers/emails.spec.ts index 63141a3fc..d16adbf5d 100644 --- a/backend/src/schema/resolvers/emails.spec.ts +++ b/backend/src/schema/resolvers/emails.spec.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/emails.ts b/backend/src/schema/resolvers/emails.ts index 0638ec634..be721dda5 100644 --- a/backend/src/schema/resolvers/emails.ts +++ b/backend/src/schema/resolvers/emails.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' // eslint-disable-next-line import/extensions import Validator from 'neode/build/Services/Validator.js' @@ -43,7 +48,7 @@ export default { // check email does not belong to anybody const existingEmail = await existingEmailAddress({ args, context }) - if (existingEmail && existingEmail.alreadyExistingEmail && existingEmail.user) + if (existingEmail?.alreadyExistingEmail && existingEmail.user) return existingEmail.alreadyExistingEmail const nonce = generateNonce() diff --git a/backend/src/schema/resolvers/embeds.spec.ts b/backend/src/schema/resolvers/embeds.spec.ts index 92dd224e3..a3b33f81f 100644 --- a/backend/src/schema/resolvers/embeds.spec.ts +++ b/backend/src/schema/resolvers/embeds.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import fs from 'node:fs' import path from 'node:path' @@ -55,6 +58,7 @@ describe('Query', () => { beforeEach(() => { embedAction = async (variables) => { const { server } = createServer({ + // eslint-disable-next-line @typescript-eslint/no-empty-function context: () => {}, }) const { query } = createTestClient(server) diff --git a/backend/src/schema/resolvers/embeds.ts b/backend/src/schema/resolvers/embeds.ts index a75365ec7..8ce144b4f 100644 --- a/backend/src/schema/resolvers/embeds.ts +++ b/backend/src/schema/resolvers/embeds.ts @@ -1,9 +1,12 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import scrape from './embeds/scraper' import { undefinedToNullResolver } from './helpers/Resolver' export default { Query: { - embed: async (object, { url }, context, resolveInfo) => { + embed: async (_object, { url }, _context, _resolveInfo) => { return scrape(url) }, }, @@ -22,7 +25,7 @@ export default { 'lang', 'html', ]), - sources: async (parent, params, context, resolveInfo) => { + sources: async (parent, _params, _context, _resolveInfo) => { return typeof parent.sources === 'undefined' ? [] : parent.sources }, }, diff --git a/backend/src/schema/resolvers/embeds/findProvider.ts b/backend/src/schema/resolvers/embeds/findProvider.ts index a9a30f2bf..9be9732bf 100644 --- a/backend/src/schema/resolvers/embeds/findProvider.ts +++ b/backend/src/schema/resolvers/embeds/findProvider.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import fs from 'node:fs' import path from 'node:path' diff --git a/backend/src/schema/resolvers/embeds/scraper.ts b/backend/src/schema/resolvers/embeds/scraper.ts index e4e19e6b9..bcd25046b 100644 --- a/backend/src/schema/resolvers/embeds/scraper.ts +++ b/backend/src/schema/resolvers/embeds/scraper.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable n/no-extraneous-require */ /* eslint-disable n/global-require */ /* eslint-disable import/no-commonjs */ diff --git a/backend/src/schema/resolvers/filter-posts.spec.ts b/backend/src/schema/resolvers/filter-posts.spec.ts index d5d4485a3..87ba2a8e5 100644 --- a/backend/src/schema/resolvers/filter-posts.spec.ts +++ b/backend/src/schema/resolvers/filter-posts.spec.ts @@ -1,9 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import CONFIG from '@config/index' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { filterPosts, createPostMutation } from '@graphql/posts' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import { filterPosts } from '@graphql/queries/filterPosts' import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false diff --git a/backend/src/schema/resolvers/follow.spec.ts b/backend/src/schema/resolvers/follow.spec.ts index 1e05b2fea..437b4e160 100644 --- a/backend/src/schema/resolvers/follow.spec.ts +++ b/backend/src/schema/resolvers/follow.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/follow.ts b/backend/src/schema/resolvers/follow.ts index 11447974d..d3c1a9081 100644 --- a/backend/src/schema/resolvers/follow.ts +++ b/backend/src/schema/resolvers/follow.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { getNeode } from '@db/neo4j' const neode = getNeode() diff --git a/backend/src/schema/resolvers/groups.spec.ts b/backend/src/schema/resolvers/groups.spec.ts index 624b09e39..39ab87dd4 100644 --- a/backend/src/schema/resolvers/groups.spec.ts +++ b/backend/src/schema/resolvers/groups.spec.ts @@ -1,18 +1,21 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { createTestClient } from 'apollo-server-testing' import CONFIG from '@config/index' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { - createGroupMutation, - updateGroupMutation, - joinGroupMutation, - leaveGroupMutation, - changeGroupMemberRoleMutation, - removeUserFromGroupMutation, - groupMembersQuery, - groupQuery, -} from '@graphql/groups' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { groupMembersQuery } from '@graphql/queries/groupMembersQuery' +import { groupQuery } from '@graphql/queries/groupQuery' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation' +import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation' +import { updateGroupMutation } from '@graphql/queries/updateGroupMutation' import createServer from '@src/server' const driver = getDriver() @@ -2424,7 +2427,7 @@ describe('in mode', () => { id: groupId, }, }) - return result.data && result.data.GroupMembers + return result.data?.GroupMembers ? !!result.data.GroupMembers.find((member) => member.id === userId) : null } diff --git a/backend/src/schema/resolvers/groups.ts b/backend/src/schema/resolvers/groups.ts index 4bf535f35..96d806bf8 100644 --- a/backend/src/schema/resolvers/groups.ts +++ b/backend/src/schema/resolvers/groups.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' import { v4 as uuid } from 'uuid' @@ -440,7 +447,7 @@ export default { }, boolean: { isMutedByMe: - 'MATCH (this)<-[:MUTED]-(related:User {id: $cypherParams.currentUserId}) RETURN COUNT(related) >= 1', + 'MATCH (this) RETURN EXISTS( (this)<-[:MUTED]-(:User {id: $cypherParams.currentUserId}) )', }, }), }, diff --git a/backend/src/schema/resolvers/helpers/Resolver.ts b/backend/src/schema/resolvers/helpers/Resolver.ts index a21893f7d..2d7faa7b7 100644 --- a/backend/src/schema/resolvers/helpers/Resolver.ts +++ b/backend/src/schema/resolvers/helpers/Resolver.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/no-dynamic-delete */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable security/detect-object-injection */ import log from './databaseLogger' @@ -11,6 +19,7 @@ export const undefinedToNullResolver = (list) => { return resolvers } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export default function Resolver(type, options: any = {}) { const { idAttribute = 'id', @@ -21,8 +30,8 @@ export default function Resolver(type, options: any = {}) { hasMany = {}, } = options - const _hasResolver = (resolvers, { key, connection }, { returnType }) => { - return async (parent, params, { driver, cypherParams }, resolveInfo) => { + const _hasResolver = (_resolvers, { key, connection }, { returnType }) => { + return async (parent, _params, { driver, cypherParams }, _resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] const id = parent[idAttribute] const session = driver.session() @@ -45,10 +54,11 @@ export default function Resolver(type, options: any = {}) { } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any const booleanResolver = (obj: any[]) => { const resolvers = {} for (const [key, condition] of Object.entries(obj)) { - resolvers[key] = async (parent, params, { cypherParams, driver }, resolveInfo) => { + resolvers[key] = async (parent, _params, { cypherParams, driver }, _resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] const id = parent[idAttribute] const session = driver.session() @@ -73,7 +83,7 @@ export default function Resolver(type, options: any = {}) { const countResolver = (obj) => { const resolvers = {} for (const [key, connection] of Object.entries(obj)) { - resolvers[key] = async (parent, params, { driver, cypherParams }, resolveInfo) => { + resolvers[key] = async (parent, _params, { driver, cypherParams }, _resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] const session = driver.session() const readTxResultPromise = session.readTransaction(async (txc) => { diff --git a/backend/src/schema/resolvers/helpers/createPasswordReset.ts b/backend/src/schema/resolvers/helpers/createPasswordReset.ts index ec0349c18..0727c5d4e 100644 --- a/backend/src/schema/resolvers/helpers/createPasswordReset.ts +++ b/backend/src/schema/resolvers/helpers/createPasswordReset.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import normalizeEmail from './normalizeEmail' export default async function createPasswordReset(options) { diff --git a/backend/src/schema/resolvers/helpers/databaseLogger.ts b/backend/src/schema/resolvers/helpers/databaseLogger.ts index f2db22965..ad1de5828 100644 --- a/backend/src/schema/resolvers/helpers/databaseLogger.ts +++ b/backend/src/schema/resolvers/helpers/databaseLogger.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable import/no-named-as-default */ // eslint-disable-next-line import/no-extraneous-dependencies import Debug from 'debug' diff --git a/backend/src/schema/resolvers/helpers/events.ts b/backend/src/schema/resolvers/helpers/events.ts index d4fc1fb11..3e5f8d5a8 100644 --- a/backend/src/schema/resolvers/helpers/events.ts +++ b/backend/src/schema/resolvers/helpers/events.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { UserInputError } from 'apollo-server' export const validateEventParams = (params) => { @@ -18,7 +22,7 @@ export const validateEventParams = (params) => { throw new UserInputError('Event venue must be present if event location is given!') } params.eventVenue = eventInput.eventVenue - params.eventLocationName = eventInput.eventLocationName && eventInput.eventLocationName.trim() + params.eventLocationName = eventInput.eventLocationName?.trim() if (params.eventLocationName) { locationName = params.eventLocationName } else { diff --git a/backend/src/schema/resolvers/helpers/existingEmailAddress.ts b/backend/src/schema/resolvers/helpers/existingEmailAddress.ts index 288a14a6d..e1e27bda0 100644 --- a/backend/src/schema/resolvers/helpers/existingEmailAddress.ts +++ b/backend/src/schema/resolvers/helpers/existingEmailAddress.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ export default async function alreadyExistingMail({ args, context }) { const session = context.driver.session() try { diff --git a/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts b/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts index 5a53bf9cb..88d66dd65 100644 --- a/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts +++ b/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { mergeWith, isArray } from 'lodash' import { getMutedUsers } from '@schema/resolvers/users' diff --git a/backend/src/schema/resolvers/helpers/filterInvisiblePosts.ts b/backend/src/schema/resolvers/helpers/filterInvisiblePosts.ts index 73dfaad91..2a264ced4 100644 --- a/backend/src/schema/resolvers/helpers/filterInvisiblePosts.ts +++ b/backend/src/schema/resolvers/helpers/filterInvisiblePosts.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { mergeWith, isArray } from 'lodash' const getInvisiblePosts = async (context) => { @@ -5,7 +9,7 @@ const getInvisiblePosts = async (context) => { const readTxResultPromise = await session.readTransaction(async (transaction) => { let cypher = '' const { user } = context - if (user && user.id) { + if (user?.id) { cypher = ` MATCH (post:Post)<-[:CANNOT_SEE]-(user:User { id: $userId }) RETURN collect(post.id) AS invisiblePostIds` diff --git a/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.ts b/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.ts index a808a5582..9d40b097e 100644 --- a/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.ts +++ b/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.ts @@ -1,8 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { mergeWith, isArray } from 'lodash' const getMyGroupIds = async (context) => { const { user } = context - if (!(user && user.id)) return [] + if (!user?.id) return [] const session = context.driver.session() const readTxResultPromise = await session.readTransaction(async (transaction) => { @@ -22,7 +26,7 @@ const getMyGroupIds = async (context) => { } export const filterPostsOfMyGroups = async (params, context) => { - if (!(params.filter && params.filter.postsInMyGroups)) return params + if (!params.filter?.postsInMyGroups) return params delete params.filter.postsInMyGroups const myGroupIds = await getMyGroupIds(context) params.filter = mergeWith( diff --git a/backend/src/schema/resolvers/helpers/normalizeEmail.ts b/backend/src/schema/resolvers/helpers/normalizeEmail.ts index bc13467c3..9b6be73d7 100644 --- a/backend/src/schema/resolvers/helpers/normalizeEmail.ts +++ b/backend/src/schema/resolvers/helpers/normalizeEmail.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { normalizeEmail } from 'validator' export default (email) => diff --git a/backend/src/schema/resolvers/images/images.spec.ts b/backend/src/schema/resolvers/images/images.spec.ts index a4eb5b1a5..0a2cbadbd 100644 --- a/backend/src/schema/resolvers/images/images.spec.ts +++ b/backend/src/schema/resolvers/images/images.spec.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable promise/prefer-await-to-callbacks */ import { UserInputError } from 'apollo-server' @@ -40,7 +47,7 @@ describe('deleteImage', () => { {}, { avatar: Factory.build('image', { - url: '/some/avatar/url/', + url: 'http://localhost/some/avatar/url/', alt: 'This is the avatar image of a user', }), }, @@ -329,7 +336,7 @@ describe('mergeImage', () => { ), image: Factory.build('image', { alt: 'This is the previous, not updated image', - url: '/some/original/url', + url: 'http://localhost/some/original/url', }), }, ) diff --git a/backend/src/schema/resolvers/images/images.ts b/backend/src/schema/resolvers/images/images.ts index 46eb453c5..d8ce03758 100644 --- a/backend/src/schema/resolvers/images/images.ts +++ b/backend/src/schema/resolvers/images/images.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable promise/avoid-new */ /* eslint-disable security/detect-non-literal-fs-filename */ import { existsSync, unlinkSync, createWriteStream } from 'node:fs' @@ -14,6 +21,7 @@ import { getDriver } from '@db/neo4j' // const widths = [34, 160, 320, 640, 1024] const { AWS_ENDPOINT: endpoint, AWS_REGION: region, AWS_BUCKET: Bucket, S3_CONFIGURED } = CONFIG +// eslint-disable-next-line @typescript-eslint/no-explicit-any export async function deleteImage(resource, relationshipType, opts: any = {}) { sanitizeRelationshipType(relationshipType) const { transaction, deleteCallback } = opts @@ -36,6 +44,7 @@ export async function deleteImage(resource, relationshipType, opts: any = {}) { return image } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export async function mergeImage(resource, relationshipType, imageInput, opts: any = {}) { if (typeof imageInput === 'undefined') return if (imageInput === null) return deleteImage(resource, relationshipType, opts) diff --git a/backend/src/schema/resolvers/inviteCodes.spec.ts b/backend/src/schema/resolvers/inviteCodes.spec.ts index e1a0dac17..aac79210f 100644 --- a/backend/src/schema/resolvers/inviteCodes.spec.ts +++ b/backend/src/schema/resolvers/inviteCodes.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable security/detect-non-literal-regexp */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/inviteCodes.ts b/backend/src/schema/resolvers/inviteCodes.ts index 442ff17b1..02680b5bc 100644 --- a/backend/src/schema/resolvers/inviteCodes.ts +++ b/backend/src/schema/resolvers/inviteCodes.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import generateInviteCode from './helpers/generateInviteCode' import Resolver from './helpers/Resolver' import { validateInviteCode } from './transactions/inviteCodes' diff --git a/backend/src/schema/resolvers/locations.spec.ts b/backend/src/schema/resolvers/locations.spec.ts index 824372d28..d4510284c 100644 --- a/backend/src/schema/resolvers/locations.spec.ts +++ b/backend/src/schema/resolvers/locations.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/locations.ts b/backend/src/schema/resolvers/locations.ts index fcc2fa0aa..bcefa2337 100644 --- a/backend/src/schema/resolvers/locations.ts +++ b/backend/src/schema/resolvers/locations.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ import { UserInputError } from 'apollo-server' import Resolver from './helpers/Resolver' @@ -20,7 +22,7 @@ export default { }), }, Query: { - queryLocations: async (object, args, context, resolveInfo) => { + queryLocations: async (_object, args, _context, _resolveInfo) => { try { return queryLocations(args) } catch (e) { diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts index 4384ddd0f..7b46e0205 100644 --- a/backend/src/schema/resolvers/messages.spec.ts +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -1,21 +1,29 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { createMessageMutation, messageQuery, markMessagesAsSeen } from '@graphql/messages' -import { createRoomMutation, roomQuery } from '@graphql/rooms' +import { createMessageMutation } from '@graphql/queries/createMessageMutation' +import { createRoomMutation } from '@graphql/queries/createRoomMutation' +import { markMessagesAsSeen } from '@graphql/queries/markMessagesAsSeen' +import { messageQuery } from '@graphql/queries/messageQuery' +import { roomQuery } from '@graphql/queries/roomQuery' import createServer, { pubsub } from '@src/server' const driver = getDriver() const neode = getNeode() -const pubsubSpy = jest.spyOn(pubsub, 'publish') - let query let mutate let authenticatedUser let chattingUser, otherChattingUser, notChattingUser +const pubsubSpy = jest.spyOn(pubsub, 'publish') + beforeAll(async () => { await cleanDatabase() @@ -118,7 +126,7 @@ describe('Message', () => { }) describe('user chats in room', () => { - it('returns the message and publishes subscriptions', async () => { + it('returns the message', async () => { await expect( mutate({ mutation: createMessageMutation(), @@ -143,24 +151,6 @@ describe('Message', () => { }, }, }) - expect(pubsubSpy).toBeCalledWith('ROOM_COUNT_UPDATED', { - roomCountUpdated: '1', - userId: 'other-chatting-user', - }) - expect(pubsubSpy).toBeCalledWith('CHAT_MESSAGE_ADDED', { - chatMessageAdded: expect.objectContaining({ - id: expect.any(String), - content: 'Some nice message to other chatting user', - senderId: 'chatting-user', - username: 'Chatting User', - avatar: expect.any(String), - date: expect.any(String), - saved: true, - distributed: false, - seen: false, - }), - userId: 'other-chatting-user', - }) }) describe('room is updated as well', () => { diff --git a/backend/src/schema/resolvers/messages.ts b/backend/src/schema/resolvers/messages.ts index 6879c4be9..c3f362660 100644 --- a/backend/src/schema/resolvers/messages.ts +++ b/backend/src/schema/resolvers/messages.ts @@ -1,10 +1,15 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { withFilter } from 'graphql-subscriptions' import { neo4jgraphql } from 'neo4j-graphql-js' -import { pubsub, ROOM_COUNT_UPDATED, CHAT_MESSAGE_ADDED } from '@src/server' +import { pubsub, CHAT_MESSAGE_ADDED } from '@src/server' import Resolver from './helpers/Resolver' -import { getUnreadRoomsCount } from './rooms' const setMessagesAsDistributed = async (undistributedMessagesIds, session) => { return session.writeTransaction(async (transaction) => { @@ -111,22 +116,7 @@ export default { return message }) try { - const message = await writeTxResultPromise - if (message) { - const roomCountUpdated = await getUnreadRoomsCount(message.recipientId, session) - - // send subscriptions - void pubsub.publish(ROOM_COUNT_UPDATED, { - roomCountUpdated, - userId: message.recipientId, - }) - void pubsub.publish(CHAT_MESSAGE_ADDED, { - chatMessageAdded: message, - userId: message.recipientId, - }) - } - - return message + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { diff --git a/backend/src/schema/resolvers/moderation.spec.ts b/backend/src/schema/resolvers/moderation.spec.ts index 46befdf10..67d070893 100644 --- a/backend/src/schema/resolvers/moderation.spec.ts +++ b/backend/src/schema/resolvers/moderation.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/moderation.ts b/backend/src/schema/resolvers/moderation.ts index a29a411aa..6fe8637c6 100644 --- a/backend/src/schema/resolvers/moderation.ts +++ b/backend/src/schema/resolvers/moderation.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import log from './helpers/databaseLogger' export default { diff --git a/backend/src/schema/resolvers/notifications.spec.ts b/backend/src/schema/resolvers/notifications.spec.ts index a10f97590..2aebe4c24 100644 --- a/backend/src/schema/resolvers/notifications.spec.ts +++ b/backend/src/schema/resolvers/notifications.spec.ts @@ -1,13 +1,16 @@ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import Factory, { cleanDatabase } from '@db/factories' import { getDriver } from '@db/neo4j' -import { - markAsReadMutation, - markAllAsReadMutation, - notificationQuery, -} from '@graphql/notifications' +import { markAllAsReadMutation } from '@graphql/queries/markAllAsReadMutation' +import { markAsReadMutation } from '@graphql/queries/markAsReadMutation' +import { notificationQuery } from '@graphql/queries/notificationQuery' import createServer from '@src/server' const driver = getDriver() diff --git a/backend/src/schema/resolvers/notifications.ts b/backend/src/schema/resolvers/notifications.ts index 5dbbe3d40..6151d305e 100644 --- a/backend/src/schema/resolvers/notifications.ts +++ b/backend/src/schema/resolvers/notifications.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { withFilter } from 'graphql-subscriptions' import { pubsub, NOTIFICATION_ADDED } from '@src/server' @@ -82,7 +88,7 @@ export default { }, }, Mutation: { - markAsRead: async (parent, args, context, resolveInfo) => { + markAsRead: async (_parent, args, context, _resolveInfo) => { const { user: currentUser } = context const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -112,7 +118,7 @@ export default { session.close() } }, - markAllAsRead: async (parent, args, context, resolveInfo) => { + markAllAsRead: async (parent, args, context, _resolveInfo) => { const { user: currentUser } = context const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { diff --git a/backend/src/schema/resolvers/observePosts.spec.ts b/backend/src/schema/resolvers/observePosts.spec.ts index 13fd5ccfc..9176d424e 100644 --- a/backend/src/schema/resolvers/observePosts.spec.ts +++ b/backend/src/schema/resolvers/observePosts.spec.ts @@ -1,10 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import CONFIG from '@config/index' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { createPostMutation } from '@graphql/posts' +import { createPostMutation } from '@graphql/queries/createPostMutation' import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false diff --git a/backend/src/schema/resolvers/passwordReset.spec.ts b/backend/src/schema/resolvers/passwordReset.spec.ts index b5c7e10dd..7a260d345 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.ts +++ b/backend/src/schema/resolvers/passwordReset.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -121,6 +125,7 @@ describe('passwordReset', () => { }) describe('resetPassword', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const setup = async (options: any = {}) => { const { email = 'user@example.org', issuedAt = new Date(), nonce = '12345' } = options await createPasswordReset({ driver, email, issuedAt, nonce }) diff --git a/backend/src/schema/resolvers/passwordReset.ts b/backend/src/schema/resolvers/passwordReset.ts index b9d4d7f51..ab53d65fa 100644 --- a/backend/src/schema/resolvers/passwordReset.ts +++ b/backend/src/schema/resolvers/passwordReset.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import bcrypt from 'bcryptjs' import { v4 as uuid } from 'uuid' @@ -41,7 +47,7 @@ export default { ) }) const [reset] = await passwordResetTxPromise - return !!(reset && reset.properties.usedAt) + return !!reset?.properties.usedAt } finally { session.close() } diff --git a/backend/src/schema/resolvers/posts.spec.ts b/backend/src/schema/resolvers/posts.spec.ts index 103ba98c0..403db3950 100644 --- a/backend/src/schema/resolvers/posts.spec.ts +++ b/backend/src/schema/resolvers/posts.spec.ts @@ -1,10 +1,15 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import CONFIG from '@config/index' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { createPostMutation } from '@graphql/posts' +import { createPostMutation } from '@graphql/queries/createPostMutation' import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = true @@ -1463,7 +1468,7 @@ describe('DeletePost', () => { }, { image: Factory.build('image', { - url: 'path/to/some/image', + url: 'http://localhost/path/to/some/image', }), author, categoryIds, diff --git a/backend/src/schema/resolvers/posts.ts b/backend/src/schema/resolvers/posts.ts index cb48d78ea..f981662ba 100644 --- a/backend/src/schema/resolvers/posts.ts +++ b/backend/src/schema/resolvers/posts.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' import { isEmpty } from 'lodash' import { neo4jgraphql } from 'neo4j-graphql-js' @@ -48,7 +54,7 @@ export default { params = await filterForMutedUsers(params, context) return neo4jgraphql(object, params, context, resolveInfo) }, - PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { + PostsEmotionsCountByEmotion: async (_object, params, context, _resolveInfo) => { const { postId, data } = params const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (transaction) => { @@ -70,7 +76,7 @@ export default { session.close() } }, - PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => { + PostsEmotionsByCurrentUser: async (_object, params, context, _resolveInfo) => { const { postId } = params const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (transaction) => { @@ -242,7 +248,7 @@ export default { } }, - DeletePost: async (object, args, context, resolveInfo) => { + DeletePost: async (_object, args, context, _resolveInfo) => { const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { const deletePostTransactionResponse = await transaction.run( @@ -269,7 +275,7 @@ export default { session.close() } }, - AddPostEmotions: async (object, params, context, resolveInfo) => { + AddPostEmotions: async (_object, params, context, _resolveInfo) => { const { to, data } = params const { user } = context const session = context.driver.session() @@ -296,7 +302,7 @@ export default { session.close() } }, - RemovePostEmotions: async (object, params, context, resolveInfo) => { + RemovePostEmotions: async (_object, params, context, _resolveInfo) => { const { to, data } = params const { id: from } = context.user const session = context.driver.session() @@ -499,7 +505,7 @@ export default { 'MATCH (this)<-[obs:OBSERVES]-(related:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(related) >= 1', }, }), - relatedContributions: async (parent, params, context, resolveInfo) => { + relatedContributions: async (parent, _params, context, _resolveInfo) => { if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions const { id } = parent const session = context.driver.session() diff --git a/backend/src/schema/resolvers/postsInGroups.spec.ts b/backend/src/schema/resolvers/postsInGroups.spec.ts index 17d4f274e..664a64b9f 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.ts +++ b/backend/src/schema/resolvers/postsInGroups.spec.ts @@ -1,22 +1,22 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import CONFIG from '@config/index' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { signupVerificationMutation } from '@graphql/authentications' -import { createCommentMutation } from '@graphql/comments' -import { - createGroupMutation, - changeGroupMemberRoleMutation, - leaveGroupMutation, -} from '@graphql/groups' -import { - createPostMutation, - postQuery, - filterPosts, - profilePagePosts, - searchPosts, -} from '@graphql/posts' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createCommentMutation } from '@graphql/queries/createCommentMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import { filterPosts } from '@graphql/queries/filterPosts' +import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation' +import { postQuery } from '@graphql/queries/postQuery' +import { profilePagePosts } from '@graphql/queries/profilePagePosts' +import { searchPosts } from '@graphql/queries/searchPosts' +import { signupVerificationMutation } from '@graphql/queries/signupVerificationMutation' import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false diff --git a/backend/src/schema/resolvers/registration.spec.ts b/backend/src/schema/resolvers/registration.spec.ts index e61460786..9a0e578cd 100644 --- a/backend/src/schema/resolvers/registration.spec.ts +++ b/backend/src/schema/resolvers/registration.spec.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/registration.ts b/backend/src/schema/resolvers/registration.ts index fc3fc37bb..138a21aea 100644 --- a/backend/src/schema/resolvers/registration.ts +++ b/backend/src/schema/resolvers/registration.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' import { getNeode } from '@db/neo4j' diff --git a/backend/src/schema/resolvers/reports.spec.ts b/backend/src/schema/resolvers/reports.spec.ts index a57efc011..4401329cb 100644 --- a/backend/src/schema/resolvers/reports.spec.ts +++ b/backend/src/schema/resolvers/reports.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/reports.ts b/backend/src/schema/resolvers/reports.ts index f7945e060..35e250f48 100644 --- a/backend/src/schema/resolvers/reports.ts +++ b/backend/src/schema/resolvers/reports.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import log from './helpers/databaseLogger' export default { diff --git a/backend/src/schema/resolvers/rewards.spec.ts b/backend/src/schema/resolvers/rewards.spec.ts deleted file mode 100644 index 2cfe122a0..000000000 --- a/backend/src/schema/resolvers/rewards.spec.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { createTestClient } from 'apollo-server-testing' -import gql from 'graphql-tag' - -import Factory, { cleanDatabase } from '@db/factories' -import { getNeode, getDriver } from '@db/neo4j' -import createServer from '@src/server' - -const driver = getDriver() -const instance = getNeode() - -let authenticatedUser, regularUser, administrator, moderator, badge, query, mutate - -describe('rewards', () => { - const variables = { - from: 'indiegogo_en_rhino', - to: 'regular-user-id', - } - - beforeAll(async () => { - await cleanDatabase() - - const { server } = createServer({ - context: () => { - return { - driver, - neode: instance, - user: authenticatedUser, - } - }, - }) - query = createTestClient(server).query - mutate = createTestClient(server).mutate - }) - - afterAll(async () => { - await cleanDatabase() - driver.close() - }) - - beforeEach(async () => { - regularUser = await Factory.build( - 'user', - { - id: 'regular-user-id', - role: 'user', - }, - { - email: 'user@example.org', - password: '1234', - }, - ) - moderator = await Factory.build( - 'user', - { - id: 'moderator-id', - role: 'moderator', - }, - { - email: 'moderator@example.org', - }, - ) - administrator = await Factory.build( - 'user', - { - id: 'admin-id', - role: 'admin', - }, - { - email: 'admin@example.org', - }, - ) - badge = await Factory.build('badge', { - id: 'indiegogo_en_rhino', - type: 'crowdfunding', - status: 'permanent', - icon: '/img/badges/indiegogo_en_rhino.svg', - }) - }) - - // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 - afterEach(async () => { - await cleanDatabase() - }) - - describe('reward', () => { - const rewardMutation = gql` - mutation ($from: ID!, $to: ID!) { - reward(badgeKey: $from, userId: $to) { - id - badges { - id - } - } - } - ` - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - authenticatedUser = null - await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject({ - data: { reward: null }, - errors: [{ message: 'Not Authorized!' }], - }) - }) - }) - - describe('authenticated admin', () => { - beforeEach(async () => { - authenticatedUser = await administrator.toJson() - }) - - describe('badge for id does not exist', () => { - it('rejects with an informative error message', async () => { - await expect( - mutate({ - mutation: rewardMutation, - variables: { to: 'regular-user-id', from: 'non-existent-badge-id' }, - }), - ).resolves.toMatchObject({ - data: { reward: null }, - errors: [{ message: "Couldn't find a badge with that id" }], - }) - }) - }) - - describe('non-existent user', () => { - it('rejects with a telling error message', async () => { - await expect( - mutate({ - mutation: rewardMutation, - variables: { to: 'non-existent-user-id', from: 'indiegogo_en_rhino' }, - }), - ).resolves.toMatchObject({ - data: { reward: null }, - errors: [{ message: "Couldn't find a user with that id" }], - }) - }) - }) - - it('rewards a badge to user', async () => { - const expected = { - data: { - reward: { - id: 'regular-user-id', - badges: [{ id: 'indiegogo_en_rhino' }], - }, - }, - errors: undefined, - } - await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - - it('rewards a second different badge to same user', async () => { - await Factory.build('badge', { - id: 'indiegogo_en_racoon', - icon: '/img/badges/indiegogo_en_racoon.svg', - }) - const badges = [{ id: 'indiegogo_en_racoon' }, { id: 'indiegogo_en_rhino' }] - const expected = { - data: { - reward: { - id: 'regular-user-id', - badges: expect.arrayContaining(badges), - }, - }, - errors: undefined, - } - await mutate({ - mutation: rewardMutation, - variables: { - to: 'regular-user-id', - from: 'indiegogo_en_rhino', - }, - }) - await expect( - mutate({ - mutation: rewardMutation, - variables: { - to: 'regular-user-id', - from: 'indiegogo_en_racoon', - }, - }), - ).resolves.toMatchObject(expected) - }) - - it('rewards the same badge as well to another user', async () => { - const expected = { - data: { - reward: { - id: 'regular-user-2-id', - badges: [{ id: 'indiegogo_en_rhino' }], - }, - }, - errors: undefined, - } - await Factory.build( - 'user', - { - id: 'regular-user-2-id', - }, - { - email: 'regular2@email.com', - }, - ) - await mutate({ - mutation: rewardMutation, - variables, - }) - await expect( - mutate({ - mutation: rewardMutation, - variables: { - to: 'regular-user-2-id', - from: 'indiegogo_en_rhino', - }, - }), - ).resolves.toMatchObject(expected) - }) - - it('creates no duplicate reward relationships', async () => { - await mutate({ - mutation: rewardMutation, - variables, - }) - await mutate({ - mutation: rewardMutation, - variables, - }) - - const userQuery = gql` - { - User(id: "regular-user-id") { - badgesCount - badges { - id - } - } - } - ` - const expected = { - data: { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] }, - errors: undefined, - } - - await expect(query({ query: userQuery })).resolves.toMatchObject(expected) - }) - }) - - describe('authenticated moderator', () => { - beforeEach(async () => { - authenticatedUser = moderator.toJson() - }) - - describe('rewards badge to user', () => { - it('throws authorization error', async () => { - await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject({ - data: { reward: null }, - errors: [{ message: 'Not Authorized!' }], - }) - }) - }) - }) - }) - - describe('unreward', () => { - beforeEach(async () => { - await regularUser.relateTo(badge, 'rewarded') - }) - const expected = { - data: { unreward: { id: 'regular-user-id', badges: [] } }, - errors: undefined, - } - - const unrewardMutation = gql` - mutation ($from: ID!, $to: ID!) { - unreward(badgeKey: $from, userId: $to) { - id - badges { - id - } - } - } - ` - - describe('check test setup', () => { - it('user has one badge', async () => { - authenticatedUser = regularUser.toJson() - const userQuery = gql` - { - User(id: "regular-user-id") { - badgesCount - badges { - id - } - } - } - ` - const expected = { - data: { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] }, - errors: undefined, - } - await expect(query({ query: userQuery })).resolves.toMatchObject(expected) - }) - }) - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - authenticatedUser = null - await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject({ - data: { unreward: null }, - errors: [{ message: 'Not Authorized!' }], - }) - }) - }) - - describe('authenticated admin', () => { - beforeEach(async () => { - authenticatedUser = await administrator.toJson() - }) - - it('removes a badge from user', async () => { - await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - - it('does not crash when unrewarding multiple times', async () => { - await mutate({ mutation: unrewardMutation, variables }) - await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - }) - - describe('authenticated moderator', () => { - beforeEach(async () => { - authenticatedUser = await moderator.toJson() - }) - - describe('removes bage from user', () => { - it('throws authorization error', async () => { - await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject({ - data: { unreward: null }, - errors: [{ message: 'Not Authorized!' }], - }) - }) - }) - }) - }) -}) diff --git a/backend/src/schema/resolvers/rewards.ts b/backend/src/schema/resolvers/rewards.ts deleted file mode 100644 index bbb889c41..000000000 --- a/backend/src/schema/resolvers/rewards.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { UserInputError } from 'apollo-server' - -import { getNeode } from '@db/neo4j' - -const neode = getNeode() - -const getUserAndBadge = async ({ badgeKey, userId }) => { - const user = await neode.first('User', 'id', userId) - const badge = await neode.first('Badge', 'id', badgeKey) - if (!user) throw new UserInputError("Couldn't find a user with that id") - if (!badge) throw new UserInputError("Couldn't find a badge with that id") - return { user, badge } -} - -export default { - Mutation: { - reward: async (_object, params, context, _resolveInfo) => { - const { user, badge } = await getUserAndBadge(params) - await user.relateTo(badge, 'rewarded') - return user.toJson() - }, - - unreward: async (_object, params, context, _resolveInfo) => { - const { badgeKey, userId } = params - const { user } = await getUserAndBadge(params) - const session = context.driver.session() - try { - await session.writeTransaction((transaction) => { - return transaction.run( - ` - MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId}) - DELETE reward - RETURN rewardedUser - `, - { - badgeKey, - userId, - }, - ) - }) - } finally { - session.close() - } - return user.toJson() - }, - }, -} diff --git a/backend/src/schema/resolvers/roles.ts b/backend/src/schema/resolvers/roles.ts index be9861e08..006d4f5ba 100644 --- a/backend/src/schema/resolvers/roles.ts +++ b/backend/src/schema/resolvers/roles.ts @@ -1,6 +1,7 @@ +/* eslint-disable @typescript-eslint/require-await */ export default { Query: { - availableRoles: async (_parent, args, context, _resolveInfo) => { + availableRoles: async (_parent, _args, _context, _resolveInfo) => { return ['admin', 'moderator', 'user'] }, }, diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index 87ebb4557..f374285e1 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -1,9 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { createMessageMutation } from '@graphql/messages' -import { createRoomMutation, roomQuery, unreadRoomsQuery } from '@graphql/rooms' +import { createMessageMutation } from '@graphql/queries/createMessageMutation' +import { createRoomMutation } from '@graphql/queries/createRoomMutation' +import { roomQuery } from '@graphql/queries/roomQuery' +import { unreadRoomsQuery } from '@graphql/queries/unreadRoomsQuery' import createServer from '@src/server' const driver = getDriver() @@ -387,6 +392,34 @@ describe('Room', () => { }, }) }) + + it('when chattingUser is blocked has 0 unread rooms', async () => { + authenticatedUser = await otherChattingUser.toJson() + await otherChattingUser.relateTo(chattingUser, 'blocked') + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 0, + }, + }) + }) + + it('when chattingUser is muted has 0 unread rooms', async () => { + authenticatedUser = await otherChattingUser.toJson() + await otherChattingUser.relateTo(chattingUser, 'muted') + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 0, + }, + }) + }) }) describe('as not chatting user', () => { @@ -558,6 +591,7 @@ describe('Room', () => { }) describe('query single room', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let result: any = null beforeAll(async () => { diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index 0ff37b594..9c6751695 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -1,6 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { withFilter } from 'graphql-subscriptions' import { neo4jgraphql } from 'neo4j-graphql-js' +// eslint-disable-next-line import/no-cycle import { pubsub, ROOM_COUNT_UPDATED } from '@src/server' import Resolver from './helpers/Resolver' @@ -8,8 +15,10 @@ import Resolver from './helpers/Resolver' export const getUnreadRoomsCount = async (userId, session) => { return session.readTransaction(async (transaction) => { const unreadRoomsCypher = ` - MATCH (:User { id: $userId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User) + MATCH (user:User { id: $userId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User) WHERE NOT sender.id = $userId AND NOT message.seen + AND NOT (user)-[:BLOCKED]->(sender) + AND NOT (user)-[:MUTED]->(sender) RETURN toString(COUNT(DISTINCT room)) AS count ` const unreadRoomsTxResponse = await transaction.run(unreadRoomsCypher, { userId }) @@ -36,7 +45,7 @@ export default { } return neo4jgraphql(object, params, context, resolveInfo) }, - UnreadRooms: async (object, params, context, resolveInfo) => { + UnreadRooms: async (_object, _params, context, _resolveInfo) => { const { user: { id: currentUserId }, } = context diff --git a/backend/src/schema/resolvers/searches.spec.ts b/backend/src/schema/resolvers/searches.spec.ts index 5902f2746..cb774cad5 100644 --- a/backend/src/schema/resolvers/searches.spec.ts +++ b/backend/src/schema/resolvers/searches.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/searches.ts b/backend/src/schema/resolvers/searches.ts index 5f4097c17..845a070a5 100644 --- a/backend/src/schema/resolvers/searches.ts +++ b/backend/src/schema/resolvers/searches.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import log from './helpers/databaseLogger' import { queryString } from './searches/queryString' @@ -250,6 +256,7 @@ export default { ] params.limit = 15 + // eslint-disable-next-line @typescript-eslint/no-explicit-any const type: any = multiSearchMap.find((obj) => obj.symbol === searchType) return getSearchResults(context, type.setup, params) }, diff --git a/backend/src/schema/resolvers/searches/queryString.ts b/backend/src/schema/resolvers/searches/queryString.ts index 8f415c5e6..da8e7bffb 100644 --- a/backend/src/schema/resolvers/searches/queryString.ts +++ b/backend/src/schema/resolvers/searches/queryString.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export function queryString(str) { const normalizedString = normalizeWhitespace(str) const escapedString = escapeSpecialCharacters(normalizedString) diff --git a/backend/src/schema/resolvers/shout.spec.ts b/backend/src/schema/resolvers/shout.spec.ts index 7fe7176ab..4ec6189bd 100644 --- a/backend/src/schema/resolvers/shout.spec.ts +++ b/backend/src/schema/resolvers/shout.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/shout.ts b/backend/src/schema/resolvers/shout.ts index 8c330cd67..0a7ec6a39 100644 --- a/backend/src/schema/resolvers/shout.ts +++ b/backend/src/schema/resolvers/shout.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import log from './helpers/databaseLogger' export default { diff --git a/backend/src/schema/resolvers/socialMedia.spec.ts b/backend/src/schema/resolvers/socialMedia.spec.ts index 3a36e791e..584e64cfb 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.ts +++ b/backend/src/schema/resolvers/socialMedia.spec.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/socialMedia.ts b/backend/src/schema/resolvers/socialMedia.ts index ac27eb1f9..d3a563d2c 100644 --- a/backend/src/schema/resolvers/socialMedia.ts +++ b/backend/src/schema/resolvers/socialMedia.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { getNeode } from '@db/neo4j' import Resolver from './helpers/Resolver' @@ -6,7 +10,7 @@ const neode = getNeode() export default { Mutation: { - CreateSocialMedia: async (object, params, context, resolveInfo) => { + CreateSocialMedia: async (_object, params, context, _resolveInfo) => { const [user, socialMedia] = await Promise.all([ neode.find('User', context.user.id), neode.create('SocialMedia', params), @@ -16,14 +20,14 @@ export default { return response }, - UpdateSocialMedia: async (object, params, context, resolveInfo) => { + UpdateSocialMedia: async (_object, params, _context, _resolveInfo) => { const socialMedia = await neode.find('SocialMedia', params.id) await socialMedia.update({ url: params.url }) const response = await socialMedia.toJson() return response }, - DeleteSocialMedia: async (object, { id }, context, resolveInfo) => { + DeleteSocialMedia: async (_object, { id }, _context, _resolveInfo) => { const socialMedia = await neode.find('SocialMedia', id) if (!socialMedia) return null await socialMedia.delete() diff --git a/backend/src/schema/resolvers/statistics.spec.ts b/backend/src/schema/resolvers/statistics.spec.ts index 4c8c8aa01..9d68b611f 100644 --- a/backend/src/schema/resolvers/statistics.spec.ts +++ b/backend/src/schema/resolvers/statistics.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/statistics.ts b/backend/src/schema/resolvers/statistics.ts index 6bf73b0b2..e2b93bbea 100644 --- a/backend/src/schema/resolvers/statistics.ts +++ b/backend/src/schema/resolvers/statistics.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable security/detect-object-injection */ import log from './helpers/databaseLogger' @@ -5,6 +9,7 @@ export default { Query: { statistics: async (_parent, _args, { driver }) => { const session = driver.session() + // eslint-disable-next-line @typescript-eslint/no-explicit-any const counts: any = {} try { const mapping = { diff --git a/backend/src/schema/resolvers/transactions/inviteCodes.ts b/backend/src/schema/resolvers/transactions/inviteCodes.ts index 554b15f86..0381893ad 100644 --- a/backend/src/schema/resolvers/transactions/inviteCodes.ts +++ b/backend/src/schema/resolvers/transactions/inviteCodes.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export async function validateInviteCode(session, inviteCode) { const readTxResultPromise = session.readTransaction(async (txc) => { const result = await txc.run( diff --git a/backend/src/schema/resolvers/userData.spec.ts b/backend/src/schema/resolvers/userData.spec.ts index 1165ec33c..b3cd75694 100644 --- a/backend/src/schema/resolvers/userData.spec.ts +++ b/backend/src/schema/resolvers/userData.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/userData.ts b/backend/src/schema/resolvers/userData.ts index 3cd5f1c01..15c65b59b 100644 --- a/backend/src/schema/resolvers/userData.ts +++ b/backend/src/schema/resolvers/userData.ts @@ -1,6 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export default { Query: { - userData: async (object, args, context, resolveInfo) => { + userData: async (_object, _args, context, _resolveInfo) => { const id = context.user.id const cypher = ` MATCH (user:User { id: $id }) diff --git a/backend/src/schema/resolvers/user_management.spec.ts b/backend/src/schema/resolvers/user_management.spec.ts index 527821856..92832baa2 100644 --- a/backend/src/schema/resolvers/user_management.spec.ts +++ b/backend/src/schema/resolvers/user_management.spec.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable promise/prefer-await-to-callbacks */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -7,7 +12,7 @@ import CONFIG from '@config/index' import { categories } from '@constants/categories' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { loginMutation } from '@graphql/userManagement' +import { loginMutation } from '@graphql/queries/loginMutation' import encode from '@jwt/encode' import createServer, { context } from '@src/server' diff --git a/backend/src/schema/resolvers/user_management.ts b/backend/src/schema/resolvers/user_management.ts index e9376f940..13437e815 100644 --- a/backend/src/schema/resolvers/user_management.ts +++ b/backend/src/schema/resolvers/user_management.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { AuthenticationError } from 'apollo-server' import bcrypt from 'bcryptjs' import { neo4jgraphql } from 'neo4j-graphql-js' @@ -16,7 +23,7 @@ export default { neo4jgraphql(object, { id: context.user.id }, context, resolveInfo), }, Mutation: { - login: async (_, { email, password }, { driver, req, user }) => { + login: async (_, { email, password }, { driver }) => { // if (user && user.id) { // throw new Error('Already logged in.') // } @@ -42,7 +49,7 @@ export default { ) { delete currentUser.encryptedPassword return encode(currentUser) - } else if (currentUser && currentUser.disabled) { + } else if (currentUser?.disabled) { throw new AuthenticationError('Your account has been disabled.') } else { throw new AuthenticationError('Incorrect email address or password.') @@ -51,7 +58,7 @@ export default { session.close() } }, - changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => { + changePassword: async (_, { oldPassword, newPassword }, { user }) => { const currentUser = await neode.find('User', user.id) const encryptedPassword = currentUser.get('encryptedPassword') diff --git a/backend/src/schema/resolvers/users.spec.ts b/backend/src/schema/resolvers/users.spec.ts index 0b14575db..0a74d46d3 100644 --- a/backend/src/schema/resolvers/users.spec.ts +++ b/backend/src/schema/resolvers/users.spec.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -70,6 +75,36 @@ const updateOnlineStatus = gql` } ` +const setTrophyBadgeSelected = gql` + mutation ($slot: Int!, $badgeId: ID!) { + setTrophyBadgeSelected(slot: $slot, badgeId: $badgeId) { + badgeTrophiesCount + badgeTrophiesSelected { + id + } + badgeTrophiesUnused { + id + } + badgeTrophiesUnusedCount + } + } +` + +const resetTrophyBadgesSelected = gql` + mutation { + resetTrophyBadgesSelected { + badgeTrophiesCount + badgeTrophiesSelected { + id + } + badgeTrophiesUnused { + id + } + badgeTrophiesUnusedCount + } + } +` + beforeAll(async () => { await cleanDatabase() @@ -1070,3 +1105,279 @@ describe('updateOnlineStatus', () => { }) }) }) + +describe('setTrophyBadgeSelected', () => { + beforeEach(async () => { + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + const badgeBear = await Factory.build('badge', { + id: 'trophy_bear', + type: 'trophy', + description: 'You earned a Bear', + icon: '/img/badges/trophy_blue_bear.svg', + }) + const badgePanda = await Factory.build('badge', { + id: 'trophy_panda', + type: 'trophy', + description: 'You earned a Panda', + icon: '/img/badges/trophy_blue_panda.svg', + }) + await Factory.build('badge', { + id: 'trophy_rabbit', + type: 'trophy', + description: 'You earned a Rabbit', + icon: '/img/badges/trophy_blue_rabbit.svg', + }) + + await user.relateTo(badgeBear, 'rewarded') + await user.relateTo(badgePanda, 'rewarded') + }) + + describe('not authenticated', () => { + beforeEach(async () => { + authenticatedUser = undefined + }) + + it('throws an error', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_bear' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + it('throws Error when slot is out of bound', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: -1, badgeId: 'trophy_bear' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Invalid slot! There is only 9 badge-slots to fill', + }), + ], + }), + ) + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 9, badgeId: 'trophy_bear' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Invalid slot! There is only 9 badge-slots to fill', + }), + ], + }), + ) + }) + + it('throws Error when badge was not rewarded to user', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_rabbit' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Error: You cannot set badges not rewarded to you.', + }), + ], + }), + ) + }) + + it('throws Error when badge is unknown', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_unknown' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Error: You cannot set badges not rewarded to you.', + }), + ], + }), + ) + }) + + it('returns the user with badges set on slots', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_bear' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + setTrophyBadgeSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'trophy_bear', + }, + null, + null, + null, + null, + null, + null, + null, + null, + ], + badgeTrophiesUnused: [ + { + id: 'trophy_panda', + }, + ], + badgeTrophiesUnusedCount: 1, + }, + }, + }), + ) + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: 'trophy_panda' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + setTrophyBadgeSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'trophy_bear', + }, + null, + null, + null, + null, + { + id: 'trophy_panda', + }, + null, + null, + null, + ], + badgeTrophiesUnused: [], + badgeTrophiesUnusedCount: 0, + }, + }, + }), + ) + }) + }) +}) + +describe('resetTrophyBadgesSelected', () => { + beforeEach(async () => { + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + const badgeBear = await Factory.build('badge', { + id: 'trophy_bear', + type: 'trophy', + description: 'You earned a Bear', + icon: '/img/badges/trophy_blue_bear.svg', + }) + const badgePanda = await Factory.build('badge', { + id: 'trophy_panda', + type: 'trophy', + description: 'You earned a Panda', + icon: '/img/badges/trophy_blue_panda.svg', + }) + await Factory.build('badge', { + id: 'trophy_rabbit', + type: 'trophy', + description: 'You earned a Rabbit', + icon: '/img/badges/trophy_blue_rabbit.svg', + }) + + await user.relateTo(badgeBear, 'rewarded') + await user.relateTo(badgePanda, 'rewarded') + + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_bear' }, + }) + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: 'trophy_panda' }, + }) + }) + + describe('not authenticated', () => { + beforeEach(async () => { + authenticatedUser = undefined + }) + + it('throws an error', async () => { + await expect(mutate({ mutation: resetTrophyBadgesSelected })).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + it('returns the user with no profile badges badges set', async () => { + await expect(mutate({ mutation: resetTrophyBadgesSelected })).resolves.toEqual( + expect.objectContaining({ + data: { + resetTrophyBadgesSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [null, null, null, null, null, null, null, null, null], + badgeTrophiesUnused: [ + { + id: 'trophy_panda', + }, + { + id: 'trophy_bear', + }, + ], + badgeTrophiesUnusedCount: 2, + }, + }, + }), + ) + }) + }) +}) diff --git a/backend/src/schema/resolvers/users.ts b/backend/src/schema/resolvers/users.ts index e93dffbd0..4f1fb6d5b 100644 --- a/backend/src/schema/resolvers/users.ts +++ b/backend/src/schema/resolvers/users.ts @@ -1,6 +1,14 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError, ForbiddenError } from 'apollo-server' import { neo4jgraphql } from 'neo4j-graphql-js' +import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges' import { getNeode } from '@db/neo4j' import log from './helpers/databaseLogger' @@ -42,14 +50,14 @@ export const getBlockedUsers = async (context) => { export default { Query: { - mutedUsers: async (object, args, context, resolveInfo) => { + mutedUsers: async (_object, _args, context, _resolveInfo) => { try { return getMutedUsers(context) } catch (e) { throw new UserInputError(e.message) } }, - blockedUsers: async (object, args, context, resolveInfo) => { + blockedUsers: async (_object, _args, context, _resolveInfo) => { try { return getBlockedUsers(context) } catch (e) { @@ -110,7 +118,7 @@ export default { const unmutedUser = await neode.find('User', params.id) return unmutedUser.toJson() }, - blockUser: async (object, args, context, resolveInfo) => { + blockUser: async (_object, args, context, _resolveInfo) => { const { user: currentUser } = context if (currentUser.id === args.id) return null @@ -137,7 +145,7 @@ export default { session.close() } }, - unblockUser: async (object, args, context, resolveInfo) => { + unblockUser: async (_object, args, context, _resolveInfo) => { const { user: currentUser } = context if (currentUser.id === args.id) return null @@ -215,12 +223,12 @@ export default { session.close() } }, - DeleteUser: async (object, params, context, resolveInfo) => { + DeleteUser: async (_object, params, context, _resolveInfo) => { const { resource, id: userId } = params const session = context.driver.session() const deleteUserTxResultPromise = session.writeTransaction(async (transaction) => { - if (resource && resource.length) { + if (resource?.length) { await Promise.all( resource.map(async (node) => { const txResult = await transaction.run( @@ -282,7 +290,7 @@ export default { session.close() } }, - switchUserRole: async (object, args, context, resolveInfo) => { + switchUserRole: async (_object, args, context, _resolveInfo) => { const { role, id } = args if (context.user.id === id) throw new Error('you-cannot-change-your-own-role') @@ -307,7 +315,7 @@ export default { session.close() } }, - saveCategorySettings: async (object, args, context, resolveInfo) => { + saveCategorySettings: async (_object, args, context, _resolveInfo) => { const { activeCategories } = args const { user: { id }, @@ -350,7 +358,7 @@ export default { session.close() } }, - updateOnlineStatus: async (object, args, context, resolveInfo) => { + updateOnlineStatus: async (_object, args, context, _resolveInfo) => { const { status } = args const { user: { id }, @@ -381,9 +389,76 @@ export default { return true }, + setTrophyBadgeSelected: async (_object, args, context, _resolveInfo) => { + const { slot, badgeId } = args + const { + user: { id: userId }, + } = context + + if (slot >= TROPHY_BADGES_SELECTED_MAX || slot < 0) { + throw new Error( + `Invalid slot! There is only ${TROPHY_BADGES_SELECTED_MAX} badge-slots to fill`, + ) + } + + const session = context.driver.session() + + const query = session.writeTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge {id: $badgeId}) + OPTIONAL MATCH (user)-[badgeRelation:SELECTED]->(badge) + OPTIONAL MATCH (user)-[slotRelation:SELECTED{slot: $slot}]->(:Badge) + DELETE badgeRelation, slotRelation + MERGE (user)-[:SELECTED{slot: toInteger($slot)}]->(badge) + RETURN user {.*} + `, + { userId, badgeId, slot }, + ) + return result.records.map((record) => record.get('user'))[0] + }) + try { + const user = await query + if (!user) { + throw new Error('You cannot set badges not rewarded to you.') + } + return user + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + resetTrophyBadgesSelected: async (_object, _args, context, _resolveInfo) => { + const { + user: { id: userId }, + } = context + + const session = context.driver.session() + + const query = session.writeTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $userId}) + OPTIONAL MATCH (user)-[relation:SELECTED]->(:Badge) + DELETE relation + RETURN user {.*} + `, + { userId }, + ) + return result.records.map((record) => record.get('user'))[0] + }) + try { + return await query + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, }, User: { - emailNotificationSettings: async (parent, params, context, resolveInfo) => { + emailNotificationSettings: async (parent, _params, _context, _resolveInfo) => { return [ { type: 'post', @@ -438,6 +513,87 @@ export default { }, ] }, + badgeTrophiesSelected: async (parent, _params, context, _resolveInfo) => { + const session = context.driver.session() + + const query = session.readTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $parent.id})-[relation:SELECTED]->(badge:Badge) + WITH relation, badge + ORDER BY relation.slot ASC + RETURN relation.slot as slot, badge {.*} + `, + { parent }, + ) + return result.records + }) + try { + const badgesSelected = await query + const result = Array(TROPHY_BADGES_SELECTED_MAX).fill(null) + badgesSelected.map((record) => { + result[record.get('slot')] = record.get('badge') + return true + }) + return result + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + badgeTrophiesUnused: async (_parent, _params, context, _resolveInfo) => { + const { + user: { id: userId }, + } = context + + const session = context.driver.session() + + const query = session.writeTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge) + WHERE NOT (user)-[:SELECTED]-(badge) + RETURN badge {.*} + `, + { userId }, + ) + return result.records.map((record) => record.get('badge')) + }) + try { + return await query + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + badgeTrophiesUnusedCount: async (_parent, _params, context, _resolveInfo) => { + const { + user: { id: userId }, + } = context + + const session = context.driver.session() + + const query = session.writeTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge) + WHERE NOT (user)-[:SELECTED]-(badge) + RETURN toString(COUNT(badge)) as count + `, + { userId }, + ) + return result.records.map((record) => record.get('count'))[0] + }) + try { + return await query + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, ...Resolver('User', { undefinedToNull: [ 'actorId', @@ -471,13 +627,14 @@ export default { '-[:WROTE]->(c:Comment)-[:COMMENTS]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true', shoutedCount: '-[:SHOUTED]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true', - badgesCount: '<-[:REWARDED]-(related:Badge)', + badgeTrophiesCount: '<-[:REWARDED]-(related:Badge)', }, hasOne: { avatar: '-[:AVATAR_IMAGE]->(related:Image)', invitedBy: '<-[:INVITED]-(related:User)', location: '-[:IS_IN]->(related:Location)', redeemedInviteCode: '-[:REDEEMED]->(related:InviteCode)', + badgeVerification: '<-[:VERIFIES]-(related:Badge)', }, hasMany: { followedBy: '<-[:FOLLOWS]-(related:User)', @@ -488,7 +645,7 @@ export default { comments: '-[:WROTE]->(related:Comment)', shouted: '-[:SHOUTED]->(related:Post)', categories: '-[:CATEGORIZED]->(related:Category)', - badges: '<-[:REWARDED]-(related:Badge)', + badgeTrophies: '<-[:REWARDED]-(related:Badge)', inviteCodes: '-[:GENERATED]->(related:InviteCode)', }, }), diff --git a/backend/src/schema/resolvers/users/location.spec.ts b/backend/src/schema/resolvers/users/location.spec.ts index 4f54dcc06..9c3791e35 100644 --- a/backend/src/schema/resolvers/users/location.spec.ts +++ b/backend/src/schema/resolvers/users/location.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/resolvers/users/location.ts b/backend/src/schema/resolvers/users/location.ts index b663eebdf..d32c03cd2 100644 --- a/backend/src/schema/resolvers/users/location.ts +++ b/backend/src/schema/resolvers/users/location.ts @@ -1,3 +1,11 @@ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable promise/avoid-new */ /* eslint-disable promise/prefer-await-to-callbacks */ /* eslint-disable import/no-named-as-default */ @@ -39,8 +47,8 @@ const createLocation = async (session, mapboxData) => { nameRU: mapboxData.text_ru, type: mapboxData.id.split('.')[0].toLowerCase(), address: mapboxData.address, - lng: mapboxData.center && mapboxData.center.length ? mapboxData.center[0] : null, - lat: mapboxData.center && mapboxData.center.length ? mapboxData.center[1] : null, + lng: mapboxData.center?.length ? mapboxData.center[0] : null, + lat: mapboxData.center?.length ? mapboxData.center[1] : null, } let mutation = @@ -87,7 +95,7 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s debug(res) - if (!res || !res.features || !res.features[0]) { + if (!res?.features?.[0]) { throw new UserInputError('locationName is invalid') } @@ -102,7 +110,7 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s data = res.features[0] } - if (!data || !data.place_type || !data.place_type.length) { + if (!data?.place_type?.length) { throw new UserInputError('locationName is invalid') } @@ -164,7 +172,7 @@ export const queryLocations = async ({ place, lang }) => { `https://api.mapbox.com/geocoding/v5/mapbox.places/${place}.json?access_token=${CONFIG.MAPBOX_TOKEN}&types=region,place,country&language=${lang}`, ) // Return empty array if no location found or error occurred - if (!res || !res.features) { + if (!res?.features) { return [] } return res.features diff --git a/backend/src/schema/resolvers/users/mutedUsers.spec.ts b/backend/src/schema/resolvers/users/mutedUsers.spec.ts index 1fda2b392..455672199 100644 --- a/backend/src/schema/resolvers/users/mutedUsers.spec.ts +++ b/backend/src/schema/resolvers/users/mutedUsers.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -60,6 +64,7 @@ describe('mutedUsers', () => { it('throws permission error', async () => { const { query } = createTestClient(server) const result = await query({ query: mutedUserQuery }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expect(result.errors![0]).toHaveProperty('message', 'Not Authorized!') }) diff --git a/backend/src/schema/resolvers/viewedTeaserCount.spec.ts b/backend/src/schema/resolvers/viewedTeaserCount.spec.ts index ebcb19c4e..9fb8a7eb9 100644 --- a/backend/src/schema/resolvers/viewedTeaserCount.spec.ts +++ b/backend/src/schema/resolvers/viewedTeaserCount.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' diff --git a/backend/src/schema/types/type/Badge.gql b/backend/src/schema/types/type/Badge.gql deleted file mode 100644 index dff1de89a..000000000 --- a/backend/src/schema/types/type/Badge.gql +++ /dev/null @@ -1,29 +0,0 @@ -type Badge { - id: ID! - type: BadgeType! - status: BadgeStatus! - icon: String! - createdAt: String - updatedAt: String - - rewarded: [User]! @relation(name: "REWARDED", direction: "OUT") -} - -enum BadgeStatus { - permanent - temporary -} - -enum BadgeType { - role - crowdfunding -} - -type Query { - Badge: [Badge] -} - -type Mutation { - reward(badgeKey: ID!, userId: ID!): User - unreward(badgeKey: ID!, userId: ID!): User -} diff --git a/backend/src/server.ts b/backend/src/server.ts index 117e0c3b6..372ec964b 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable import/no-named-as-default-member */ import http from 'node:http' @@ -68,7 +74,7 @@ const createServer = (options?) => { context, schema: middleware(schema), subscriptions: { - onConnect: (connectionParams, webSocket) => { + onConnect: (connectionParams, _webSocket) => { return getContext(connectionParams) }, }, diff --git a/backend/yarn.lock b/backend/yarn.lock index 80449c983..3f0aa3990 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1091,10 +1091,10 @@ dependencies: tslib "^2.4.0" -"@eslint-community/eslint-plugin-eslint-comments@^4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.4.1.tgz#dbfab6f2447c22be8758a0a9a9c80e56d2e2b93f" - integrity sha512-lb/Z/MzbTf7CaVYM9WCFNQZ4L1yi3ev2fsFPF99h31ljhSEyUoyEsKsNWiU+qD1glbYTDJdqgyaLKtyTkkqtuQ== +"@eslint-community/eslint-plugin-eslint-comments@^4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.5.0.tgz#4ffa576583bd99dfbaf74c893635e2c76acba048" + integrity sha512-MAhuTKlr4y/CE3WYX26raZjy+I/kS2PLKSzvfmDCGrBLTFHOYwqROZdr4XwPgXwX3K9rjzMr4pSmUWGnzsUyMg== dependencies: escape-string-regexp "^4.0.0" ignore "^5.2.4" @@ -1143,10 +1143,10 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== -"@faker-js/faker@9.6.0": - version "9.6.0" - resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.6.0.tgz#64235d20330b142eef3d1d1638ba56c083b4bf1d" - integrity sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw== +"@faker-js/faker@9.7.0": + version "9.7.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.7.0.tgz#1cf1fecfcad5e2da2332140bf3b5f23cc1c2a7f4" + integrity sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg== "@graphql-toolkit/common@0.10.4": version "0.10.4" @@ -1579,7 +1579,7 @@ url-regex "~4.1.1" video-extensions "~1.1.0" -"@napi-rs/wasm-runtime@^0.2.7": +"@napi-rs/wasm-runtime@^0.2.8": version "0.2.8" resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.8.tgz#642e8390ee78ed21d6b79c467aa610e249224ed6" integrity sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg== @@ -2289,82 +2289,87 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@unrs/resolver-binding-darwin-arm64@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.3.3.tgz#394065916f98cdc1897cf7234adfdee395725fa8" - integrity sha512-EpRILdWr3/xDa/7MoyfO7JuBIJqpBMphtu4+80BK1bRfFcniVT74h3Z7q1+WOc92FuIAYatB1vn9TJR67sORGw== +"@unrs/resolver-binding-darwin-arm64@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.5.0.tgz#0c64ebe422a3d05ada91d8ba84e037383742c955" + integrity sha512-YmocNlEcX/AgJv8gI41bhjMOTcKcea4D2nRIbZj+MhRtSH5+vEU8r/pFuTuoF+JjVplLsBueU+CILfBPVISyGQ== -"@unrs/resolver-binding-darwin-x64@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.3.3.tgz#6a3c75ca984342261c7346db53293b0002e8cde1" - integrity sha512-ntj/g7lPyqwinMJWZ+DKHBse8HhVxswGTmNgFKJtdgGub3M3zp5BSZ3bvMP+kBT6dnYJLSVlDqdwOq1P8i0+/g== +"@unrs/resolver-binding-darwin-x64@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.5.0.tgz#57210874eca22ec3a07039c97c028fb19c0c6d57" + integrity sha512-qpUrXgH4e/0xu1LOhPEdfgSY3vIXOxDQv370NEL8npN8h40HcQDA+Pl2r4HBW6tTXezWIjxUFcP7tj529RZtDw== -"@unrs/resolver-binding-freebsd-x64@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.3.3.tgz#6532b8d4fecaca6c4424791c82f7a27aac94fcd5" - integrity sha512-l6BT8f2CU821EW7U8hSUK8XPq4bmyTlt9Mn4ERrfjJNoCw0/JoHAh9amZZtV3cwC3bwwIat+GUnrcHTG9+qixw== +"@unrs/resolver-binding-freebsd-x64@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.5.0.tgz#4519371d0ad8e557a86623d8497e3abcdcb5ae43" + integrity sha512-3tX8r8vgjvZzaJZB4jvxUaaFCDCb3aWDCpZN3EjhGnnwhztslI05KSG5NY/jNjlcZ5QWZ7dEZZ/rNBFsmTaSPw== -"@unrs/resolver-binding-linux-arm-gnueabihf@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.3.3.tgz#69a8e430095fcf6a76f7350cc27b83464f8cbb91" - integrity sha512-8ScEc5a4y7oE2BonRvzJ+2GSkBaYWyh0/Ko4Q25e/ix6ANpJNhwEPZvCR6GVRmsQAYMIfQvYLdM6YEN+qRjnAQ== +"@unrs/resolver-binding-linux-arm-gnueabihf@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.5.0.tgz#4fc05aec9e65a6478003a0b9034a06ac0da886ab" + integrity sha512-FH+ixzBKaUU9fWOj3TYO+Yn/eO6kYvMLV9eNJlJlkU7OgrxkCmiMS6wUbyT0KA3FOZGxnEQ2z3/BHgYm2jqeLA== -"@unrs/resolver-binding-linux-arm-musleabihf@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.3.3.tgz#e1fc8440e54929b1f0f6aff6f6e3e9e19ac4a73c" - integrity sha512-8qQ6l1VTzLNd3xb2IEXISOKwMGXDCzY/UNy/7SovFW2Sp0K3YbL7Ao7R18v6SQkLqQlhhqSBIFRk+u6+qu5R5A== +"@unrs/resolver-binding-linux-arm-musleabihf@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.5.0.tgz#c24b35dd5818fcd25569425b1dc1a98a883e248b" + integrity sha512-pxCgXMgwB/4PfqFQg73lMhmWwcC0j5L+dNXhZoz/0ek0iS/oAWl65fxZeT/OnU7fVs52MgdP2q02EipqJJXHSg== -"@unrs/resolver-binding-linux-arm64-gnu@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.3.3.tgz#1249e18b5fa1419addda637d62ef201ce9bcf5a4" - integrity sha512-v81R2wjqcWXJlQY23byqYHt9221h4anQ6wwN64oMD/WAE+FmxPHFZee5bhRkNVtzqO/q7wki33VFWlhiADwUeQ== +"@unrs/resolver-binding-linux-arm64-gnu@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.5.0.tgz#07dc8478a0a356d343790208dc557d6d053689af" + integrity sha512-FX2FV7vpLE/+Z0NZX9/1pwWud5Wocm/2PgpUXbT5aSV3QEB10kBPJAzssOQylvdj8mOHoKl5pVkXpbCwww/T2g== -"@unrs/resolver-binding-linux-arm64-musl@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.3.3.tgz#9af549ce9dde57b31c32a36cbe9eafa05f96befd" - integrity sha512-cAOx/j0u5coMg4oct/BwMzvWJdVciVauUvsd+GQB/1FZYKQZmqPy0EjJzJGbVzFc6gbnfEcSqvQE6gvbGf2N8Q== +"@unrs/resolver-binding-linux-arm64-musl@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.5.0.tgz#169e531731f7e462dffa410034a1d06a7a921aa8" + integrity sha512-+gF97xst1BZb28T3nwwzEtq2ewCoMDGKsenYsZuvpmNrW0019G1iUAunZN+FG55L21y+uP7zsGX06OXDQ/viKw== -"@unrs/resolver-binding-linux-ppc64-gnu@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.3.3.tgz#45aab52319f3e3b2627038a80c0331b0793a4be3" - integrity sha512-mq2blqwErgDJD4gtFDlTX/HZ7lNP8YCHYFij2gkXPtMzrXxPW1hOtxL6xg4NWxvnj4bppppb0W3s/buvM55yfg== +"@unrs/resolver-binding-linux-ppc64-gnu@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.5.0.tgz#f6ad2ff47d74c8158b28a18536a71a8ecf84a17f" + integrity sha512-5bEmVcQw9js8JYM2LkUBw5SeELSIxX+qKf9bFrfFINKAp4noZ//hUxLpbF7u/3gTBN1GsER6xOzIZlw/VTdXtA== -"@unrs/resolver-binding-linux-s390x-gnu@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.3.3.tgz#7d2fe5c43e291d42e66d74fce07d9cf0050b4241" - integrity sha512-u0VRzfFYysarYHnztj2k2xr+eu9rmgoTUUgCCIT37Nr+j0A05Xk2c3RY8Mh5+DhCl2aYibihnaAEJHeR0UOFIQ== +"@unrs/resolver-binding-linux-riscv64-gnu@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.5.0.tgz#2f3986cb44f285f90d27e87cee8b4059de3ffbdd" + integrity sha512-GGk/8TPUsf1Q99F+lzMdjE6sGL26uJCwQ9TlvBs8zR3cLQNw/MIumPN7zrs3GFGySjnwXc8gA6J3HKbejywmqA== -"@unrs/resolver-binding-linux-x64-gnu@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.3.3.tgz#be54ff88c581610c42d8614475c0560f043d7ded" - integrity sha512-OrVo5ZsG29kBF0Ug95a2KidS16PqAMmQNozM6InbquOfW/udouk063e25JVLqIBhHLB2WyBnixOQ19tmeC/hIg== +"@unrs/resolver-binding-linux-s390x-gnu@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.5.0.tgz#813ea07833012bc34ecc59f023e422b421138761" + integrity sha512-5uRkFYYVNAeVaA4W/CwugjFN3iDOHCPqsBLCCOoJiMfFMMz4evBRsg+498OFa9w6VcTn2bD5aI+RRayaIgk2Sw== -"@unrs/resolver-binding-linux-x64-musl@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.3.3.tgz#4efa7a1e4f7bf231098ed23df1e19174d360c24f" - integrity sha512-PYnmrwZ4HMp9SkrOhqPghY/aoL+Rtd4CQbr93GlrRTjK6kDzfMfgz3UH3jt6elrQAfupa1qyr1uXzeVmoEAxUA== +"@unrs/resolver-binding-linux-x64-gnu@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.5.0.tgz#18b0d7553268fa490db92be578ac4b0fd8cae049" + integrity sha512-j905CZH3nehYy6NimNqC2B14pxn4Ltd7guKMyPTzKehbFXTUgihQS/ZfHQTdojkMzbSwBOSgq1dOrY+IpgxDsA== -"@unrs/resolver-binding-wasm32-wasi@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.3.3.tgz#6df454b4a9b28d47850bcb665d243f09101b782c" - integrity sha512-81AnQY6fShmktQw4hWDUIilsKSdvr/acdJ5azAreu2IWNlaJOKphJSsUVWE+yCk6kBMoQyG9ZHCb/krb5K0PEA== +"@unrs/resolver-binding-linux-x64-musl@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.5.0.tgz#04541e98d16e358c695393251e365bc3d802dfa4" + integrity sha512-dmLevQTuzQRwu5A+mvj54R5aye5I4PVKiWqGxg8tTaYP2k2oTs/3Mo8mgnhPk28VoYCi0fdFYpgzCd4AJndQvQ== + +"@unrs/resolver-binding-wasm32-wasi@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.5.0.tgz#7a2ae7467c4c52d53c20ad7fc2bace1b23de8168" + integrity sha512-LtJMhwu7avhoi+kKfAZOKN773RtzLBVVF90YJbB0wyMpUj9yQPeA+mteVUI9P70OG/opH47FeV5AWeaNWWgqJg== dependencies: - "@napi-rs/wasm-runtime" "^0.2.7" + "@napi-rs/wasm-runtime" "^0.2.8" -"@unrs/resolver-binding-win32-arm64-msvc@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.3.3.tgz#fb19e118350e1392993a0a6565b427d38c1c1760" - integrity sha512-X/42BMNw7cW6xrB9syuP5RusRnWGoq+IqvJO8IDpp/BZg64J1uuIW6qA/1Cl13Y4LyLXbJVYbYNSKwR/FiHEng== +"@unrs/resolver-binding-win32-arm64-msvc@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.5.0.tgz#11deb282b8ce73fab26f1d04df0fa4d6363752c2" + integrity sha512-FTZBxLL4SO1mgIM86KykzJmPeTPisBDHQV6xtfDXbTMrentuZ6SdQKJUV5BWaoUK3p8kIULlrCcucqdCnk8Npg== -"@unrs/resolver-binding-win32-ia32-msvc@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.3.3.tgz#23a9c4b5621bba2d472bc78fadde7273a8c4548d" - integrity sha512-EGNnNGQxMU5aTN7js3ETYvuw882zcO+dsVjs+DwO2j/fRVKth87C8e2GzxW1L3+iWAXMyJhvFBKRavk9Og1Z6A== +"@unrs/resolver-binding-win32-ia32-msvc@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.5.0.tgz#2a5d414912379425bd395ea15901a5dd5febc7c1" + integrity sha512-i5bB7vJ1waUsFciU/FKLd4Zw0VnAkvhiJ4//jYQXyDUuiLKodmtQZVTcOPU7pp97RrNgCFtXfC1gnvj/DHPJTw== -"@unrs/resolver-binding-win32-x64-msvc@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.3.3.tgz#eee226e5b4c4d91c862248afd24452c8698ed542" - integrity sha512-GraLbYqOJcmW1qY3osB+2YIiD62nVf2/bVLHZmrb4t/YSUwE03l7TwcDJl08T/Tm3SVhepX8RQkpzWbag/Sb4w== +"@unrs/resolver-binding-win32-x64-msvc@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.5.0.tgz#5768c6bba4a27833a48a8a77e50eb01b520d0962" + integrity sha512-wAvXp4k7jhioi4SebXW/yfzzYwsUCr9kIX4gCsUFKpCTUf8Mi7vScJXI3S+kupSUf0LbVHudR8qBbe2wFMSNUw== "@wry/context@^0.4.0": version "0.4.4" @@ -3159,10 +3164,10 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -bcryptjs@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" - integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= +bcryptjs@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-3.0.2.tgz#caadcca1afefe372ed6e20f86db8e8546361c1ca" + integrity sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog== binary-extensions@^2.0.0: version "2.0.0" @@ -3999,11 +4004,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -denque@^1.1.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" - integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== - denque@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" @@ -4600,17 +4600,17 @@ eslint-import-resolver-node@^0.3.9: is-core-module "^2.13.0" resolve "^1.22.4" -eslint-import-resolver-typescript@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.3.1.tgz#6721c639716de3685363ddb284e2cec60cee60ee" - integrity sha512-/dR9YMomeBlvfuvX5q0C3Y/2PHC9OCRdT2ijFwdfq/4Bq+4m5/lqstEp9k3P6ocha1pCbhoY9fkwVYLmOqR0VQ== +eslint-import-resolver-typescript@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.3.2.tgz#1d2371be6d073bade177ee04a4548dbacdc334c0" + integrity sha512-T2LqBXj87ndEC9t1LrDiPkzalSFzD4rrXr6BTzGdgMx1jdQM4T972guQvg7Ih+LNO51GURXI/qMHS5GF3h1ilw== dependencies: debug "^4.4.0" get-tsconfig "^4.10.0" is-bun-module "^2.0.0" stable-hash "^0.0.5" tinyglobby "^0.2.12" - unrs-resolver "^1.3.3" + unrs-resolver "^1.4.1" eslint-module-utils@^2.12.0: version "2.12.0" @@ -5637,6 +5637,16 @@ graphql-upload@^11.0.0, graphql-upload@^8.0.2: isobject "^4.0.0" object-path "^0.11.4" +graphql-upload@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-13.0.0.tgz#1a255b64d3cbf3c9f9171fa62a8fb0b9b59bb1d9" + integrity sha512-YKhx8m/uOtKu4Y1UzBFJhbBGJTlk7k4CydlUUiNrtxnwZv0WigbRHP+DVhRNKt7u7DXOtcKZeYJlGtnMXvreXA== + dependencies: + busboy "^0.3.1" + fs-capacitor "^6.2.0" + http-errors "^1.8.1" + object-path "^0.11.8" + graphql@^14.2.1, graphql@^14.5.3, graphql@^14.6.0: version "14.6.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.6.0.tgz#57822297111e874ea12f5cd4419616930cd83e49" @@ -5846,16 +5856,16 @@ http-errors@2.0.0, http-errors@^2.0.0: statuses "2.0.1" toidentifier "1.0.1" -http-errors@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== +http-errors@^1.7.3, http-errors@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== dependencies: depd "~1.1.2" inherits "2.0.4" - setprototypeof "1.1.1" + setprototypeof "1.2.0" statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" + toidentifier "1.0.1" http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.2: version "7.0.2" @@ -5932,12 +5942,7 @@ ignore-by-default@^1.0.1: resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= -ignore@^5.1.4, ignore@^5.2.0, ignore@^5.2.4: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" - integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== - -ignore@^5.3.2: +ignore@^5.1.4, ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== @@ -6027,25 +6032,10 @@ internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" -ioredis@^4.16.1: - version "4.16.1" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.16.1.tgz#377c21d2a4fa8cc31fe9028c666f8dd16a6255bf" - integrity sha512-g76Mm9dE7BLuewncu1MimGZw5gDDjDwjoRony/VoSxSJEKAhuYncDEwYKYjtHi2NWsTNIB6XXRjE64uVa/wpKQ== - dependencies: - cluster-key-slot "^1.1.0" - debug "^4.1.1" - denque "^1.1.0" - lodash.defaults "^4.2.0" - lodash.flatten "^4.4.0" - redis-commands "1.5.0" - redis-errors "^1.2.0" - redis-parser "^3.0.0" - standard-as-callback "^2.0.1" - -ioredis@^5.3.2: - version "5.4.2" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.4.2.tgz#ebb6f1a10b825b2c0fb114763d7e82114a0bee6c" - integrity sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg== +ioredis@^5.3.2, ioredis@^5.6.1: + version "5.6.1" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.6.1.tgz#1ed7dc9131081e77342503425afceaf7357ae599" + integrity sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA== dependencies: "@ioredis/commands" "^1.1.1" cluster-key-slot "^1.1.0" @@ -7129,11 +7119,6 @@ lodash.defaults@^4.2.0: resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= - lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -7868,10 +7853,10 @@ nodemailer-html-to-text@^3.2.0: dependencies: html-to-text "7.1.1" -nodemailer@^6.10.0: - version "6.10.0" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.10.0.tgz#1f24c9de94ad79c6206f66d132776b6503003912" - integrity sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA== +nodemailer@^6.10.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.10.1.tgz#cbc434c54238f83a51c07eabd04e2b3e832da623" + integrity sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA== nodemon@~3.1.9: version "3.1.9" @@ -7991,10 +7976,10 @@ object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object-path@^0.11.4: - version "0.11.5" - resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.5.tgz#d4e3cf19601a5140a55a16ad712019a9c50b577a" - integrity sha512-jgSbThcoR/s+XumvGMTMf81QVBmah+/Q7K7YduKeKVWL7N111unR2d6pZZarSk6kY/caeNxUDyxOvMWyzoU2eg== +object-path@^0.11.4, object-path@^0.11.8: + version "0.11.8" + resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.8.tgz#ed002c02bbdd0070b78a27455e8ae01fc14d4742" + integrity sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA== object.assign@^4.1.0: version "4.1.0" @@ -8604,11 +8589,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -redis-commands@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785" - integrity sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg== - redis-errors@^1.0.0, redis-errors@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" @@ -8925,10 +8905,10 @@ safe-regex@^2.1.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sanitize-html@~2.15.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.15.0.tgz#8e7f97ee1fecdec1bb1fb2e37f6d2c6acfdbabe3" - integrity sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA== +sanitize-html@~2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.16.0.tgz#2b9973b63fa42e3580020499cbda1d894b3642bc" + integrity sha512-0s4caLuHHaZFVxFTG74oW91+j6vW7gKbGD6CD2+miP73CE6z6YtOBN0ArtLd2UGyi4IC7K47v3ENUbQX4jV3Mg== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" @@ -9066,11 +9046,6 @@ set-function-name@^2.0.1: functions-have-names "^1.2.3" has-property-descriptors "^1.0.2" -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -9318,11 +9293,6 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" -standard-as-callback@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.0.1.tgz#ed8bb25648e15831759b6023bdb87e6b60b38126" - integrity sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg== - standard-as-callback@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" @@ -9672,11 +9642,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - toidentifier@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" @@ -9773,10 +9738,10 @@ ts-invariant@^0.4.0: dependencies: tslib "^1.9.3" -ts-jest@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.3.1.tgz#2e459e1f94a833bd8216ba4b045fac948e265937" - integrity sha512-FT2PIRtZABwl6+ZCry8IY7JZ3xMuppsEV9qFVHOVe8jDzggwUZ9TsM4chyJxL9yi6LvkqcZYU3LmapEE454zBQ== +ts-jest@^29.3.2: + version "29.3.2" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.3.2.tgz#0576cdf0a507f811fe73dcd16d135ce89f8156cb" + integrity sha512-bJJkrWc6PjFVz5g2DGCNUo8z7oFEYaz1xP1NpeDU7KNLMWPpEyV8Chbpkn8xjzgRDpQhnGMyvyldoL7h8JXyug== dependencies: bs-logger "^0.2.6" ejs "^3.1.10" @@ -9786,7 +9751,7 @@ ts-jest@^29.3.1: lodash.memoize "^4.1.2" make-error "^1.3.6" semver "^7.7.1" - type-fest "^4.38.0" + type-fest "^4.39.1" yargs-parser "^21.1.1" ts-node@^10.9.2: @@ -9808,10 +9773,10 @@ ts-node@^10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tsc-alias@^1.8.14: - version "1.8.14" - resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.8.14.tgz#cbe9566bcb4b014c32d0704ac495ab3361436edc" - integrity sha512-abPL5KpLkZCR06QkgyOBaswNPgNL/Ub/am16tvQ0kTsmPx3FEhBOAsvIPUU8OkYrLv0JlzJEaJ1r6XkJBIQvYg== +tsc-alias@^1.8.15: + version "1.8.15" + resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.8.15.tgz#7a07a77a4157872f834841a2a1647fad9464884d" + integrity sha512-yKLVx8ddUurRwhVcS6JFF2ZjksOX2ZWDRIdgt+PQhJBDegIdAdilptiHsuAbx9UFxa16GFrxeKQ2kTcGvR6fkQ== dependencies: chokidar "^3.5.3" commander "^9.0.0" @@ -9896,7 +9861,7 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -type-fest@^4.38.0: +type-fest@^4.39.1: version "4.39.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.39.1.tgz#7521f6944e279abaf79cf60cfbc4823f4858083e" integrity sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w== @@ -10085,26 +10050,27 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= -unrs-resolver@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.3.3.tgz#46bd5dd2ecc650365e050055fc208b5f4ae57803" - integrity sha512-PFLAGQzYlyjniXdbmQ3dnGMZJXX5yrl2YS4DLRfR3BhgUsE1zpRIrccp9XMOGRfIHpdFvCn/nr5N1KMVda4x3A== +unrs-resolver@^1.4.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.5.0.tgz#d0a608f08321d8e90ba8eb10a3240e7995997275" + integrity sha512-6aia3Oy7SEe0MuUGQm2nsyob0L2+g57w178K5SE/3pvSGAIp28BB2O921fKx424Ahc/gQ6v0DXFbhcpyhGZdOA== optionalDependencies: - "@unrs/resolver-binding-darwin-arm64" "1.3.3" - "@unrs/resolver-binding-darwin-x64" "1.3.3" - "@unrs/resolver-binding-freebsd-x64" "1.3.3" - "@unrs/resolver-binding-linux-arm-gnueabihf" "1.3.3" - "@unrs/resolver-binding-linux-arm-musleabihf" "1.3.3" - "@unrs/resolver-binding-linux-arm64-gnu" "1.3.3" - "@unrs/resolver-binding-linux-arm64-musl" "1.3.3" - "@unrs/resolver-binding-linux-ppc64-gnu" "1.3.3" - "@unrs/resolver-binding-linux-s390x-gnu" "1.3.3" - "@unrs/resolver-binding-linux-x64-gnu" "1.3.3" - "@unrs/resolver-binding-linux-x64-musl" "1.3.3" - "@unrs/resolver-binding-wasm32-wasi" "1.3.3" - "@unrs/resolver-binding-win32-arm64-msvc" "1.3.3" - "@unrs/resolver-binding-win32-ia32-msvc" "1.3.3" - "@unrs/resolver-binding-win32-x64-msvc" "1.3.3" + "@unrs/resolver-binding-darwin-arm64" "1.5.0" + "@unrs/resolver-binding-darwin-x64" "1.5.0" + "@unrs/resolver-binding-freebsd-x64" "1.5.0" + "@unrs/resolver-binding-linux-arm-gnueabihf" "1.5.0" + "@unrs/resolver-binding-linux-arm-musleabihf" "1.5.0" + "@unrs/resolver-binding-linux-arm64-gnu" "1.5.0" + "@unrs/resolver-binding-linux-arm64-musl" "1.5.0" + "@unrs/resolver-binding-linux-ppc64-gnu" "1.5.0" + "@unrs/resolver-binding-linux-riscv64-gnu" "1.5.0" + "@unrs/resolver-binding-linux-s390x-gnu" "1.5.0" + "@unrs/resolver-binding-linux-x64-gnu" "1.5.0" + "@unrs/resolver-binding-linux-x64-musl" "1.5.0" + "@unrs/resolver-binding-wasm32-wasi" "1.5.0" + "@unrs/resolver-binding-win32-arm64-msvc" "1.5.0" + "@unrs/resolver-binding-win32-ia32-msvc" "1.5.0" + "@unrs/resolver-binding-win32-x64-msvc" "1.5.0" update-browserslist-db@^1.1.0: version "1.1.0" diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 000000000..5871e601c --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +v20.12.1 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0b708bace..4174bf891 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ocelot-social", - "version": "3.2.1", + "version": "3.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ocelot-social", - "version": "3.2.1", + "version": "3.3.0", "license": "MIT", "devDependencies": { "@babel/core": "^7.26.10", @@ -15,14 +15,14 @@ "@badeball/cypress-cucumber-preprocessor": "^22.0.1", "@cucumber/cucumber": "11.2.0", "@cypress/browserify-preprocessor": "^3.0.2", - "@faker-js/faker": "9.6.0", + "@faker-js/faker": "9.7.0", "auto-changelog": "^2.5.0", - "bcryptjs": "^2.4.3", + "bcryptjs": "^3.0.2", "cross-env": "^7.0.3", - "cypress": "^14.2.1", + "cypress": "^14.3.1", "cypress-network-idle": "^1.15.0", "date-fns": "^3.6.0", - "dotenv": "^16.4.7", + "dotenv": "^16.5.0", "expect": "^29.6.4", "graphql-request": "^2.0.0", "import": "^0.0.6", @@ -2786,9 +2786,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.6.0.tgz", - "integrity": "sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw==", + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.7.0.tgz", + "integrity": "sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==", "dev": true, "funding": [ { @@ -6597,10 +6597,14 @@ } }, "node_modules/bcryptjs": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", - "dev": true + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -7758,9 +7762,9 @@ "optional": true }, "node_modules/cypress": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.2.1.tgz", - "integrity": "sha512-5xd0E7fUp0pjjib1D7ljkmCwFDgMkWuW06jWiz8dKrI7MNRrDo0C65i4Sh+oZ9YHjMHZRJBR0XZk1DfekOhOUw==", + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.3.1.tgz", + "integrity": "sha512-/2q06qvHMK3PNiadnRW1Je0lJ43gAFPQJUAK2zIxjr22kugtWxVQznTBLVu1AvRH+RP3oWZhCdWqiEi+0NuqCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -8477,10 +8481,11 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index cc97c0399..e86458b61 100644 --- a/package.json +++ b/package.json @@ -39,14 +39,14 @@ "@badeball/cypress-cucumber-preprocessor": "^22.0.1", "@cucumber/cucumber": "11.2.0", "@cypress/browserify-preprocessor": "^3.0.2", - "@faker-js/faker": "9.6.0", + "@faker-js/faker": "9.7.0", "auto-changelog": "^2.5.0", - "bcryptjs": "^2.4.3", + "bcryptjs": "^3.0.2", "cross-env": "^7.0.3", - "cypress": "^14.2.1", + "cypress": "^14.3.1", "cypress-network-idle": "^1.15.0", "date-fns": "^3.6.0", - "dotenv": "^16.4.7", + "dotenv": "^16.5.0", "expect": "^29.6.4", "graphql-request": "^2.0.0", "import": "^0.0.6", diff --git a/webapp/assets/_new/styles/export.scss b/webapp/assets/_new/styles/export.scss index e29c014e2..c8f16b750 100644 --- a/webapp/assets/_new/styles/export.scss +++ b/webapp/assets/_new/styles/export.scss @@ -20,6 +20,10 @@ backgroundColorPrimary: $background-color-primary; colorNeutral30: $color-neutral-30; + + chatSidemenuBg: $chat-sidemenu-bg; + chatSidemenuBackgroundOver: $chat-sidemenu-background-over; + chatSidemenuBackgroundActive: $chat-sidemenu-background-active; chatMessageColor: $chat-message-color; @@ -34,4 +38,8 @@ chatRoomBackgroundCounterBadge: $chat-room-background-counter-badge; chatRoomColorCounterBadge: $chat-room-color-counter-badge; + + chatIconAdd: $chat-icon-add; + chatIconSend: $chat-icon-send; + chatIconEmoji: $chat-icon-emoji; } \ No newline at end of file diff --git a/webapp/assets/_new/styles/tokens.scss b/webapp/assets/_new/styles/tokens.scss index dd3a042d1..578484355 100644 --- a/webapp/assets/_new/styles/tokens.scss +++ b/webapp/assets/_new/styles/tokens.scss @@ -403,10 +403,12 @@ $color-toast-green: $color-success; * @tokens Ribbon Color */ -$color-ribbon-event: $background-color-third; -$color-ribbon-event-active: $background-color-third-active; -$color-ribbon-article: $background-color-secondary; -$color-ribbon-article-active: $background-color-secondary-active; +$color-ribbon-announcement: $background-color-warning; +$color-ribbon-announcement-shadow: color.adjust($color-ribbon-announcement, $lightness: -20%,); +$color-ribbon-article: $background-color-secondary-active; +$color-ribbon-article-shadow: color.adjust($color-ribbon-article, $lightness: -20%); +$color-ribbon-event: $background-color-third-active; +$color-ribbon-event-shadow: color.adjust($color-ribbon-event, $lightness: -20%); /** * @tokens Chat Color @@ -415,10 +417,15 @@ $color-ribbon-article-active: $background-color-secondary-active; $chat-message-bg-me: $color-primary-light; $chat-message-color: $text-color-base; $chat-message-bg-others: $color-neutral-80; -$chat-sidemenu-bg: $color-secondary-active; +$chat-sidemenu-bg: white; +$chat-sidemenu-background-over: '#f6f6f6'; +$chat-sidemenu-background-active: $color-primary-light; $chat-new-message-color: $color-secondary-active; $chat-message-timestamp: $text-color-soft; $chat-message-checkmark-seen: $text-color-secondary; $chat-message-checkmark: $text-color-soft; $chat-room-color-counter-badge: $text-color-inverse; $chat-room-background-counter-badge: $color-secondary; +$chat-icon-add: $color-primary; +$chat-icon-send: $color-primary; +$chat-icon-emoji: $color-primary; diff --git a/webapp/assets/_new/styles/uses.scss b/webapp/assets/_new/styles/uses.scss new file mode 100644 index 000000000..37ad2e69c --- /dev/null +++ b/webapp/assets/_new/styles/uses.scss @@ -0,0 +1,4 @@ +// we can not use the command '@use' in 'tokens.scss' because of the compiler error 'SassError: @use rules must be written before any other rules.' +// therefore we integrate this file at first in 'nuxt.config.js' + +@use 'sass:color'; diff --git a/webapp/components/AvatarMenu/AvatarMenu.vue b/webapp/components/AvatarMenu/AvatarMenu.vue index ac583ed4b..d19f0bf95 100644 --- a/webapp/components/AvatarMenu/AvatarMenu.vue +++ b/webapp/components/AvatarMenu/AvatarMenu.vue @@ -168,10 +168,10 @@ export default { background-color: $color-neutral-90; } .logout-link { - color: $text-color-base; + color: $text-color-danger; padding-top: $space-xx-small; &:hover { - color: $text-color-link-active; + color: color.adjust($text-color-danger, $lightness: -10%); } } } diff --git a/webapp/components/CommentCard/CommentCard.story.js b/webapp/components/CommentCard/CommentCard.story.js index 75078657e..9a5cff436 100644 --- a/webapp/components/CommentCard/CommentCard.story.js +++ b/webapp/components/CommentCard/CommentCard.story.js @@ -32,8 +32,8 @@ const comment = { location: null, badges: [ { - id: 'indiegogo_en_bear', - icon: '/img/badges/indiegogo_en_bear.svg', + id: 'trophy_bear', + icon: '/img/badges/trophy_blue_bear.svg', __typename: 'Badge', }, ], diff --git a/webapp/components/LoginButton/LoginButton.vue b/webapp/components/LoginButton/LoginButton.vue index 39f0fa4ae..c2c154f34 100644 --- a/webapp/components/LoginButton/LoginButton.vue +++ b/webapp/components/LoginButton/LoginButton.vue @@ -40,7 +40,7 @@ export default { background-color: $color-neutral-90; } .login-link { - color: $text-color-base; + color: $text-color-link; padding-top: $space-xx-small; &:hover { color: $text-color-link-active; diff --git a/webapp/components/PostTeaser/PostTeaser.story.js b/webapp/components/PostTeaser/PostTeaser.story.js index e77e85585..41db98c91 100644 --- a/webapp/components/PostTeaser/PostTeaser.story.js +++ b/webapp/components/PostTeaser/PostTeaser.story.js @@ -33,8 +33,8 @@ export const post = { badges: [ { id: 'b4', - key: 'indiegogo_en_bear', - icon: '/img/badges/indiegogo_en_bear.svg', + key: 'trophy_bear', + icon: '/img/badges/trophy_blue_bear.svg', __typename: 'Badge', }, ], diff --git a/webapp/components/Ribbon/index.vue b/webapp/components/Ribbon/index.vue index 8595c0f5d..8011238fb 100644 --- a/webapp/components/Ribbon/index.vue +++ b/webapp/components/Ribbon/index.vue @@ -25,7 +25,7 @@ export default { padding: $size-ribbon $size-ribbon; border-radius: $border-radius-small 0 0 $border-radius-small; color: $color-neutral-100; - background-color: $color-ribbon-article-active; + background-color: $color-ribbon-article; font-size: $font-size-x-small; font-weight: $font-weight-bold; @@ -36,22 +36,23 @@ export default { bottom: -$size-ribbon; border-width: $border-size-large 4px $border-size-large $border-size-large; border-style: solid; - border-color: $color-ribbon-article transparent transparent $color-ribbon-article; + border-color: $color-ribbon-article-shadow transparent transparent $color-ribbon-article-shadow; } &.--pinned { - background-color: $color-warning; + background-color: $color-ribbon-announcement; &::before { - border-color: $color-warning transparent transparent $color-warning; + border-color: $color-ribbon-announcement-shadow transparent transparent + $color-ribbon-announcement-shadow; } } } .eventBg { - background-color: $color-ribbon-event-active; + background-color: $color-ribbon-event; &::before { - border-color: $color-ribbon-event transparent transparent $color-ribbon-event; + border-color: $color-ribbon-event-shadow transparent transparent $color-ribbon-event-shadow; } } diff --git a/webapp/components/UserTeaser/UserTeaser.story.js b/webapp/components/UserTeaser/UserTeaser.story.js index aa8be58ff..1295bf2db 100644 --- a/webapp/components/UserTeaser/UserTeaser.story.js +++ b/webapp/components/UserTeaser/UserTeaser.story.js @@ -41,8 +41,8 @@ export const user = { commentedCount: 3, badges: [ { - id: 'indiegogo_en_bear', - icon: '/img/badges/indiegogo_en_bear.svg', + id: 'trophy_bear', + icon: '/img/badges/trophy_blue_bear.svg', }, ], location: { diff --git a/webapp/components/_new/features/Admin/Badges/BadgesSection.spec.js b/webapp/components/_new/features/Admin/Badges/BadgesSection.spec.js new file mode 100644 index 000000000..8baddc692 --- /dev/null +++ b/webapp/components/_new/features/Admin/Badges/BadgesSection.spec.js @@ -0,0 +1,46 @@ +import { render, fireEvent, screen } from '@testing-library/vue' +import BadgesSection from './BadgesSection.vue' + +const localVue = global.localVue + +const badge1 = { + id: 'badge1', + icon: 'icon1', + type: 'type1', + description: 'description1', + isActive: true, +} +const badge2 = { + id: 'badge2', + icon: 'icon2', + type: 'type1', + description: 'description2', + isActive: false, +} + +describe('Admin/BadgesSection', () => { + let wrapper + + const Wrapper = () => { + return render(BadgesSection, { + localVue, + propsData: { + badges: [badge1, badge2], + }, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.baseElement).toMatchSnapshot() + }) + + it('emits toggleButton', async () => { + const button = screen.getByAltText(badge1.description) + await fireEvent.click(button) + expect(wrapper.emitted().toggleBadge[0][0]).toEqual(badge1) + }) +}) diff --git a/webapp/components/_new/features/Admin/Badges/BadgesSection.vue b/webapp/components/_new/features/Admin/Badges/BadgesSection.vue new file mode 100644 index 000000000..8ff9da7ed --- /dev/null +++ b/webapp/components/_new/features/Admin/Badges/BadgesSection.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/webapp/components/_new/features/Admin/Badges/__snapshots__/BadgesSection.spec.js.snap b/webapp/components/_new/features/Admin/Badges/__snapshots__/BadgesSection.spec.js.snap new file mode 100644 index 000000000..c09a50725 --- /dev/null +++ b/webapp/components/_new/features/Admin/Badges/__snapshots__/BadgesSection.spec.js.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Admin/BadgesSection renders 1`] = ` + +
+
+

+ +

+ +
+ + +
+
+
+ +`; diff --git a/webapp/components/_new/generic/BaseCard/BaseCard.vue b/webapp/components/_new/generic/BaseCard/BaseCard.vue index 86b1a6652..e6fcd5e49 100644 --- a/webapp/components/_new/generic/BaseCard/BaseCard.vue +++ b/webapp/components/_new/generic/BaseCard/BaseCard.vue @@ -64,7 +64,7 @@ export default { } &.--highlight { - border: $border-size-base solid $color-warning; + border: $border-size-base solid $color-ribbon-announcement; } &.--wide-content { diff --git a/webapp/constants/badges.js b/webapp/constants/badges.js new file mode 100644 index 000000000..bccebb39a --- /dev/null +++ b/webapp/constants/badges.js @@ -0,0 +1,2 @@ +// this file is duplicated in `backend/src/constants/badges` and `webapp/constants/badges.js` +export const TROPHY_BADGES_SELECTED_MAX = 9 diff --git a/webapp/constants/chat.js b/webapp/constants/chat.js index c278dfd62..d471b85b8 100644 --- a/webapp/constants/chat.js +++ b/webapp/constants/chat.js @@ -44,9 +44,9 @@ const STYLE = { }, sidemenu: { - background: '#fff', - backgroundHover: '#f6f6f6', - backgroundActive: styleData.colorPrimaryLight, + background: styleData.chatSidemenuBg, + backgroundHover: styleData.chatSidemenuBackgroundOver, + backgroundActive: styleData.chatSidemenuBackgroundActive, colorActive: '#1976d2', borderColorSearch: '#e1e5e8', }, @@ -114,12 +114,12 @@ const STYLE = { }, emoji: { - background: '#fff', + background: 'white', }, icons: { search: '#9ca6af', - add: styleData.colorPrimary, + add: styleData.chatIconAdd, toggle: styleData.colorNeutral30, menu: styleData.colorNeutral30, close: '#9ca6af', @@ -128,9 +128,9 @@ const STYLE = { paperclip: styleData.colorPrimary, closeOutline: '#000', closePreview: '#fff', - send: styleData.colorPrimary, + send: styleData.chatIconSend, sendDisabled: '#9ca6af', - emoji: styleData.colorPrimary, + emoji: styleData.chatIconEmoji, emojiReaction: 'rgba(0, 0, 0, 0.3)', document: styleData.colorPrimary, pencil: '#9e9e9e', diff --git a/webapp/graphql/CommentMutations.js b/webapp/graphql/CommentMutations.js index dd00527be..f413a4496 100644 --- a/webapp/graphql/CommentMutations.js +++ b/webapp/graphql/CommentMutations.js @@ -29,7 +29,7 @@ export default (i18n) => { commentedCount followedByCount followedByCurrentUser - badges { + badgeTrophies { id icon } diff --git a/webapp/graphql/Fragments.js b/webapp/graphql/Fragments.js index 32337230b..d0ad8a0fe 100644 --- a/webapp/graphql/Fragments.js +++ b/webapp/graphql/Fragments.js @@ -26,7 +26,7 @@ export const locationFragment = (lang) => gql` export const badgesFragment = gql` fragment badges on User { - badges { + badgeTrophies { id icon } diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js index 8ad247ad1..147e93c6f 100644 --- a/webapp/graphql/User.js +++ b/webapp/graphql/User.js @@ -90,6 +90,23 @@ export const adminUserQuery = () => { ` } +export const adminUserBadgesQuery = () => { + return gql` + query User($id: ID!) { + User(id: $id) { + id + name + badgeTrophies { + id + } + badgeVerification { + id + } + } + } + ` +} + export const mapUserQuery = (i18n) => { const lang = i18n.locale().toUpperCase() return gql` diff --git a/webapp/graphql/admin/Badges.js b/webapp/graphql/admin/Badges.js new file mode 100644 index 000000000..2c037f2f3 --- /dev/null +++ b/webapp/graphql/admin/Badges.js @@ -0,0 +1,54 @@ +import gql from 'graphql-tag' + +export const queryBadges = () => gql` + query { + Badge { + id + type + icon + description + } + } +` + +export const setVerificationBadge = () => gql` + mutation ($badgeId: ID!, $userId: ID!) { + setVerificationBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + } + badgeTrophies { + id + } + } + } +` + +export const rewardTrophyBadge = () => gql` + mutation ($badgeId: ID!, $userId: ID!) { + rewardTrophyBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + } + badgeTrophies { + id + } + } + } +` + +export const revokeBadge = () => gql` + mutation ($badgeId: ID!, $userId: ID!) { + revokeBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + } + badgeTrophies { + id + } + } + } +` diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 19d0896a9..ce122672d 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -10,6 +10,28 @@ "saveCategories": "Themen speichern" }, "admin": { + "badges": { + "description": "Stelle die verfügbaren Auszeichnungen für diesen Nutzer ein.", + "revokeTrophy": { + "error": "Trophäe konnte nicht widerrufen werden!", + "success": "Trophäe erfolgreich widerrufen" + }, + "revokeVerification": { + "error": "Verifizierung konnte nicht gesetzt werden!", + "success": "Verifizierung erfolgreich widerrufen" + }, + "rewardTrophy": { + "error": "Trophäe konnte nicht vergeben werden!", + "success": "Trophäe erfolgreich vergeben!" + }, + "setVerification": { + "error": "Verifizierung konnte nicht gesetzt werden!", + "success": "Verifizierung erfolgreich gesetzt" + }, + "title": "Auszeichnungen", + "trophyBadges": "Trophäen", + "verificationBadges": "Verifizierungen" + }, "categories": { "categoryName": "Name", "name": "Themen", @@ -68,13 +90,15 @@ "roleChanged": "Rolle erfolgreich geändert!", "table": { "columns": { + "badges": "Auszeichnungen", "createdAt": "Erstellt am", "email": "E-Mail", "name": "Name", "number": "Nr.", "role": "Rolle", "slug": "Alias" - } + }, + "edit": "Bearbeiten" } } }, diff --git a/webapp/locales/en.json b/webapp/locales/en.json index b4c1125f3..f178da549 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -10,6 +10,28 @@ "saveCategories": "Save topics" }, "admin": { + "badges": { + "description": "Configure the available badges for this user", + "revokeTrophy": { + "error": "Trophy could not be revoked!", + "success": "Trophy successfully revoked!" + }, + "revokeVerification": { + "error": "Verification could not be revoked!", + "success": "Verification succesfully revoked" + }, + "rewardTrophy": { + "error": "Trophy could not be rewarded!", + "success": "Trophy successfully rewarded!" + }, + "setVerification": { + "error": "Verification could not be set!", + "success": "Verification successfully set!" + }, + "title": "Badges", + "trophyBadges": "Trophies", + "verificationBadges": "Verifications" + }, "categories": { "categoryName": "Name", "name": "Topics", @@ -68,13 +90,15 @@ "roleChanged": "Role changed successfully!", "table": { "columns": { + "badges": "Badges", "createdAt": "Created at", "email": "E-mail", "name": "Name", "number": "No.", "role": "Role", "slug": "Slug" - } + }, + "edit": "Edit" } } }, diff --git a/webapp/locales/es.json b/webapp/locales/es.json index 7184a327a..31f2cc5f4 100644 --- a/webapp/locales/es.json +++ b/webapp/locales/es.json @@ -10,6 +10,28 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Nombre", "name": "Categorías", @@ -68,13 +90,15 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": "Creado el", "email": "Correo electrónico", "name": "Nombre", "number": "No.", "role": "Rol", "slug": "Alias" - } + }, + "edit": null } } }, diff --git a/webapp/locales/fr.json b/webapp/locales/fr.json index 851743e63..4bbca2b82 100644 --- a/webapp/locales/fr.json +++ b/webapp/locales/fr.json @@ -10,6 +10,28 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Nom", "name": "Catégories", @@ -68,13 +90,15 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": "Créé à", "email": "Mail", "name": "Nom", "number": "Num.", "role": "Rôle", "slug": "Slug" - } + }, + "edit": null } } }, diff --git a/webapp/locales/it.json b/webapp/locales/it.json index 0c693ca43..21bfaa859 100644 --- a/webapp/locales/it.json +++ b/webapp/locales/it.json @@ -10,6 +10,28 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Nome", "name": "Categorie", @@ -68,13 +90,15 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": null, "email": null, "name": null, "number": null, "role": null, "slug": null - } + }, + "edit": null } } }, diff --git a/webapp/locales/nl.json b/webapp/locales/nl.json index 433adf8e8..f67518c21 100644 --- a/webapp/locales/nl.json +++ b/webapp/locales/nl.json @@ -10,6 +10,28 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Naam", "name": "Categorieën", @@ -68,13 +90,15 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": null, "email": null, "name": null, "number": null, "role": null, "slug": null - } + }, + "edit": null } } }, diff --git a/webapp/locales/pl.json b/webapp/locales/pl.json index c0ab9d09c..4c6a96a5f 100644 --- a/webapp/locales/pl.json +++ b/webapp/locales/pl.json @@ -10,6 +10,28 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Nazwa", "name": "Kategorie", @@ -68,13 +90,15 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": null, "email": null, "name": null, "number": null, "role": null, "slug": null - } + }, + "edit": null } } }, diff --git a/webapp/locales/pt.json b/webapp/locales/pt.json index 02f8fb2cc..7d5ad52c1 100644 --- a/webapp/locales/pt.json +++ b/webapp/locales/pt.json @@ -10,6 +10,28 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Nome", "name": "Categorias", @@ -68,13 +90,15 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": "Criado em", "email": "E-mail", "name": "Nome", "number": "N.º", "role": "Função", "slug": "Slug" - } + }, + "edit": null } } }, diff --git a/webapp/locales/ru.json b/webapp/locales/ru.json index ea0279450..3a394d6ff 100644 --- a/webapp/locales/ru.json +++ b/webapp/locales/ru.json @@ -10,6 +10,28 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Имя", "name": "Категории", @@ -68,13 +90,15 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": "Дата создания", "email": "Эл. почта", "name": "Имя", "number": "№", "role": "Роль", "slug": "Алиас" - } + }, + "edit": null } } }, diff --git a/webapp/nuxt.config.js b/webapp/nuxt.config.js index 9adacd4cc..1c963615a 100644 --- a/webapp/nuxt.config.js +++ b/webapp/nuxt.config.js @@ -102,6 +102,7 @@ export default { */ styleResources: { scss: [ + '~assets/_new/styles/uses.scss', styleguideStyles, '~assets/_new/styles/tokens.scss', '~assets/styles/imports/_branding.scss', @@ -206,6 +207,15 @@ export default { 'X-API-TOKEN': CONFIG.BACKEND_TOKEN, }, }, + '/img': { + // make this configurable (nuxt-dotenv) + target: CONFIG.GRAPHQL_URI, + toProxy: true, // cloudflare needs that + headers: { + 'X-UI-Request': true, + 'X-API-TOKEN': CONFIG.BACKEND_TOKEN, + }, + }, }, // Give apollo module options diff --git a/webapp/package.json b/webapp/package.json index 0f3371b95..d18a88408 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -39,7 +39,7 @@ "cross-env": "~7.0.3", "date-fns": "2.22.1", "express": "~5.1.0", - "graphql": "~14.7.0", + "graphql": "~15.10.1", "intersection-observer": "^0.12.0", "jest-serializer-vue": "^3.1.0", "jsonwebtoken": "~9.0.2", @@ -49,7 +49,7 @@ "nuxt": "~2.12.1", "nuxt-dropzone": "^1.0.4", "nuxt-env": "~0.1.0", - "sass": "^1.86.3", + "sass": "1.77.6", "stack-utils": "^2.0.3", "tippy.js": "^4.3.5", "tiptap": "~1.26.6", @@ -74,7 +74,7 @@ "@babel/core": "^7.25.8", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/preset-env": "^7.25.8", - "@faker-js/faker": "9.6.0", + "@faker-js/faker": "9.7.0", "@storybook/addon-a11y": "^8.0.8", "@storybook/addon-actions": "^5.3.21", "@storybook/addon-notes": "^5.3.18", diff --git a/webapp/pages/admin/users/__snapshots__/_id.spec.js.snap b/webapp/pages/admin/users/__snapshots__/_id.spec.js.snap new file mode 100644 index 000000000..2c5ddc686 --- /dev/null +++ b/webapp/pages/admin/users/__snapshots__/_id.spec.js.snap @@ -0,0 +1,104 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`.vue renders 1`] = ` + +
+
+
+
+
+

+ + User1 + - + admin.badges.title + +

+ +

+ admin.badges.description +

+
+ +
+
+

+ admin.badges.verificationBadges +

+ +
+ + +
+
+ +
+

+ admin.badges.trophyBadges +

+ +
+ + +
+
+ + +
+
+
+
+
+ +`; diff --git a/webapp/pages/admin/users/_id.spec.js b/webapp/pages/admin/users/_id.spec.js new file mode 100644 index 000000000..933de58de --- /dev/null +++ b/webapp/pages/admin/users/_id.spec.js @@ -0,0 +1,326 @@ +import { render, fireEvent, screen } from '@testing-library/vue' +import BadgesPage from './_id.vue' + +const localVue = global.localVue + +const availableBadges = [ + { + id: 'verification-badge-1', + icon: 'icon1', + type: 'verification', + description: 'description-v-1', + }, + { + id: 'verification-badge-2', + icon: 'icon2', + type: 'verification', + description: 'description-v-2', + }, + { + id: 'trophy-badge-1', + icon: 'icon3', + type: 'trophy', + description: 'description-t-1', + }, + { + id: 'trophy-badge-2', + icon: 'icon4', + type: 'trophy', + description: 'description-t-2', + }, +] + +const user = { + id: 'user1', + name: 'User1', + badgeVerification: { + id: 'verification-badge-1', + }, + badgeTrophies: [ + { + id: 'trophy-badge-2', + }, + ], +} + +describe('.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn((v) => v), + $apollo: { + User: { + query: jest.fn(), + }, + badges: { + query: jest.fn(), + }, + mutate: jest.fn(), + }, + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + } + }) + const Wrapper = () => { + return render(BadgesPage, { + mocks, + localVue, + data: () => ({ + user, + badges: availableBadges, + }), + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.baseElement).toMatchSnapshot() + }) + + describe('after clicking an inactive verification badge', () => { + let button + beforeEach(() => { + button = screen.getByAltText(availableBadges[1].description) + }) + + describe('and successful server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockResolvedValue({ + data: { + setVerificationBadge: { + id: 'user1', + badgeVerification: { + id: availableBadges[1].id, + }, + badgeTrophies: [], + }, + }, + }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[1].id, + userId: 'user1', + }, + }) + }) + + it('shows success message', async () => { + expect(mocks.$toast.success).toHaveBeenCalledWith('admin.badges.setVerification.success') + }) + }) + + describe('and failed server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockRejectedValue({ message: 'Ouch!' }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[1].id, + userId: 'user1', + }, + }) + }) + + it('shows error message', async () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('admin.badges.setVerification.error') + }) + }) + + describe('after clicking an inactive trophy badge', () => { + let button + beforeEach(() => { + button = screen.getByAltText(availableBadges[2].description) + }) + + describe('and successful server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockResolvedValue({ + data: { + setTrophyBadge: { + id: 'user1', + badgeVerification: null, + badgeTrophies: [ + { + id: availableBadges[2].id, + }, + ], + }, + }, + }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[2].id, + userId: 'user1', + }, + }) + }) + + it('shows success message', async () => { + expect(mocks.$toast.success).toHaveBeenCalledWith('admin.badges.rewardTrophy.success') + }) + }) + + describe('and failed server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockRejectedValue({ message: 'Ouch!' }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[2].id, + userId: 'user1', + }, + }) + }) + + it('shows error message', async () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('admin.badges.rewardTrophy.error') + }) + }) + }) + + describe('after clicking an active verification badge', () => { + let button + beforeEach(() => { + button = screen.getByAltText(availableBadges[0].description) + }) + + describe('and successful server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockResolvedValue({ + data: { + setVerificationBadge: { + id: 'user1', + badgeVerification: null, + badgeTrophies: [], + }, + }, + }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[0].id, + userId: 'user1', + }, + }) + }) + + it('shows success message', async () => { + expect(mocks.$toast.success).toHaveBeenCalledWith( + 'admin.badges.revokeVerification.success', + ) + }) + }) + + describe('and failed server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockRejectedValue({ message: 'Ouch!' }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[0].id, + userId: 'user1', + }, + }) + }) + + it('shows error message', async () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('admin.badges.revokeVerification.error') + }) + }) + }) + }) + + describe('after clicking an active trophy badge', () => { + let button + beforeEach(() => { + button = screen.getByAltText(availableBadges[3].description) + }) + + describe('and successful server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockResolvedValue({ + data: { + setTrophyBadge: { + id: 'user1', + badgeVerification: null, + badgeTrophies: [ + { + id: availableBadges[3].id, + }, + ], + }, + }, + }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[3].id, + userId: 'user1', + }, + }) + }) + + it('shows success message', async () => { + expect(mocks.$toast.success).toHaveBeenCalledWith('admin.badges.revokeTrophy.success') + }) + }) + + describe('and failed server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockRejectedValue({ message: 'Ouch!' }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[3].id, + userId: 'user1', + }, + }) + }) + + it('shows error message', async () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('admin.badges.revokeTrophy.error') + }) + }) + }) +}) diff --git a/webapp/pages/admin/users/_id.vue b/webapp/pages/admin/users/_id.vue new file mode 100644 index 000000000..808e1653a --- /dev/null +++ b/webapp/pages/admin/users/_id.vue @@ -0,0 +1,163 @@ + + + diff --git a/webapp/pages/admin/users.spec.js b/webapp/pages/admin/users/index.spec.js similarity index 99% rename from webapp/pages/admin/users.spec.js rename to webapp/pages/admin/users/index.spec.js index 43c51fb52..8d6b923c5 100644 --- a/webapp/pages/admin/users.spec.js +++ b/webapp/pages/admin/users/index.spec.js @@ -1,6 +1,6 @@ import { mount } from '@vue/test-utils' import Vuex from 'vuex' -import Users from './users.vue' +import Users from './index.vue' const localVue = global.localVue diff --git a/webapp/pages/admin/users.vue b/webapp/pages/admin/users/index.vue similarity index 93% rename from webapp/pages/admin/users.vue rename to webapp/pages/admin/users/index.vue index 44f162c77..24258a57f 100644 --- a/webapp/pages/admin/users.vue +++ b/webapp/pages/admin/users/index.vue @@ -63,6 +63,16 @@ {{ scope.row.role }} + @@ -132,6 +142,10 @@ export default { label: this.$t('admin.users.table.columns.role'), align: 'right', }, + badges: { + label: this.$t('admin.users.table.columns.badges'), + align: 'right', + }, } }, }, diff --git a/webapp/pages/profile/_id/_slug.vue b/webapp/pages/profile/_id/_slug.vue index e60ba1098..382350faf 100644 --- a/webapp/pages/profile/_id/_slug.vue +++ b/webapp/pages/profile/_id/_slug.vue @@ -42,8 +42,8 @@ {{ $t('profile.memberSince') }} {{ user.createdAt | date('MMMM yyyy') }} - - + + diff --git a/webapp/pages/settings.vue b/webapp/pages/settings.vue index 1fce64d8f..5d526c3cc 100644 --- a/webapp/pages/settings.vue +++ b/webapp/pages/settings.vue @@ -5,14 +5,14 @@ - + +
- +
@@ -87,3 +87,24 @@ export default { }, } + + diff --git a/webapp/pages/settings/blocked-users.vue b/webapp/pages/settings/blocked-users.vue index 90519452f..0eed6d370 100644 --- a/webapp/pages/settings/blocked-users.vue +++ b/webapp/pages/settings/blocked-users.vue @@ -75,8 +75,10 @@