diff --git a/backend/.eslintrc.cjs b/backend/.eslintrc.cjs index d214ae761..cff4c1de1 100644 --- a/backend/.eslintrc.cjs +++ b/backend/.eslintrc.cjs @@ -23,7 +23,7 @@ module.exports = { }, 'import/resolver': { typescript: { - project: ['./tsconfig.json'], + project: ['./tsconfig.json', './backend/tsconfig.json'], }, node: true, }, @@ -52,16 +52,16 @@ module.exports = { 'import/no-commonjs': 'error', 'import/no-import-module-exports': 'error', 'import/no-nodejs-modules': 'off', - 'import/unambiguous': 'off', // not compatible with scriptless vue files + 'import/unambiguous': 'off', // not compatible with .eslintrc.cjs 'import/default': 'error', - // 'import/named': 'error', + 'import/named': 'off', // has false positives 'import/namespace': 'error', 'import/no-absolute-path': 'error', 'import/no-cycle': 'error', 'import/no-dynamic-require': 'error', 'import/no-internal-modules': 'off', 'import/no-relative-packages': 'error', - // 'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }], + 'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }], 'import/no-self-import': 'error', 'import/no-unresolved': 'error', 'import/no-useless-path-segments': 'error', @@ -72,36 +72,36 @@ module.exports = { 'import/first': 'error', 'import/group-exports': 'off', 'import/newline-after-import': 'error', - // 'import/no-anonymous-default-export': 'error', - // 'import/no-default-export': 'error', + 'import/no-anonymous-default-export': 'off', // not compatible with neode + 'import/no-default-export': 'off', // not compatible with neode 'import/no-duplicates': 'error', 'import/no-named-default': 'error', 'import/no-namespace': 'error', 'import/no-unassigned-import': 'error', - // 'import/order': [ - // 'error', - // { - // groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], - // 'newlines-between': 'always', - // pathGroups: [ - // { - // pattern: '@?*/**', - // group: 'external', - // position: 'after', - // }, - // { - // pattern: '@/**', - // group: 'external', - // position: 'after', - // }, - // ], - // alphabetize: { - // order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */, - // caseInsensitive: true /* ignore case. Options: [true, false] */, - // }, - // distinctGroup: true, - // }, - // ], + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], + 'newlines-between': 'always', + pathGroups: [ + { + pattern: '@?*/**', + group: 'external', + position: 'after', + }, + { + pattern: '@/**', + group: 'external', + position: 'after', + }, + ], + alphabetize: { + order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */, + caseInsensitive: true /* ignore case. Options: [true, false] */, + }, + distinctGroup: true, + }, + ], 'import/prefer-default-export': 'off', // n @@ -128,7 +128,10 @@ module.exports = { 'n/no-restricted-require': 'error', // 'n/no-sync': 'error', // 'n/no-unpublished-bin': 'error', // part of n/recommended - 'n/no-unpublished-import': ['error', { 'allowModules': ['apollo-server-testing', 'rosie', '@faker-js/faker'] }], // part of n/recommended + 'n/no-unpublished-import': [ + 'error', + { allowModules: ['apollo-server-testing', 'rosie', '@faker-js/faker', 'ts-jest'] }, + ], // part of n/recommended // 'n/no-unpublished-require': 'error', // part of n/recommended // 'n/no-unsupported-features/es-builtins': 'error', // part of n/recommended // 'n/no-unsupported-features/es-syntax': 'error', // part of n/recommended @@ -148,7 +151,7 @@ module.exports = { // promise // 'promise/always-return': 'error', // part of promise/recommended - 'promise/avoid-new': 'error', + 'promise/avoid-new': 'error', // 'promise/catch-or-return': 'error', // part of promise/recommended // 'promise/no-callback-in-promise': 'warn', // part of promise/recommended 'promise/no-multiple-resolved': 'error', @@ -163,7 +166,7 @@ module.exports = { 'promise/prefer-catch': 'error', 'promise/spec-only': 'error', // 'promise/valid-params': 'error', // part of promise/recommended - + // eslint comments '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }], '@eslint-community/eslint-comments/no-restricted-disable': 'error', diff --git a/backend/jest.config.cjs b/backend/jest.config.cjs new file mode 100644 index 000000000..8d322ff08 --- /dev/null +++ b/backend/jest.config.cjs @@ -0,0 +1,26 @@ +/* eslint-disable import/no-commonjs */ +const { pathsToModuleNameMapper } = require('ts-jest') +const requireJSON5 = require('require-json5') +const { compilerOptions } = requireJSON5('./tsconfig.json') + +module.exports = { + verbose: true, + preset: 'ts-jest', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.ts', + '!**/node_modules/**', + '!**/test/**', + '!**/build/**', + '!**/src/**/?(*.)+(spec|test).ts?(x)', + '!**/src/db/**', + ], + coverageThreshold: { + global: { + lines: 90, + }, + }, + testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'], + setupFilesAfterEnv: ['/test/setup.ts'], + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/' }), +} diff --git a/backend/jest.config.js b/backend/jest.config.js deleted file mode 100644 index 15eb22477..000000000 --- a/backend/jest.config.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - verbose: true, - preset: 'ts-jest', - collectCoverage: true, - collectCoverageFrom: [ - '**/*.ts', - '!**/node_modules/**', - '!**/test/**', - '!**/build/**', - '!**/src/**/?(*.)+(spec|test).ts?(x)', - '!**/src/db/**' - ], - coverageThreshold: { - global: { - lines: 90, - }, - }, - testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'], - setupFilesAfterEnv: ['/test/setup.ts'] -} diff --git a/backend/package.json b/backend/package.json index 76c85c683..f093fff62 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,14 +11,14 @@ "__migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations", "prod:migrate": "migrate --migrations-dir ./build/src/db/migrations --store ./build/src/db/migrate/store.js", "start": "node build/src/", - "build": "tsc && ./scripts/build.copy.files.sh", - "dev": "nodemon --exec ts-node src/ -e js,ts,gql", + "build": "tsc && tsc-alias && ./scripts/build.copy.files.sh", + "dev": "nodemon --exec ts-node --require tsconfig-paths/register src/ -e js,ts,gql", "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,ts,gql", "lint": "eslint --max-warnings=0 --ext .js,.ts ./src", "test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --runInBand --coverage --forceExit --detectOpenHandles", - "db:clean": "ts-node src/db/clean.ts", + "db:clean": "ts-node --require tsconfig-paths/register src/db/clean.ts", "db:reset": "yarn run db:clean", - "db:seed": "ts-node src/db/seed.ts", + "db:seed": "ts-node --require tsconfig-paths/register src/db/seed.ts", "db:migrate": "yarn run __migrate --store ./src/db/migrate/store.ts", "db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create" }, @@ -119,9 +119,12 @@ "jest": "^29.7.0", "nodemon": "~3.1.9", "prettier": "^3.5.3", + "require-json5": "^1.3.0", "rosie": "^2.1.1", "ts-jest": "^29.3.1", "ts-node": "^10.9.2", + "tsc-alias": "^1.8.14", + "tsconfig-paths": "^4.2.0", "typescript": "^5.8.3" }, "resolutions": { diff --git a/backend/scripts/build.copy.files.sh b/backend/scripts/build.copy.files.sh index 9d17f46ae..da76a623c 100755 --- a/backend/scripts/build.copy.files.sh +++ b/backend/scripts/build.copy.files.sh @@ -1,5 +1,8 @@ #!/bin/sh +# public +cp -r public/ build/public/ + # html files mkdir -p build/src/middleware/helpers/email/templates/ cp -r src/middleware/helpers/email/templates/*.html build/src/middleware/helpers/email/templates/ diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index e6a02a87d..5eb58fa89 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -2,6 +2,7 @@ /* eslint-disable n/no-unpublished-require */ /* eslint-disable n/no-missing-require */ import { config } from 'dotenv' + import emails from './emails' import metadata from './metadata' diff --git a/backend/src/db/clean.ts b/backend/src/db/clean.ts index ae5ce7320..0f316faf8 100644 --- a/backend/src/db/clean.ts +++ b/backend/src/db/clean.ts @@ -1,5 +1,6 @@ /* eslint-disable n/no-process-exit */ -import CONFIG from '../config' +import CONFIG from '@config/index' + import { cleanDatabase } from './factories' if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) { diff --git a/backend/src/db/compiler.ts b/backend/src/db/compiler.ts index 4dd36f16b..2d897762f 100644 --- a/backend/src/db/compiler.ts +++ b/backend/src/db/compiler.ts @@ -1,5 +1,7 @@ /* eslint-disable import/no-commonjs */ // eslint-disable-next-line n/no-unpublished-require 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') module.exports = tsNode.register diff --git a/backend/src/db/factories.ts b/backend/src/db/factories.ts index c75c92fdd..e09a4f921 100644 --- a/backend/src/db/factories.ts +++ b/backend/src/db/factories.ts @@ -1,11 +1,13 @@ -import { v4 as uuid } from 'uuid' -import slugify from 'slug' +import { faker } from '@faker-js/faker' import { hashSync } from 'bcryptjs' import { Factory } from 'rosie' -import { faker } from '@faker-js/faker' +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' -import CONFIG from '../config/index' -import generateInviteCode from '../schema/resolvers/helpers/generateInviteCode' const neode = getNeode() @@ -70,7 +72,6 @@ Factory.define('basicUser') termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z', allowEmbedIframes: false, showShoutsPublicly: false, - sendNotificationEmails: true, locale: 'en', }) .attr('slug', ['slug', 'name'], (slug, name) => { @@ -173,6 +174,7 @@ Factory.define('post') ]) await Promise.all([ post.relateTo(author, 'author'), + post.relateTo(author, 'observes'), // Promise.all(categories.map((c) => c.relateTo(post, 'post'))), Promise.all(tags.map((t) => t.relateTo(post, 'post'))), ]) @@ -208,7 +210,11 @@ Factory.define('comment') options.author, options.post, ]) - await Promise.all([comment.relateTo(author, 'author'), comment.relateTo(post, 'post')]) + await Promise.all([ + comment.relateTo(author, 'author'), + comment.relateTo(post, 'post'), + post.relateTo(author, 'observes'), + ]) return comment }) diff --git a/backend/src/db/migrate/store.ts b/backend/src/db/migrate/store.ts index b5dd43e16..e373c41c0 100644 --- a/backend/src/db/migrate/store.ts +++ b/backend/src/db/migrate/store.ts @@ -1,8 +1,9 @@ -import { getDriver, getNeode } from '../neo4j' import { hashSync } from 'bcryptjs' import { v4 as uuid } from 'uuid' -import { categories } from '../../constants/categories' -import CONFIG from '../../config' + +import CONFIG from '@config/index' +import { categories } from '@constants/categories' +import { getDriver, getNeode } from '@db/neo4j' const defaultAdmin = { email: 'admin@example.org', @@ -55,19 +56,18 @@ const createDefaultAdminUser = async (session) => { `MERGE (e:EmailAddress { email: "${defaultAdmin.email}", createdAt: toString(datetime()) - })-[:BELONGS_TO]->(u:User { - name: "${defaultAdmin.name}", - encryptedPassword: "${defaultAdmin.password}", - role: "admin", - id: "${defaultAdmin.id}", - slug: "${defaultAdmin.slug}", - createdAt: toString(datetime()), - allowEmbedIframes: false, - showShoutsPublicly: false, - sendNotificationEmails: true, - deleted: false, - disabled: false - })-[:PRIMARY_EMAIL]->(e)`, + })-[:BELONGS_TO]->(u:User { + name: "${defaultAdmin.name}", + encryptedPassword: "${defaultAdmin.password}", + role: "admin", + id: "${defaultAdmin.id}", + slug: "${defaultAdmin.slug}", + createdAt: toString(datetime()), + allowEmbedIframes: false, + showShoutsPublicly: false, + deleted: false, + disabled: false + })-[:PRIMARY_EMAIL]->(e)`, ) }) try { diff --git a/backend/src/db/migrate/template.ts b/backend/src/db/migrate/template.ts index 9661dcf9c..f9eb1a338 100644 --- a/backend/src/db/migrate/template.ts +++ b/backend/src/db/migrate/template.ts @@ -1,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = '' 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 1480715ae..df4cec41e 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 @@ -2,8 +2,9 @@ /* eslint-disable promise/prefer-await-to-callbacks */ import { throwError, concat } from 'rxjs' import { flatMap, mergeMap, map, catchError, filter } from 'rxjs/operators' -import { getDriver } from '../neo4j' -import normalizeEmail from '../../schema/resolvers/helpers/normalizeEmail' + +import { getDriver } from '@db/neo4j' +import normalizeEmail from '@schema/resolvers/helpers/normalizeEmail' export const description = ` This migration merges duplicate :User and :EmailAddress nodes. It became 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 f56389045..89cef62fc 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 @@ -2,7 +2,8 @@ /* eslint-disable promise/prefer-await-to-callbacks */ import { throwError, concat } from 'rxjs' import { flatMap, mergeMap, map, catchError } from 'rxjs/operators' -import { getDriver } from '../neo4j' + +import { getDriver } from '@db/neo4j' export const description = ` This migration merges duplicate :Location nodes. It became 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 49506aae3..4743ff175 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,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = ` This migration creates a MUTED relationship between two edges(:User) that have a pre-existing BLOCKED relationship. 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 73c329bfc..84e15f9fb 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,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = ` This migration swaps the value stored in Location.lat with the value 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 8ef6976a3..8eee22318 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,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = 'This migration adds a fulltext index for the tags in order to search for Hasthags.' 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 e949713b8..2a30d769e 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,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = ` We introduced a new node label 'Image' and we need a primary key for it. Best 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 6643e3540..f0531b6c8 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,11 +1,13 @@ /* eslint-disable security/detect-non-literal-fs-filename */ -import { getDriver } from '../neo4j' +import https from 'https' import { existsSync, createReadStream } from 'node:fs' import path from 'node:path' + import { S3 } from 'aws-sdk' import mime from 'mime-types' -import s3Configs from '../../config' -import https from 'https' + +import s3Configs from '@config/index' +import { getDriver } from '@db/neo4j' export const description = ` Upload all image files to a S3 compatible object storage in order to reduce 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 6f347b99b..355eb8476 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,5 +1,5 @@ /* eslint-disable no-console */ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = ` Refactor all our image properties on posts and users to a dedicated type 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 a8880d8e8..5ce75ab28 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,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = 'We should not maintain obsolete attributes for users who have been deleted.' 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 70d81e5c0..a2b5ff159 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,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = 'We should not maintain obsolete attributes for posts which have been deleted.' 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 765c7919b..0190ead48 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,7 +1,8 @@ /* eslint-disable security/detect-non-literal-fs-filename */ -import { getDriver } from '../neo4j' import { existsSync } from 'node:fs' +import { getDriver } from '@db/neo4j' + export const description = ` In this review: https://github.com/Human-Connection/Human-Connection/pull/3262#discussion_r398634249 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 0d8f28e1b..ce3515ac7 100644 --- a/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts +++ b/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts @@ -1,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = ` This migration adds the clickedCount property to all posts, setting it to 0. 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 31b9d69ff..5615aa4e0 100644 --- a/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts +++ b/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts @@ -1,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = ` This migration adds the viewedTeaserCount property to all posts, setting it to 0. diff --git a/backend/src/db/migrations/20210506150512-add-donations-node.ts b/backend/src/db/migrations/20210506150512-add-donations-node.ts index b7e0e026a..3d01f28bb 100644 --- a/backend/src/db/migrations/20210506150512-add-donations-node.ts +++ b/backend/src/db/migrations/20210506150512-add-donations-node.ts @@ -1,6 +1,7 @@ -import { getDriver } from '../neo4j' import { v4 as uuid } from 'uuid' +import { getDriver } from '@db/neo4j' + export const description = 'This migration adds a Donations node with default settings to the database.' 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 a555efa3a..bd886db02 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,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = '' 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 586a090f4..08dc558fb 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,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = ` We introduced a new node label 'Group' and we need two primary keys 'id' and 'slug' for it. diff --git a/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts b/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts index 34cf7b7a2..2239d6d06 100644 --- a/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts +++ b/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts @@ -1,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = '' 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 2ca705bf4..f33aa818a 100644 --- a/backend/src/db/migrations/20230329150329-article-label-for-posts.ts +++ b/backend/src/db/migrations/20230329150329-article-label-for-posts.ts @@ -1,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = 'Add to all existing posts the Article label' diff --git a/backend/src/db/migrations/20230608130637-add-postType-property.ts b/backend/src/db/migrations/20230608130637-add-postType-property.ts index 83c2f4ed3..26c99ce48 100644 --- a/backend/src/db/migrations/20230608130637-add-postType-property.ts +++ b/backend/src/db/migrations/20230608130637-add-postType-property.ts @@ -1,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = 'Add postType property Article to all posts' diff --git a/backend/src/db/migrations/20231017141022-fix-event-dates.ts b/backend/src/db/migrations/20231017141022-fix-event-dates.ts index e793e173c..b2edf17dc 100644 --- a/backend/src/db/migrations/20231017141022-fix-event-dates.ts +++ b/backend/src/db/migrations/20231017141022-fix-event-dates.ts @@ -1,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = ` Transform event start and end date of format 'YYYY-MM-DD HH:MM:SS' in CEST 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 026f7f29c..619b5f1fa 100644 --- a/backend/src/db/migrations/20250331130323-author-observes-own-post.ts +++ b/backend/src/db/migrations/20250331130323-author-observes-own-post.ts @@ -1,4 +1,4 @@ -import { getDriver } from '../neo4j' +import { getDriver } from '@db/neo4j' export const description = ` All authors observe their posts. diff --git a/backend/src/db/migrations/20250405030454-email-notification-settings.ts b/backend/src/db/migrations/20250405030454-email-notification-settings.ts new file mode 100644 index 000000000..07ce9ab79 --- /dev/null +++ b/backend/src/db/migrations/20250405030454-email-notification-settings.ts @@ -0,0 +1,68 @@ +import { getDriver } from '@db/neo4j' + +export const description = + 'Transforms the `sendNotificationEmails` property on User to a multi value system' + +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 (user:User) + SET user.emailNotificationsCommentOnObservedPost = user.sendNotificationEmails + SET user.emailNotificationsMention = user.sendNotificationEmails + SET user.emailNotificationsChatMessage = user.sendNotificationEmails + SET user.emailNotificationsGroupMemberJoined = user.sendNotificationEmails + SET user.emailNotificationsGroupMemberLeft = user.sendNotificationEmails + SET user.emailNotificationsGroupMemberRemoved = user.sendNotificationEmails + SET user.emailNotificationsGroupMemberRoleChanged = user.sendNotificationEmails + REMOVE user.sendNotificationEmails + `) + await transaction.commit() + next() + } 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 { + // Implement your migration here. + await transaction.run(` + MATCH (user:User) + SET user.sendNotificationEmails = true + REMOVE user.emailNotificationsCommentOnObservedPost + REMOVE user.emailNotificationsMention + REMOVE user.emailNotificationsChatMessage + REMOVE user.emailNotificationsGroupMemberJoined + REMOVE user.emailNotificationsGroupMemberLeft + REMOVE user.emailNotificationsGroupMemberRemoved + REMOVE user.emailNotificationsGroupMemberRoleChanged + `) + await transaction.commit() + next() + } 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 dc5bf2764..c94c552f0 100644 --- a/backend/src/db/neo4j.ts +++ b/backend/src/db/neo4j.ts @@ -1,8 +1,9 @@ /* eslint-disable import/no-named-as-default-member */ import neo4j from 'neo4j-driver' -import CONFIG from '../config' import Neode from 'neode' -import models from '../models' + +import CONFIG from '@config/index' +import models from '@models/index' let driver const defaultOptions = { diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index 4183b8ce5..34a6ebb03 100644 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -1,21 +1,23 @@ /* eslint-disable n/no-process-exit */ -import sample from 'lodash/sample' -import { createTestClient } from 'apollo-server-testing' -import CONFIG from '../config' -import createServer from '../server' import { faker } from '@faker-js/faker' -import Factory from './factories' -import { getNeode, getDriver } from './neo4j' +import { createTestClient } from 'apollo-server-testing' +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 { createPostMutation } from '../graphql/posts' -import { createRoomMutation } from '../graphql/rooms' -import { createMessageMutation } from '../graphql/messages' -import { createCommentMutation } from '../graphql/comments' -import { categories } from '../constants/categories' +} from '@graphql/groups' +import { createMessageMutation } from '@graphql/messages' +import { createPostMutation } from '@graphql/posts' +import { createRoomMutation } from '@graphql/rooms' +import createServer from '@src/server' + +import Factory from './factories' +import { getNeode, getDriver } from './neo4j' if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) { throw new Error(`You cannot seed the database in a non-staging and real production environment!`) diff --git a/backend/src/index.ts b/backend/src/index.ts index 59718dad1..35c215803 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,5 +1,5 @@ -import createServer from './server' import CONFIG from './config' +import createServer from './server' const { server, httpServer } = createServer() const url = new URL(CONFIG.GRAPHQL_URI) diff --git a/backend/src/jwt/decode.spec.ts b/backend/src/jwt/decode.spec.ts index ca27ef624..29783bc6b 100644 --- a/backend/src/jwt/decode.spec.ts +++ b/backend/src/jwt/decode.spec.ts @@ -1,5 +1,6 @@ -import Factory, { cleanDatabase } from '../db/factories' -import { getDriver, getNeode } from '../db/neo4j' +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver, getNeode } from '@db/neo4j' + import decode from './decode' import encode from './encode' diff --git a/backend/src/jwt/decode.ts b/backend/src/jwt/decode.ts index 45888dead..b4424e660 100644 --- a/backend/src/jwt/decode.ts +++ b/backend/src/jwt/decode.ts @@ -1,5 +1,6 @@ import jwt from 'jsonwebtoken' -import CONFIG from '../config' + +import CONFIG from '@config/index' export default async (driver, authorizationHeader) => { if (!authorizationHeader) return null diff --git a/backend/src/jwt/encode.spec.ts b/backend/src/jwt/encode.spec.ts index 37775eb55..55c74bf8d 100644 --- a/backend/src/jwt/encode.spec.ts +++ b/backend/src/jwt/encode.spec.ts @@ -1,6 +1,8 @@ -import encode from './encode' import jwt from 'jsonwebtoken' -import CONFIG from '../config' + +import CONFIG from '@config/index' + +import encode from './encode' describe('encode', () => { let payload diff --git a/backend/src/jwt/encode.ts b/backend/src/jwt/encode.ts index 0df81fa02..110111faf 100644 --- a/backend/src/jwt/encode.ts +++ b/backend/src/jwt/encode.ts @@ -1,5 +1,6 @@ import jwt from 'jsonwebtoken' -import CONFIG from '../config' + +import CONFIG from '@config/index' // Generate an Access Token for the given User ID export default function encode(user) { diff --git a/backend/src/middleware/excerptMiddleware.ts b/backend/src/middleware/excerptMiddleware.ts index 28b30fb4f..f903dd01c 100644 --- a/backend/src/middleware/excerptMiddleware.ts +++ b/backend/src/middleware/excerptMiddleware.ts @@ -1,5 +1,6 @@ import trunc from 'trunc-html' -import { DESCRIPTION_EXCERPT_HTML_LENGTH } from '../constants/groups' + +import { DESCRIPTION_EXCERPT_HTML_LENGTH } from '@constants/groups' export default { Mutation: { diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts index 10d53ab7b..2bb617a3d 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts @@ -1,8 +1,9 @@ -import gql from 'graphql-tag' -import { cleanDatabase } from '../../db/factories' import { createTestClient } from 'apollo-server-testing' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' +import gql from 'graphql-tag' + +import { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' let server let query diff --git a/backend/src/middleware/helpers/cleanHtml.ts b/backend/src/middleware/helpers/cleanHtml.ts index 04d6deae4..e72746fcf 100644 --- a/backend/src/middleware/helpers/cleanHtml.ts +++ b/backend/src/middleware/helpers/cleanHtml.ts @@ -1,6 +1,6 @@ /* eslint-disable security/detect-unsafe-regex */ -import sanitizeHtml from 'sanitize-html' import linkifyHtml from 'linkify-html' +import sanitizeHtml from 'sanitize-html' export const removeHtmlTags = (input) => { return sanitizeHtml(input, { diff --git a/backend/src/middleware/helpers/email/sendMail.ts b/backend/src/middleware/helpers/email/sendMail.ts index 6c1e0d8ba..02456f391 100644 --- a/backend/src/middleware/helpers/email/sendMail.ts +++ b/backend/src/middleware/helpers/email/sendMail.ts @@ -1,8 +1,9 @@ -import CONFIG from '../../../config' -import { cleanHtml } from '../cleanHtml' import nodemailer from 'nodemailer' import { htmlToText } from 'nodemailer-html-to-text' +import CONFIG from '@config/index' +import { cleanHtml } from '@middleware/helpers/cleanHtml' + const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD const hasDKIMData = diff --git a/backend/src/middleware/helpers/email/templateBuilder.spec.ts b/backend/src/middleware/helpers/email/templateBuilder.spec.ts index 437672a9a..48e8b4c99 100644 --- a/backend/src/middleware/helpers/email/templateBuilder.spec.ts +++ b/backend/src/middleware/helpers/email/templateBuilder.spec.ts @@ -1,5 +1,6 @@ -import CONFIG from '../../../config' -import logosWebapp from '../../../config/logos' +import CONFIG from '@config/index' +import logosWebapp from '@config/logos' + import { signupTemplate, emailVerificationTemplate, diff --git a/backend/src/middleware/helpers/email/templateBuilder.ts b/backend/src/middleware/helpers/email/templateBuilder.ts index 398cbabf9..c091bf1f8 100644 --- a/backend/src/middleware/helpers/email/templateBuilder.ts +++ b/backend/src/middleware/helpers/email/templateBuilder.ts @@ -1,12 +1,13 @@ /* eslint-disable import/no-namespace */ import mustache from 'mustache' -import CONFIG from '../../../config' -import metadata from '../../../config/metadata' -import logosWebapp from '../../../config/logos' + +import logosWebapp from '@config//logos' +import metadata from '@config//metadata' +import CONFIG from '@config/index' import * as templates from './templates' -import * as templatesEN from './templates/en' import * as templatesDE from './templates/de' +import * as templatesEN from './templates/en' const from = CONFIG.EMAIL_DEFAULT_SENDER const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI) diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 8eca3c8e8..225e02209 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -1,22 +1,24 @@ /* eslint-disable security/detect-object-injection */ import { applyMiddleware } from 'graphql-middleware' -import CONFIG from '../config' -import softDelete from './softDelete/softDeleteMiddleware' -import sluggify from './sluggifyMiddleware' + +import CONFIG from '@config/index' + +import chatMiddleware from './chatMiddleware' import excerpt from './excerptMiddleware' -import xss from './xssMiddleware' -import permissions from './permissionsMiddleware' +import hashtags from './hashtags/hashtagsMiddleware' import includedFields from './includedFieldsMiddleware' -import orderBy from './orderByMiddleware' -import validation from './validation/validationMiddleware' +import languages from './languages/languages' +import login from './login/loginMiddleware' // eslint-disable-next-line import/no-cycle import notifications from './notifications/notificationsMiddleware' -import hashtags from './hashtags/hashtagsMiddleware' -import login from './login/loginMiddleware' +import orderBy from './orderByMiddleware' +import permissions from './permissionsMiddleware' import sentry from './sentryMiddleware' -import languages from './languages/languages' +import sluggify from './sluggifyMiddleware' +import softDelete from './softDelete/softDeleteMiddleware' import userInteractions from './userInteractions' -import chatMiddleware from './chatMiddleware' +import validation from './validation/validationMiddleware' +import xss from './xssMiddleware' export default (schema) => { const middlewares = { diff --git a/backend/src/middleware/languages/languages.spec.ts b/backend/src/middleware/languages/languages.spec.ts index 8daa311e1..ca77acac8 100644 --- a/backend/src/middleware/languages/languages.spec.ts +++ b/backend/src/middleware/languages/languages.spec.ts @@ -1,8 +1,9 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' 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' let mutate let authenticatedUser diff --git a/backend/src/middleware/languages/languages.ts b/backend/src/middleware/languages/languages.ts index 3c043ceec..6149b90d5 100644 --- a/backend/src/middleware/languages/languages.ts +++ b/backend/src/middleware/languages/languages.ts @@ -1,5 +1,6 @@ import LanguageDetect from 'languagedetect' -import { removeHtmlTags } from '../helpers/cleanHtml' + +import { removeHtmlTags } from '@middleware/helpers/cleanHtml' const setPostLanguage = (text, defaultLanguage) => { const lngDetector = new LanguageDetect() diff --git a/backend/src/middleware/login/loginMiddleware.ts b/backend/src/middleware/login/loginMiddleware.ts index abf0d0b18..04d189b4b 100644 --- a/backend/src/middleware/login/loginMiddleware.ts +++ b/backend/src/middleware/login/loginMiddleware.ts @@ -1,10 +1,10 @@ -import { sendMail } from '../helpers/email/sendMail' +import { sendMail } from '@middleware/helpers/email/sendMail' import { signupTemplate, resetPasswordTemplate, wrongAccountTemplate, emailVerificationTemplate, -} from '../helpers/email/templateBuilder' +} from '@middleware/helpers/email/templateBuilder' const sendSignupMail = async (resolve, root, args, context, resolveInfo) => { const { inviteCode } = args diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts index 50d655484..c636a7c87 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts @@ -1,17 +1,18 @@ -import gql from 'graphql-tag' -import Factory, { cleanDatabase } from '../../db/factories' import { createTestClient } from 'apollo-server-testing' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer, { pubsub } from '../../server' +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' +} from '@graphql/groups' +import { createMessageMutation } from '@graphql/messages' +import { createRoomMutation } from '@graphql/rooms' +import createServer, { pubsub } from '@src/server' const sendMailMock = jest.fn() jest.mock('../helpers/email/sendMail', () => ({ @@ -19,8 +20,10 @@ jest.mock('../helpers/email/sendMail', () => ({ })) const chatMessageTemplateMock = jest.fn() +const notificationTemplateMock = jest.fn() jest.mock('../helpers/email/templateBuilder', () => ({ chatMessageTemplate: () => chatMessageTemplateMock(), + notificationTemplate: () => notificationTemplateMock(), })) let isUserOnlineMock = jest.fn() @@ -85,8 +88,8 @@ afterAll(async () => { beforeEach(async () => { publishSpy.mockClear() - notifiedUser = await neode.create( - 'User', + notifiedUser = await Factory.build( + 'user', { id: 'you', name: 'Al Capone', @@ -186,6 +189,7 @@ describe('notifications', () => { describe('commenter is not me', () => { beforeEach(async () => { + jest.clearAllMocks() commentContent = 'Commenters comment.' commentAuthor = await neode.create( 'User', @@ -201,25 +205,8 @@ describe('notifications', () => { ) }) - it('sends me a notification', async () => { + it('sends me a notification and email', async () => { await createCommentOnPostAction() - const expected = expect.objectContaining({ - data: { - notifications: [ - { - read: false, - createdAt: expect.any(String), - reason: 'commented_on_post', - from: { - __typename: 'Comment', - id: 'c47', - content: commentContent, - }, - relatedUser: null, - }, - ], - }, - }) await expect( query({ query: notificationQuery, @@ -227,24 +214,85 @@ describe('notifications', () => { read: false, }, }), - ).resolves.toEqual(expected) + ).resolves.toMatchObject( + expect.objectContaining({ + data: { + notifications: [ + { + read: false, + createdAt: expect.any(String), + reason: 'commented_on_post', + from: { + __typename: 'Comment', + id: 'c47', + content: commentContent, + }, + relatedUser: null, + }, + ], + }, + }), + ) + + // Mail + expect(sendMailMock).toHaveBeenCalledTimes(1) + expect(notificationTemplateMock).toHaveBeenCalledTimes(1) }) - it('sends me no notification if I have blocked the comment author', async () => { - await notifiedUser.relateTo(commentAuthor, 'blocked') - await createCommentOnPostAction() - const expected = expect.objectContaining({ - data: { notifications: [] }, - }) + describe('if I have disabled `emailNotificationsCommentOnObservedPost`', () => { + it('sends me a notification but no email', async () => { + await notifiedUser.update({ emailNotificationsCommentOnObservedPost: false }) + await createCommentOnPostAction() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject( + expect.objectContaining({ + data: { + notifications: [ + { + read: false, + createdAt: expect.any(String), + reason: 'commented_on_post', + from: { + __typename: 'Comment', + id: 'c47', + content: commentContent, + }, + relatedUser: null, + }, + ], + }, + }), + ) - await expect( - query({ - query: notificationQuery, - variables: { - read: false, - }, - }), - ).resolves.toEqual(expected) + // No Mail + expect(sendMailMock).not.toHaveBeenCalled() + expect(notificationTemplateMock).not.toHaveBeenCalled() + }) + }) + + describe('if I have blocked the comment author', () => { + it('sends me no notification', async () => { + await notifiedUser.relateTo(commentAuthor, 'blocked') + await createCommentOnPostAction() + const expected = expect.objectContaining({ + data: { notifications: [] }, + }) + + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) }) }) @@ -273,6 +321,7 @@ describe('notifications', () => { }) beforeEach(async () => { + jest.clearAllMocks() postAuthor = await neode.create( 'User', { @@ -295,7 +344,7 @@ describe('notifications', () => { 'Hey @al-capone how do you do?' }) - it('sends me a notification', async () => { + it('sends me a notification and email', async () => { await createPostAction() const expectedContent = 'Hey @al-capone how do you do?' @@ -323,6 +372,47 @@ describe('notifications', () => { ], }, }) + + // Mail + expect(sendMailMock).toHaveBeenCalledTimes(1) + expect(notificationTemplateMock).toHaveBeenCalledTimes(1) + }) + + describe('if I have disabled `emailNotificationsMention`', () => { + it('sends me a notification but no email', async () => { + await notifiedUser.update({ emailNotificationsMention: false }) + await createPostAction() + const expectedContent = + 'Hey @al-capone how do you do?' + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + errors: undefined, + data: { + notifications: [ + { + read: false, + createdAt: expect.any(String), + reason: 'mentioned_in_post', + from: { + __typename: 'Post', + id: 'p47', + content: expectedContent, + }, + }, + ], + }, + }) + + // Mail + expect(sendMailMock).not.toHaveBeenCalled() + expect(notificationTemplateMock).not.toHaveBeenCalled() + }) }) it('publishes `NOTIFICATION_ADDED` to me', async () => { @@ -688,7 +778,7 @@ describe('notifications', () => { roomId = room.data.CreateRoom.id }) - describe('chatReceiver is online', () => { + describe('if the chatReceiver is online', () => { it('sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(true) @@ -705,7 +795,7 @@ describe('notifications', () => { }) }) - describe('chatReceiver is offline', () => { + describe('if the chatReceiver is offline', () => { it('sends an email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) @@ -722,7 +812,7 @@ describe('notifications', () => { }) }) - describe('chatReceiver has blocked chatSender', () => { + describe('if the chatReceiver has blocked chatSender', () => { it('sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await chatReceiver.relateTo(chatSender, 'blocked') @@ -740,10 +830,10 @@ describe('notifications', () => { }) }) - describe('chatReceiver has disabled email notifications', () => { + describe('if the chatReceiver has disabled `emailNotificationsChatMessage`', () => { it('sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) - await chatReceiver.update({ sendNotificationEmails: false }) + await chatReceiver.update({ emailNotificationsChatMessage: false }) await mutate({ mutation: createMessageMutation(), @@ -763,8 +853,8 @@ describe('notifications', () => { let groupOwner beforeEach(async () => { - groupOwner = await neode.create( - 'User', + groupOwner = await Factory.build( + 'user', { id: 'group-owner', name: 'Group Owner', @@ -791,7 +881,7 @@ describe('notifications', () => { }) describe('user joins group', () => { - beforeEach(async () => { + const joinGroupAction = async () => { authenticatedUser = await notifiedUser.toJson() await mutate({ mutation: joinGroupMutation(), @@ -801,9 +891,14 @@ describe('notifications', () => { }, }) authenticatedUser = await groupOwner.toJson() + } + + beforeEach(async () => { + jest.clearAllMocks() }) - it('has the notification in database', async () => { + it('sends the group owner a notification and email', async () => { + await joinGroupAction() await expect( query({ query: notificationQuery, @@ -827,19 +922,50 @@ describe('notifications', () => { }, errors: undefined, }) + + // Mail + expect(sendMailMock).toHaveBeenCalledTimes(1) + expect(notificationTemplateMock).toHaveBeenCalledTimes(1) + }) + + describe('if the group owner has disabled `emailNotificationsGroupMemberJoined`', () => { + it('sends the group owner a notification but no email', async () => { + await groupOwner.update({ emailNotificationsGroupMemberJoined: false }) + await joinGroupAction() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + read: false, + reason: 'user_joined_group', + createdAt: expect.any(String), + from: { + __typename: 'Group', + id: 'closed-group', + }, + relatedUser: { + id: 'you', + }, + }, + ], + }, + errors: undefined, + }) + + // Mail + expect(sendMailMock).not.toHaveBeenCalled() + expect(notificationTemplateMock).not.toHaveBeenCalled() + }) }) }) - describe('user leaves group', () => { - beforeEach(async () => { + describe('user joins and leaves group', () => { + const leaveGroupAction = async () => { authenticatedUser = await notifiedUser.toJson() - await mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'closed-group', - userId: authenticatedUser.id, - }, - }) await mutate({ mutation: leaveGroupMutation(), variables: { @@ -848,9 +974,22 @@ describe('notifications', () => { }, }) authenticatedUser = await groupOwner.toJson() + } + + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await notifiedUser.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: authenticatedUser.id, + }, + }) }) - it('has two the notification in database', async () => { + it('sends the group owner two notifications and emails', async () => { + await leaveGroupAction() await expect( query({ query: notificationQuery, @@ -886,19 +1025,61 @@ describe('notifications', () => { }, errors: undefined, }) + + // Mail + expect(sendMailMock).toHaveBeenCalledTimes(2) + expect(notificationTemplateMock).toHaveBeenCalledTimes(2) + }) + + describe('if the group owner has disabled `emailNotificationsGroupMemberLeft`', () => { + it('sends the group owner two notification but only only one email', async () => { + await groupOwner.update({ emailNotificationsGroupMemberLeft: false }) + await leaveGroupAction() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + read: false, + reason: 'user_left_group', + createdAt: expect.any(String), + from: { + __typename: 'Group', + id: 'closed-group', + }, + relatedUser: { + id: 'you', + }, + }, + { + read: false, + reason: 'user_joined_group', + createdAt: expect.any(String), + from: { + __typename: 'Group', + id: 'closed-group', + }, + relatedUser: { + id: 'you', + }, + }, + ], + }, + errors: undefined, + }) + + // Mail + expect(sendMailMock).toHaveBeenCalledTimes(1) + expect(notificationTemplateMock).toHaveBeenCalledTimes(1) + }) }) }) describe('user role in group changes', () => { - beforeEach(async () => { - authenticatedUser = await notifiedUser.toJson() - await mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'closed-group', - userId: authenticatedUser.id, - }, - }) + const changeGroupMemberRoleAction = async () => { authenticatedUser = await groupOwner.toJson() await mutate({ mutation: changeGroupMemberRoleMutation(), @@ -909,9 +1090,23 @@ describe('notifications', () => { }, }) authenticatedUser = await notifiedUser.toJson() + } + + beforeEach(async () => { + authenticatedUser = await notifiedUser.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: authenticatedUser.id, + }, + }) + // Clear after because the above generates a notification not related + jest.clearAllMocks() }) - it('has notification in database', async () => { + it('sends the group member a notification and email', async () => { + await changeGroupMemberRoleAction() await expect( query({ query: notificationQuery, @@ -935,19 +1130,49 @@ describe('notifications', () => { }, errors: undefined, }) + + // Mail + expect(sendMailMock).toHaveBeenCalledTimes(1) + expect(notificationTemplateMock).toHaveBeenCalledTimes(1) + }) + + describe('if the group member has disabled `emailNotificationsGroupMemberRoleChanged`', () => { + it('sends the group member a notification but no email', async () => { + notifiedUser.update({ emailNotificationsGroupMemberRoleChanged: false }) + await changeGroupMemberRoleAction() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + read: false, + reason: 'changed_group_member_role', + createdAt: expect.any(String), + from: { + __typename: 'Group', + id: 'closed-group', + }, + relatedUser: { + id: 'group-owner', + }, + }, + ], + }, + errors: undefined, + }) + + // Mail + expect(sendMailMock).not.toHaveBeenCalled() + expect(notificationTemplateMock).not.toHaveBeenCalled() + }) }) }) describe('user is removed from group', () => { - beforeEach(async () => { - authenticatedUser = await notifiedUser.toJson() - await mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'closed-group', - userId: authenticatedUser.id, - }, - }) + const removeUserFromGroupAction = async () => { authenticatedUser = await groupOwner.toJson() await mutate({ mutation: removeUserFromGroupMutation(), @@ -957,9 +1182,23 @@ describe('notifications', () => { }, }) authenticatedUser = await notifiedUser.toJson() + } + + beforeEach(async () => { + authenticatedUser = await notifiedUser.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: authenticatedUser.id, + }, + }) + // Clear after because the above generates a notification not related + jest.clearAllMocks() }) - it('has notification in database', async () => { + it('sends the previous group member a notification and email', async () => { + await removeUserFromGroupAction() await expect( query({ query: notificationQuery, @@ -983,6 +1222,44 @@ describe('notifications', () => { }, errors: undefined, }) + + // Mail + expect(sendMailMock).toHaveBeenCalledTimes(1) + expect(notificationTemplateMock).toHaveBeenCalledTimes(1) + }) + + describe('if the previous group member has disabled `emailNotificationsGroupMemberRemoved`', () => { + it('sends the previous group member a notification but no email', async () => { + notifiedUser.update({ emailNotificationsGroupMemberRemoved: false }) + await removeUserFromGroupAction() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + read: false, + reason: 'removed_user_from_group', + createdAt: expect.any(String), + from: { + __typename: 'Group', + id: 'closed-group', + }, + relatedUser: { + id: 'group-owner', + }, + }, + ], + }, + errors: undefined, + }) + + // Mail + expect(sendMailMock).not.toHaveBeenCalled() + expect(notificationTemplateMock).not.toHaveBeenCalled() + }) }) }) }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.ts b/backend/src/middleware/notifications/notificationsMiddleware.ts index 09212a29d..faf4fd994 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.ts @@ -1,11 +1,15 @@ /* eslint-disable security/detect-object-injection */ +import { sendMail } from '@middleware/helpers/email/sendMail' +import { + chatMessageTemplate, + notificationTemplate, +} from '@middleware/helpers/email/templateBuilder' +import { isUserOnline } from '@middleware/helpers/isUserOnline' +import { validateNotifyUsers } from '@middleware/validation/validationMiddleware' // eslint-disable-next-line import/no-cycle -import { pubsub, NOTIFICATION_ADDED } from '../../server' +import { pubsub, NOTIFICATION_ADDED } from '@src/server' + import extractMentionedUsers from './mentions/extractMentionedUsers' -import { validateNotifyUsers } from '../validation/validationMiddleware' -import { sendMail } from '../helpers/email/sendMail' -import { chatMessageTemplate, notificationTemplate } from '../helpers/email/templateBuilder' -import { isUserOnline } from '../helpers/isUserOnline' const queryNotificationEmails = async (context, notificationUserIds) => { if (!(notificationUserIds && notificationUserIds.length)) return [] @@ -34,7 +38,7 @@ const queryNotificationEmails = async (context, notificationUserIds) => { } } -const publishNotifications = async (context, promises) => { +const publishNotifications = async (context, promises, emailNotificationSetting: string) => { let notifications = await Promise.all(promises) notifications = notifications.flat() const notificationsEmailAddresses = await queryNotificationEmails( @@ -43,7 +47,7 @@ const publishNotifications = async (context, promises) => { ) notifications.forEach((notificationAdded, index) => { pubsub.publish(NOTIFICATION_ADDED, { notificationAdded }) - if (notificationAdded.to.sendNotificationEmails) { + if (notificationAdded.to[emailNotificationSetting] ?? true) { sendMail( notificationTemplate({ email: notificationsEmailAddresses[index].email, @@ -58,9 +62,11 @@ const handleJoinGroup = async (resolve, root, args, context, resolveInfo) => { const { groupId, userId } = args const user = await resolve(root, args, context, resolveInfo) if (user) { - await publishNotifications(context, [ - notifyOwnersOfGroup(groupId, userId, 'user_joined_group', context), - ]) + await publishNotifications( + context, + [notifyOwnersOfGroup(groupId, userId, 'user_joined_group', context)], + 'emailNotificationsGroupMemberJoined', + ) } return user } @@ -69,9 +75,11 @@ const handleLeaveGroup = async (resolve, root, args, context, resolveInfo) => { const { groupId, userId } = args const user = await resolve(root, args, context, resolveInfo) if (user) { - await publishNotifications(context, [ - notifyOwnersOfGroup(groupId, userId, 'user_left_group', context), - ]) + await publishNotifications( + context, + [notifyOwnersOfGroup(groupId, userId, 'user_left_group', context)], + 'emailNotificationsGroupMemberLeft', + ) } return user } @@ -80,9 +88,11 @@ const handleChangeGroupMemberRole = async (resolve, root, args, context, resolve const { groupId, userId } = args const user = await resolve(root, args, context, resolveInfo) if (user) { - await publishNotifications(context, [ - notifyMemberOfGroup(groupId, userId, 'changed_group_member_role', context), - ]) + await publishNotifications( + context, + [notifyMemberOfGroup(groupId, userId, 'changed_group_member_role', context)], + 'emailNotificationsGroupMemberRoleChanged', + ) } return user } @@ -91,9 +101,11 @@ const handleRemoveUserFromGroup = async (resolve, root, args, context, resolveIn const { groupId, userId } = args const user = await resolve(root, args, context, resolveInfo) if (user) { - await publishNotifications(context, [ - notifyMemberOfGroup(groupId, userId, 'removed_user_from_group', context), - ]) + await publishNotifications( + context, + [notifyMemberOfGroup(groupId, userId, 'removed_user_from_group', context)], + 'emailNotificationsGroupMemberRemoved', + ) } return user } @@ -102,9 +114,11 @@ 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(context, [ - notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context), - ]) + await publishNotifications( + context, + [notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context)], + 'emailNotificationsMention', + ) } return post } @@ -115,16 +129,26 @@ 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(context, [ - notifyUsersOfMention( - 'Comment', - comment.id, - idsOfMentionedUsers, - 'mentioned_in_comment', - context, - ), - notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context), - ]) + await publishNotifications( + context, + [ + notifyUsersOfMention( + 'Comment', + comment.id, + idsOfMentionedUsers, + 'mentioned_in_comment', + context, + ), + ], + 'emailNotificationsMention', + ) + + await publishNotifications( + context, + [notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context)], + 'emailNotificationsCommentOnObservedPost', + ) + return comment } @@ -335,7 +359,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]-(currentUser) - AND recipientUser.sendNotificationEmails = true + AND NOT recipientUser.emailNotificationsChatMessage = false RETURN recipientUser, emailAddress {.email} ` const txResponse = await transaction.run(messageRecipientCypher, { diff --git a/backend/src/middleware/notifications/observing-posts.spec.ts b/backend/src/middleware/notifications/observing-posts.spec.ts index 13b971ed8..e10d61d9f 100644 --- a/backend/src/middleware/notifications/observing-posts.spec.ts +++ b/backend/src/middleware/notifications/observing-posts.spec.ts @@ -1,10 +1,10 @@ -import gql from 'graphql-tag' -import { cleanDatabase } from '../../db/factories' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' -import CONFIG from '../../config' +import CONFIG from '@config/index' +import { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false diff --git a/backend/src/middleware/orderByMiddleware.spec.ts b/backend/src/middleware/orderByMiddleware.spec.ts index 7453cf301..9534af76d 100644 --- a/backend/src/middleware/orderByMiddleware.spec.ts +++ b/backend/src/middleware/orderByMiddleware.spec.ts @@ -1,8 +1,9 @@ -import gql from 'graphql-tag' -import { cleanDatabase } from '../db/factories' -import { getNeode, getDriver } from '../db/neo4j' import { createTestClient } from 'apollo-server-testing' -import createServer from '../server' +import gql from 'graphql-tag' + +import { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const neode = getNeode() const driver = getDriver() diff --git a/backend/src/middleware/permissionsMiddleware.spec.ts b/backend/src/middleware/permissionsMiddleware.spec.ts index 667e74164..81d73bae8 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.ts +++ b/backend/src/middleware/permissionsMiddleware.spec.ts @@ -1,9 +1,10 @@ import { createTestClient } from 'apollo-server-testing' -import createServer from '../server' -import Factory, { cleanDatabase } from '../db/factories' import gql from 'graphql-tag' -import { getDriver, getNeode } from '../db/neo4j' -import CONFIG from '../config' + +import CONFIG from '@config/index' +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver, getNeode } from '@db/neo4j' +import createServer from '@src/server' const instance = getNeode() const driver = getDriver() diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index a38610efd..fcda6d218 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -1,7 +1,8 @@ import { rule, shield, deny, allow, or, and } from 'graphql-shield' -import { getNeode } from '../db/neo4j' -import CONFIG from '../config' -import { validateInviteCode } from '../schema/resolvers/transactions/inviteCodes' + +import CONFIG from '@config/index' +import { getNeode } from '@db/neo4j' +import { validateInviteCode } from '@schema/resolvers/transactions/inviteCodes' const debug = !!CONFIG.DEBUG const allowExternalErrors = true @@ -470,6 +471,7 @@ export default shield( }, User: { email: or(isMyOwn, isAdmin), + emailNotificationSettings: isMyOwn, }, Report: isModerator, }, diff --git a/backend/src/middleware/sentryMiddleware.ts b/backend/src/middleware/sentryMiddleware.ts index ace2c4eeb..b77f680d6 100644 --- a/backend/src/middleware/sentryMiddleware.ts +++ b/backend/src/middleware/sentryMiddleware.ts @@ -1,5 +1,6 @@ import { sentry } from 'graphql-middleware-sentry' -import CONFIG from '../config' + +import CONFIG from '@config/index' // eslint-disable-next-line import/no-mutable-exports let sentryMiddleware: any = (resolve, root, args, context, resolveInfo) => diff --git a/backend/src/middleware/slugifyMiddleware.spec.ts b/backend/src/middleware/slugifyMiddleware.spec.ts index 26bb2cb96..9e55d54b1 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.ts +++ b/backend/src/middleware/slugifyMiddleware.spec.ts @@ -1,10 +1,11 @@ -import { getNeode, getDriver } from '../db/neo4j' -import createServer from '../server' import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../db/factories' -import { createGroupMutation, updateGroupMutation } from '../graphql/groups' -import { createPostMutation } from '../graphql/posts' -import { signupVerificationMutation } from '../graphql/authentications' + +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 createServer from '@src/server' let authenticatedUser let variables diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts index 88d46a1c7..fa62ed101 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts @@ -1,8 +1,9 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' 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 neode = getNeode() const driver = getDriver() diff --git a/backend/src/middleware/userInteractions.spec.ts b/backend/src/middleware/userInteractions.spec.ts index 94d1ff274..37b5401e3 100644 --- a/backend/src/middleware/userInteractions.spec.ts +++ b/backend/src/middleware/userInteractions.spec.ts @@ -1,8 +1,9 @@ -import Factory, { cleanDatabase } from '../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../db/neo4j' -import createServer from '../server' 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' let query, aUser, bUser, post, authenticatedUser, variables diff --git a/backend/src/middleware/validation/validationMiddleware.spec.ts b/backend/src/middleware/validation/validationMiddleware.spec.ts index 2e1cd6fa7..8e4b4329f 100644 --- a/backend/src/middleware/validation/validationMiddleware.spec.ts +++ b/backend/src/middleware/validation/validationMiddleware.spec.ts @@ -1,8 +1,9 @@ -import gql from 'graphql-tag' -import Factory, { cleanDatabase } from '../../db/factories' -import { getNeode, getDriver } from '../../db/neo4j' import { createTestClient } from 'apollo-server-testing' -import createServer from '../../server' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const neode = getNeode() const driver = getDriver() diff --git a/backend/src/middleware/xssMiddleware.ts b/backend/src/middleware/xssMiddleware.ts index 7b1b66145..3ed310b40 100644 --- a/backend/src/middleware/xssMiddleware.ts +++ b/backend/src/middleware/xssMiddleware.ts @@ -1,4 +1,5 @@ -import walkRecursive from '../helpers/walkRecursive' +import walkRecursive from '@helpers/walkRecursive' + import { cleanHtml } from './helpers/cleanHtml' // exclamation mark separetes field names, that should not be sanitized diff --git a/backend/src/models/Post.ts b/backend/src/models/Post.ts index e206ea1f5..75081b728 100644 --- a/backend/src/models/Post.ts +++ b/backend/src/models/Post.ts @@ -58,4 +58,15 @@ export default { }, pinned: { type: 'boolean', default: null, valid: [null, true] }, postType: { type: 'string', default: 'Article', valid: ['Article', 'Event'] }, + observes: { + type: 'relationship', + relationship: 'OBSERVES', + target: 'User', + direction: 'in', + properties: { + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + active: { type: 'boolean', default: true }, + }, + }, } diff --git a/backend/src/models/User.spec.ts b/backend/src/models/User.spec.ts index 17f2fe0a9..3fde03462 100644 --- a/backend/src/models/User.spec.ts +++ b/backend/src/models/User.spec.ts @@ -1,5 +1,5 @@ -import { cleanDatabase } from '../db/factories' -import { getNeode, getDriver } from '../db/neo4j' +import { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' const driver = getDriver() const neode = getNeode() diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 9b828e27e..e9fbfb6ce 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -155,12 +155,50 @@ export default { type: 'boolean', default: false, }, - sendNotificationEmails: { + + // emailNotifications + emailNotificationsCommentOnObservedPost: { type: 'boolean', default: true, }, + emailNotificationsMention: { + type: 'boolean', + default: true, + }, + emailNotificationsChatMessage: { + type: 'boolean', + default: true, + }, + emailNotificationsGroupMemberJoined: { + type: 'boolean', + default: true, + }, + emailNotificationsGroupMemberLeft: { + type: 'boolean', + default: true, + }, + emailNotificationsGroupMemberRemoved: { + type: 'boolean', + default: true, + }, + emailNotificationsGroupMemberRoleChanged: { + type: 'boolean', + default: true, + }, + locale: { type: 'string', allow: [null], }, + observes: { + type: 'relationship', + relationship: 'OBSERVES', + target: 'Post', + direction: 'out', + properties: { + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + active: { type: 'boolean', default: true }, + }, + }, } diff --git a/backend/src/schema/index.ts b/backend/src/schema/index.ts index 07721bceb..e043bc243 100644 --- a/backend/src/schema/index.ts +++ b/backend/src/schema/index.ts @@ -1,6 +1,7 @@ import { makeAugmentedSchema } from 'neo4j-graphql-js' -import typeDefs from './types' + import resolvers from './resolvers' +import typeDefs from './types' export default makeAugmentedSchema({ typeDefs, @@ -10,6 +11,8 @@ export default makeAugmentedSchema({ exclude: [ 'Badge', 'Embed', + 'EmailNotificationSettings', + 'EmailNotificationSettingsOption', 'EmailAddress', 'Notification', 'Statistics', diff --git a/backend/src/schema/resolvers/comments.spec.ts b/backend/src/schema/resolvers/comments.spec.ts index b2730dad4..e92daf86e 100644 --- a/backend/src/schema/resolvers/comments.spec.ts +++ b/backend/src/schema/resolvers/comments.spec.ts @@ -1,8 +1,9 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' import { createTestClient } from 'apollo-server-testing' -import createServer from '../../server' -import { getNeode, getDriver } from '../../db/neo4j' +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 neode = getNeode() diff --git a/backend/src/schema/resolvers/comments.ts b/backend/src/schema/resolvers/comments.ts index b9c0271c1..897c71d6f 100644 --- a/backend/src/schema/resolvers/comments.ts +++ b/backend/src/schema/resolvers/comments.ts @@ -1,4 +1,5 @@ import { v4 as uuid } from 'uuid' + import Resolver from './helpers/Resolver' export default { diff --git a/backend/src/schema/resolvers/donations.spec.ts b/backend/src/schema/resolvers/donations.spec.ts index 9fc010eca..ef2070d4e 100644 --- a/backend/src/schema/resolvers/donations.spec.ts +++ b/backend/src/schema/resolvers/donations.spec.ts @@ -1,8 +1,9 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' let mutate, query, authenticatedUser, variables const instance = getNeode() diff --git a/backend/src/schema/resolvers/emails.spec.ts b/backend/src/schema/resolvers/emails.spec.ts index 02a631495..c594f99f7 100644 --- a/backend/src/schema/resolvers/emails.spec.ts +++ b/backend/src/schema/resolvers/emails.spec.ts @@ -1,8 +1,9 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getDriver, getNeode } from '../../db/neo4j' -import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver, getNeode } from '@db/neo4j' +import createServer from '@src/server' const neode = getNeode() diff --git a/backend/src/schema/resolvers/emails.ts b/backend/src/schema/resolvers/emails.ts index ff37948f2..d4de9c87b 100644 --- a/backend/src/schema/resolvers/emails.ts +++ b/backend/src/schema/resolvers/emails.ts @@ -1,10 +1,11 @@ -import generateNonce from './helpers/generateNonce' -import Resolver from './helpers/Resolver' -import existingEmailAddress from './helpers/existingEmailAddress' import { UserInputError } from 'apollo-server' // eslint-disable-next-line import/extensions import Validator from 'neode/build/Services/Validator.js' + +import existingEmailAddress from './helpers/existingEmailAddress' +import generateNonce from './helpers/generateNonce' import normalizeEmail from './helpers/normalizeEmail' +import Resolver from './helpers/Resolver' export default { Query: { diff --git a/backend/src/schema/resolvers/embeds.spec.ts b/backend/src/schema/resolvers/embeds.spec.ts index 51dc18bd8..92dd224e3 100644 --- a/backend/src/schema/resolvers/embeds.spec.ts +++ b/backend/src/schema/resolvers/embeds.spec.ts @@ -1,9 +1,11 @@ -import fetch from 'node-fetch' import fs from 'node:fs' import path from 'node:path' + import { createTestClient } from 'apollo-server-testing' -import createServer from '../../server' import gql from 'graphql-tag' +import fetch from 'node-fetch' + +import createServer from '@src/server' jest.mock('node-fetch') const mockedFetch = jest.mocked(fetch) diff --git a/backend/src/schema/resolvers/embeds/findProvider.ts b/backend/src/schema/resolvers/embeds/findProvider.ts index 7bedf2a77..a9a30f2bf 100644 --- a/backend/src/schema/resolvers/embeds/findProvider.ts +++ b/backend/src/schema/resolvers/embeds/findProvider.ts @@ -1,5 +1,6 @@ import fs from 'node:fs' import path from 'node:path' + import { minimatch } from 'minimatch' let oEmbedProvidersFile = fs.readFileSync( diff --git a/backend/src/schema/resolvers/embeds/scraper.ts b/backend/src/schema/resolvers/embeds/scraper.ts index 4771ba160..e4e19e6b9 100644 --- a/backend/src/schema/resolvers/embeds/scraper.ts +++ b/backend/src/schema/resolvers/embeds/scraper.ts @@ -2,13 +2,14 @@ /* eslint-disable n/global-require */ /* eslint-disable import/no-commonjs */ /* eslint-disable import/no-named-as-default */ + +import { ApolloError } from 'apollo-server' +import isArray from 'lodash/isArray' +import isEmpty from 'lodash/isEmpty' +import mergeWith from 'lodash/mergeWith' import Metascraper from 'metascraper' import fetch from 'node-fetch' -import { ApolloError } from 'apollo-server' -import isEmpty from 'lodash/isEmpty' -import isArray from 'lodash/isArray' -import mergeWith from 'lodash/mergeWith' import findProvider from './findProvider' // eslint-disable-next-line import/no-extraneous-dependencies diff --git a/backend/src/schema/resolvers/filter-posts.spec.ts b/backend/src/schema/resolvers/filter-posts.spec.ts index 95a072d8a..d5d4485a3 100644 --- a/backend/src/schema/resolvers/filter-posts.spec.ts +++ b/backend/src/schema/resolvers/filter-posts.spec.ts @@ -1,9 +1,10 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' -import CONFIG from '../../config' -import { filterPosts, createPostMutation } from '../../graphql/posts' + +import CONFIG from '@config/index' +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import { filterPosts, createPostMutation } from '@graphql/posts' +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 c9d8dc1bf..1e05b2fea 100644 --- a/backend/src/schema/resolvers/follow.spec.ts +++ b/backend/src/schema/resolvers/follow.spec.ts @@ -1,9 +1,10 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' -import { getDriver, getNeode } from '../../db/neo4j' -import createServer from '../../server' import gql from 'graphql-tag' +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver, getNeode } from '@db/neo4j' +import createServer from '@src/server' + const driver = getDriver() const neode = getNeode() diff --git a/backend/src/schema/resolvers/follow.ts b/backend/src/schema/resolvers/follow.ts index 6cf4938c7..11447974d 100644 --- a/backend/src/schema/resolvers/follow.ts +++ b/backend/src/schema/resolvers/follow.ts @@ -1,4 +1,4 @@ -import { getNeode } from '../../db/neo4j' +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 1d66b376c..624b09e39 100644 --- a/backend/src/schema/resolvers/groups.spec.ts +++ b/backend/src/schema/resolvers/groups.spec.ts @@ -1,5 +1,8 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' + +import CONFIG from '@config/index' +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' import { createGroupMutation, updateGroupMutation, @@ -9,10 +12,8 @@ import { removeUserFromGroupMutation, groupMembersQuery, groupQuery, -} from '../../graphql/groups' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' -import CONFIG from '../../config' +} from '@graphql/groups' +import createServer from '@src/server' const driver = getDriver() const neode = getNeode() diff --git a/backend/src/schema/resolvers/groups.ts b/backend/src/schema/resolvers/groups.ts index f5282a3bb..8b383e702 100644 --- a/backend/src/schema/resolvers/groups.ts +++ b/backend/src/schema/resolvers/groups.ts @@ -1,9 +1,11 @@ -import { v4 as uuid } from 'uuid' import { UserInputError } from 'apollo-server' -import CONFIG from '../../config' -import { CATEGORIES_MIN, CATEGORIES_MAX } from '../../constants/categories' -import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups' -import { removeHtmlTags } from '../../middleware/helpers/cleanHtml' +import { v4 as uuid } from 'uuid' + +import CONFIG from '@config/index' +import { CATEGORIES_MIN, CATEGORIES_MAX } from '@constants/categories' +import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '@constants/groups' +import { removeHtmlTags } from '@middleware/helpers/cleanHtml' + import Resolver, { removeUndefinedNullValuesFromObject, convertObjectToCypherMapLiteral, diff --git a/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts b/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts index 1d1369e0d..5a53bf9cb 100644 --- a/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts +++ b/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts @@ -1,6 +1,7 @@ -import { getMutedUsers } from '../users' import { mergeWith, isArray } from 'lodash' +import { getMutedUsers } from '@schema/resolvers/users' + export const filterForMutedUsers = async (params, context) => { if (!context.user) return params const [mutedUsers] = await Promise.all([getMutedUsers(context)]) diff --git a/backend/src/schema/resolvers/helpers/generateInviteCode.ts b/backend/src/schema/resolvers/helpers/generateInviteCode.ts index e3f555931..6e580fab9 100644 --- a/backend/src/schema/resolvers/helpers/generateInviteCode.ts +++ b/backend/src/schema/resolvers/helpers/generateInviteCode.ts @@ -1,4 +1,4 @@ -import CONSTANTS_REGISTRATION from '../../../constants/registration' +import CONSTANTS_REGISTRATION from '@constants/registration' export default function generateInviteCode() { // 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z]) diff --git a/backend/src/schema/resolvers/helpers/generateNonce.ts b/backend/src/schema/resolvers/helpers/generateNonce.ts index de1294567..7e0f7542c 100644 --- a/backend/src/schema/resolvers/helpers/generateNonce.ts +++ b/backend/src/schema/resolvers/helpers/generateNonce.ts @@ -1,4 +1,4 @@ -import CONSTANTS_REGISTRATION from '../../../constants/registration' +import CONSTANTS_REGISTRATION from '@constants/registration' // TODO: why this is not used in resolver 'requestPasswordReset'? export default function generateNonce() { diff --git a/backend/src/schema/resolvers/images/images.spec.ts b/backend/src/schema/resolvers/images/images.spec.ts index 94602ccd8..a4eb5b1a5 100644 --- a/backend/src/schema/resolvers/images/images.spec.ts +++ b/backend/src/schema/resolvers/images/images.spec.ts @@ -1,9 +1,11 @@ /* eslint-disable promise/prefer-await-to-callbacks */ -import { deleteImage, mergeImage } from './images' -import { getNeode, getDriver } from '../../../db/neo4j' -import Factory, { cleanDatabase } from '../../../db/factories' import { UserInputError } from 'apollo-server' +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' + +import { deleteImage, mergeImage } from './images' + const driver = getDriver() const neode = getNeode() const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}' diff --git a/backend/src/schema/resolvers/images/images.ts b/backend/src/schema/resolvers/images/images.ts index 5d19d96f7..46eb453c5 100644 --- a/backend/src/schema/resolvers/images/images.ts +++ b/backend/src/schema/resolvers/images/images.ts @@ -1,13 +1,15 @@ /* eslint-disable promise/avoid-new */ /* eslint-disable security/detect-non-literal-fs-filename */ +import { existsSync, unlinkSync, createWriteStream } from 'node:fs' import path from 'node:path' -import { v4 as uuid } from 'uuid' + +import { UserInputError } from 'apollo-server' import { S3 } from 'aws-sdk' import slug from 'slug' -import { existsSync, unlinkSync, createWriteStream } from 'node:fs' -import { UserInputError } from 'apollo-server' -import { getDriver } from '../../../db/neo4j' -import CONFIG from '../../../config' +import { v4 as uuid } from 'uuid' + +import CONFIG from '@config/index' +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 diff --git a/backend/src/schema/resolvers/index.ts b/backend/src/schema/resolvers/index.ts index bc028f0db..9a21f9a9d 100644 --- a/backend/src/schema/resolvers/index.ts +++ b/backend/src/schema/resolvers/index.ts @@ -1,4 +1,5 @@ import path from 'node:path' + import { fileLoader, mergeResolvers } from 'merge-graphql-schemas' // the files must be correctly evaluated in built and dev state - therefore accept both js & ts files diff --git a/backend/src/schema/resolvers/inviteCodes.spec.ts b/backend/src/schema/resolvers/inviteCodes.spec.ts index bd6a55bc8..e1a0dac17 100644 --- a/backend/src/schema/resolvers/inviteCodes.spec.ts +++ b/backend/src/schema/resolvers/inviteCodes.spec.ts @@ -1,10 +1,11 @@ /* eslint-disable security/detect-non-literal-regexp */ -import Factory, { cleanDatabase } from '../../db/factories' -import { getDriver } from '../../db/neo4j' -import gql from 'graphql-tag' -import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' -import CONSTANTS_REGISTRATION from '../../constants/registration' +import gql from 'graphql-tag' + +import CONSTANTS_REGISTRATION from '@constants/registration' +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver } from '@db/neo4j' +import createServer from '@src/server' let user let query diff --git a/backend/src/schema/resolvers/locations.spec.ts b/backend/src/schema/resolvers/locations.spec.ts index 82aebd441..824372d28 100644 --- a/backend/src/schema/resolvers/locations.spec.ts +++ b/backend/src/schema/resolvers/locations.spec.ts @@ -1,8 +1,9 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' 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' let mutate, authenticatedUser diff --git a/backend/src/schema/resolvers/locations.ts b/backend/src/schema/resolvers/locations.ts index fa0feafa1..fcc2fa0aa 100644 --- a/backend/src/schema/resolvers/locations.ts +++ b/backend/src/schema/resolvers/locations.ts @@ -1,4 +1,5 @@ import { UserInputError } from 'apollo-server' + import Resolver from './helpers/Resolver' import { queryLocations } from './users/location' diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts index 83d9fdc6b..4384ddd0f 100644 --- a/backend/src/schema/resolvers/messages.spec.ts +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -1,9 +1,10 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' -import { getNeode, getDriver } from '../../db/neo4j' -import { createRoomMutation, roomQuery } from '../../graphql/rooms' -import { createMessageMutation, messageQuery, markMessagesAsSeen } from '../../graphql/messages' -import createServer, { pubsub } from '../../server' + +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 createServer, { pubsub } from '@src/server' const driver = getDriver() const neode = getNeode() diff --git a/backend/src/schema/resolvers/messages.ts b/backend/src/schema/resolvers/messages.ts index c1381045f..6879c4be9 100644 --- a/backend/src/schema/resolvers/messages.ts +++ b/backend/src/schema/resolvers/messages.ts @@ -1,9 +1,10 @@ -import { neo4jgraphql } from 'neo4j-graphql-js' -import Resolver from './helpers/Resolver' - -import { getUnreadRoomsCount } from './rooms' -import { pubsub, ROOM_COUNT_UPDATED, CHAT_MESSAGE_ADDED } from '../../server' import { withFilter } from 'graphql-subscriptions' +import { neo4jgraphql } from 'neo4j-graphql-js' + +import { pubsub, ROOM_COUNT_UPDATED, 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) => { diff --git a/backend/src/schema/resolvers/moderation.spec.ts b/backend/src/schema/resolvers/moderation.spec.ts index 1665e9446..46befdf10 100644 --- a/backend/src/schema/resolvers/moderation.spec.ts +++ b/backend/src/schema/resolvers/moderation.spec.ts @@ -1,8 +1,9 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const neode = getNeode() const driver = getDriver() diff --git a/backend/src/schema/resolvers/notifications.spec.ts b/backend/src/schema/resolvers/notifications.spec.ts index e3bcb9489..a10f97590 100644 --- a/backend/src/schema/resolvers/notifications.spec.ts +++ b/backend/src/schema/resolvers/notifications.spec.ts @@ -1,13 +1,14 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getDriver } from '../../db/neo4j' import { createTestClient } from 'apollo-server-testing' -import createServer from '../../server' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver } from '@db/neo4j' import { markAsReadMutation, markAllAsReadMutation, notificationQuery, -} from '../../graphql/notifications' +} from '@graphql/notifications' +import createServer from '@src/server' const driver = getDriver() let authenticatedUser diff --git a/backend/src/schema/resolvers/notifications.ts b/backend/src/schema/resolvers/notifications.ts index 6a3e232cc..5dbbe3d40 100644 --- a/backend/src/schema/resolvers/notifications.ts +++ b/backend/src/schema/resolvers/notifications.ts @@ -1,6 +1,8 @@ -import log from './helpers/databaseLogger' import { withFilter } from 'graphql-subscriptions' -import { pubsub, NOTIFICATION_ADDED } from '../../server' + +import { pubsub, NOTIFICATION_ADDED } from '@src/server' + +import log from './helpers/databaseLogger' export default { Subscription: { diff --git a/backend/src/schema/resolvers/observePosts.spec.ts b/backend/src/schema/resolvers/observePosts.spec.ts index 2d98c33a7..13fd5ccfc 100644 --- a/backend/src/schema/resolvers/observePosts.spec.ts +++ b/backend/src/schema/resolvers/observePosts.spec.ts @@ -1,11 +1,11 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' -import { createPostMutation } from '../../graphql/posts' -import CONFIG from '../../config' +import CONFIG from '@config/index' +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import { createPostMutation } from '@graphql/posts' +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 d0ca3e4a8..b5c7e10dd 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.ts +++ b/backend/src/schema/resolvers/passwordReset.spec.ts @@ -1,10 +1,12 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import CONSTANTS_REGISTRATION from '../../constants/registration' -import createPasswordReset from './helpers/createPasswordReset' -import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import CONSTANTS_REGISTRATION from '@constants/registration' +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' + +import createPasswordReset from './helpers/createPasswordReset' const neode = getNeode() const driver = getDriver() diff --git a/backend/src/schema/resolvers/passwordReset.ts b/backend/src/schema/resolvers/passwordReset.ts index 4adca11d3..b9d4d7f51 100644 --- a/backend/src/schema/resolvers/passwordReset.ts +++ b/backend/src/schema/resolvers/passwordReset.ts @@ -1,6 +1,8 @@ -import { v4 as uuid } from 'uuid' import bcrypt from 'bcryptjs' -import CONSTANTS_REGISTRATION from '../../constants/registration' +import { v4 as uuid } from 'uuid' + +import CONSTANTS_REGISTRATION from '@constants/registration' + import createPasswordReset from './helpers/createPasswordReset' export default { diff --git a/backend/src/schema/resolvers/posts.spec.ts b/backend/src/schema/resolvers/posts.spec.ts index d7eb063d2..103ba98c0 100644 --- a/backend/src/schema/resolvers/posts.spec.ts +++ b/backend/src/schema/resolvers/posts.spec.ts @@ -1,10 +1,11 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' -import { createPostMutation } from '../../graphql/posts' -import CONFIG from '../../config' + +import CONFIG from '@config/index' +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import { createPostMutation } from '@graphql/posts' +import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = true diff --git a/backend/src/schema/resolvers/posts.ts b/backend/src/schema/resolvers/posts.ts index ce342cea7..cb48d78ea 100644 --- a/backend/src/schema/resolvers/posts.ts +++ b/backend/src/schema/resolvers/posts.ts @@ -1,15 +1,17 @@ -import { v4 as uuid } from 'uuid' -import { neo4jgraphql } from 'neo4j-graphql-js' -import { isEmpty } from 'lodash' import { UserInputError } from 'apollo-server' -import { mergeImage, deleteImage } from './images/images' -import Resolver from './helpers/Resolver' +import { isEmpty } from 'lodash' +import { neo4jgraphql } from 'neo4j-graphql-js' +import { v4 as uuid } from 'uuid' + +import CONFIG from '@config/index' + +import { validateEventParams } from './helpers/events' import { filterForMutedUsers } from './helpers/filterForMutedUsers' import { filterInvisiblePosts } from './helpers/filterInvisiblePosts' import { filterPostsOfMyGroups } from './helpers/filterPostsOfMyGroups' -import { validateEventParams } from './helpers/events' +import Resolver from './helpers/Resolver' +import { mergeImage, deleteImage } from './images/images' import { createOrUpdateLocations } from './users/location' -import CONFIG from '../../config' const maintainPinnedPosts = (params) => { const pinnedPostFilter = { pinned: true } diff --git a/backend/src/schema/resolvers/postsInGroups.spec.ts b/backend/src/schema/resolvers/postsInGroups.spec.ts index c7fc34ec7..17d4f274e 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.ts +++ b/backend/src/schema/resolvers/postsInGroups.spec.ts @@ -1,28 +1,27 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' + +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' +} from '@graphql/groups' import { createPostMutation, postQuery, filterPosts, profilePagePosts, searchPosts, -} from '../../graphql/posts' -import { createCommentMutation } from '../../graphql/comments' -// eslint-disable-next-line no-unused-vars -import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups' -import CONFIG from '../../config' -import { signupVerificationMutation } from '../../graphql/authentications' +} from '@graphql/posts' +import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false -jest.mock('../../constants/groups', () => { +jest.mock('@constants/groups', () => { return { __esModule: true, DESCRIPTION_WITHOUT_HTML_LENGTH_MIN: 5, diff --git a/backend/src/schema/resolvers/registration.spec.ts b/backend/src/schema/resolvers/registration.spec.ts index 54e7f1ba7..e61460786 100644 --- a/backend/src/schema/resolvers/registration.spec.ts +++ b/backend/src/schema/resolvers/registration.spec.ts @@ -1,9 +1,10 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getDriver, getNeode } from '../../db/neo4j' -import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' -import CONFIG from '../../config' +import gql from 'graphql-tag' + +import CONFIG from '@config/index' +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver, getNeode } from '@db/neo4j' +import createServer from '@src/server' const neode = getNeode() diff --git a/backend/src/schema/resolvers/registration.ts b/backend/src/schema/resolvers/registration.ts index 8d5aac346..fc3fc37bb 100644 --- a/backend/src/schema/resolvers/registration.ts +++ b/backend/src/schema/resolvers/registration.ts @@ -1,8 +1,10 @@ import { UserInputError } from 'apollo-server' -import { getNeode } from '../../db/neo4j' -import encryptPassword from '../../helpers/encryptPassword' -import generateNonce from './helpers/generateNonce' + +import { getNeode } from '@db/neo4j' +import encryptPassword from '@helpers/encryptPassword' + import existingEmailAddress from './helpers/existingEmailAddress' +import generateNonce from './helpers/generateNonce' import normalizeEmail from './helpers/normalizeEmail' const neode = getNeode() @@ -98,7 +100,6 @@ const signupCypher = (inviteCode) => { SET user.updatedAt = toString(datetime()) SET user.allowEmbedIframes = false SET user.showShoutsPublicly = false - SET user.sendNotificationEmails = true SET email.verifiedAt = toString(datetime()) WITH user OPTIONAL MATCH (post:Post)-[:IN]->(group:Group) diff --git a/backend/src/schema/resolvers/reports.spec.ts b/backend/src/schema/resolvers/reports.spec.ts index 2e6b4d302..a57efc011 100644 --- a/backend/src/schema/resolvers/reports.spec.ts +++ b/backend/src/schema/resolvers/reports.spec.ts @@ -1,8 +1,9 @@ import { createTestClient } from 'apollo-server-testing' -import createServer from '../../server' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getDriver, getNeode } from '../../db/neo4j' + +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver, getNeode } from '@db/neo4j' +import createServer from '@src/server' const instance = getNeode() const driver = getDriver() diff --git a/backend/src/schema/resolvers/rewards.spec.ts b/backend/src/schema/resolvers/rewards.spec.ts index 06fe87ec0..2cfe122a0 100644 --- a/backend/src/schema/resolvers/rewards.spec.ts +++ b/backend/src/schema/resolvers/rewards.spec.ts @@ -1,8 +1,9 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const driver = getDriver() const instance = getNeode() diff --git a/backend/src/schema/resolvers/rewards.ts b/backend/src/schema/resolvers/rewards.ts index c271ca8f8..bbb889c41 100644 --- a/backend/src/schema/resolvers/rewards.ts +++ b/backend/src/schema/resolvers/rewards.ts @@ -1,6 +1,7 @@ -import { getNeode } from '../../db/neo4j' import { UserInputError } from 'apollo-server' +import { getNeode } from '@db/neo4j' + const neode = getNeode() const getUserAndBadge = async ({ badgeKey, userId }) => { diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index 2e26dc1e3..87ebb4557 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -1,9 +1,10 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' -import { getNeode, getDriver } from '../../db/neo4j' -import { createRoomMutation, roomQuery, unreadRoomsQuery } from '../../graphql/rooms' -import { createMessageMutation } from '../../graphql/messages' -import createServer from '../../server' + +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 createServer from '@src/server' const driver = getDriver() const neode = getNeode() diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index 5382c5ee7..0ff37b594 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -1,7 +1,9 @@ -import { neo4jgraphql } from 'neo4j-graphql-js' -import Resolver from './helpers/Resolver' -import { pubsub, ROOM_COUNT_UPDATED } from '../../server' import { withFilter } from 'graphql-subscriptions' +import { neo4jgraphql } from 'neo4j-graphql-js' + +import { pubsub, ROOM_COUNT_UPDATED } from '@src/server' + +import Resolver from './helpers/Resolver' export const getUnreadRoomsCount = async (userId, session) => { return session.readTransaction(async (transaction) => { diff --git a/backend/src/schema/resolvers/searches.spec.ts b/backend/src/schema/resolvers/searches.spec.ts index f889c2ac8..5902f2746 100644 --- a/backend/src/schema/resolvers/searches.spec.ts +++ b/backend/src/schema/resolvers/searches.spec.ts @@ -1,8 +1,9 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' 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' let query, authenticatedUser, user diff --git a/backend/src/schema/resolvers/shout.spec.ts b/backend/src/schema/resolvers/shout.spec.ts index 294a28a76..7fe7176ab 100644 --- a/backend/src/schema/resolvers/shout.spec.ts +++ b/backend/src/schema/resolvers/shout.spec.ts @@ -1,8 +1,9 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' let mutate, query, authenticatedUser, variables const instance = getNeode() diff --git a/backend/src/schema/resolvers/socialMedia.spec.ts b/backend/src/schema/resolvers/socialMedia.spec.ts index 8265e8376..3a36e791e 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.ts +++ b/backend/src/schema/resolvers/socialMedia.spec.ts @@ -1,8 +1,9 @@ import { createTestClient } from 'apollo-server-testing' -import createServer from '../../server' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getDriver } from '../../db/neo4j' + +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver } from '@db/neo4j' +import createServer from '@src/server' const driver = getDriver() diff --git a/backend/src/schema/resolvers/socialMedia.ts b/backend/src/schema/resolvers/socialMedia.ts index c5b9dcd91..ac27eb1f9 100644 --- a/backend/src/schema/resolvers/socialMedia.ts +++ b/backend/src/schema/resolvers/socialMedia.ts @@ -1,4 +1,5 @@ -import { getNeode } from '../../db/neo4j' +import { getNeode } from '@db/neo4j' + import Resolver from './helpers/Resolver' const neode = getNeode() diff --git a/backend/src/schema/resolvers/statistics.spec.ts b/backend/src/schema/resolvers/statistics.spec.ts index 15aa2d449..4c8c8aa01 100644 --- a/backend/src/schema/resolvers/statistics.spec.ts +++ b/backend/src/schema/resolvers/statistics.spec.ts @@ -1,8 +1,9 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' let query, authenticatedUser const instance = getNeode() diff --git a/backend/src/schema/resolvers/userData.spec.ts b/backend/src/schema/resolvers/userData.spec.ts index 3c521a4f1..1165ec33c 100644 --- a/backend/src/schema/resolvers/userData.spec.ts +++ b/backend/src/schema/resolvers/userData.spec.ts @@ -1,8 +1,9 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' 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' let query, authenticatedUser diff --git a/backend/src/schema/resolvers/user_management.spec.ts b/backend/src/schema/resolvers/user_management.spec.ts index 797f08126..d9905fd71 100644 --- a/backend/src/schema/resolvers/user_management.spec.ts +++ b/backend/src/schema/resolvers/user_management.spec.ts @@ -1,14 +1,15 @@ /* eslint-disable promise/prefer-await-to-callbacks */ -import jwt from 'jsonwebtoken' -import CONFIG from '../../config' -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { loginMutation } from '../../graphql/userManagement' import { createTestClient } from 'apollo-server-testing' -import createServer, { context } from '../../server' -import encode from '../../jwt/encode' -import { getNeode, getDriver } from '../../db/neo4j' -import { categories } from '../../constants/categories' +import gql from 'graphql-tag' +import jwt from 'jsonwebtoken' + +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 encode from '@jwt/encode' +import createServer, { context } from '@src/server' const neode = getNeode() const driver = getDriver() diff --git a/backend/src/schema/resolvers/user_management.ts b/backend/src/schema/resolvers/user_management.ts index d88eafdae..dfc33b6ae 100644 --- a/backend/src/schema/resolvers/user_management.ts +++ b/backend/src/schema/resolvers/user_management.ts @@ -1,9 +1,11 @@ -import encode from '../../jwt/encode' -import bcrypt from 'bcryptjs' import { AuthenticationError } from 'apollo-server' -import { getNeode } from '../../db/neo4j' -import normalizeEmail from './helpers/normalizeEmail' +import bcrypt from 'bcryptjs' + +import { getNeode } from '@db/neo4j' +import encode from '@jwt/encode' + import log from './helpers/databaseLogger' +import normalizeEmail from './helpers/normalizeEmail' const neode = getNeode() diff --git a/backend/src/schema/resolvers/users.spec.ts b/backend/src/schema/resolvers/users.spec.ts index 09f98ad53..df5a7f785 100644 --- a/backend/src/schema/resolvers/users.spec.ts +++ b/backend/src/schema/resolvers/users.spec.ts @@ -1,9 +1,10 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' -import { categories } from '../../constants/categories' +import gql from 'graphql-tag' + +import { categories } from '@constants/categories' +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const categoryIds = ['cat9'] let user @@ -592,6 +593,220 @@ describe('switch user role', () => { }) }) +let anotherUser +const emailNotificationSettingsQuery = gql` + query ($id: ID!) { + User(id: $id) { + emailNotificationSettings { + type + settings { + name + value + } + } + } + } +` + +const emailNotificationSettingsMutation = gql` + mutation ($id: ID!, $emailNotificationSettings: [EmailNotificationSettingsInput]!) { + UpdateUser(id: $id, emailNotificationSettings: $emailNotificationSettings) { + emailNotificationSettings { + type + settings { + name + value + } + } + } + } +` + +describe('emailNotificationSettings', () => { + beforeEach(async () => { + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + anotherUser = await Factory.build('user', { + id: 'anotherUser', + role: 'anotherUser', + }) + }) + + describe('query the field', () => { + describe('as another user', () => { + it('throws an error', async () => { + authenticatedUser = await anotherUser.toJson() + const targetUser = await user.toJson() + await expect( + query({ query: emailNotificationSettingsQuery, variables: { id: targetUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('as self', () => { + it('returns the emailNotificationSettings', async () => { + authenticatedUser = await user.toJson() + await expect( + query({ query: emailNotificationSettingsQuery, variables: { id: authenticatedUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + emailNotificationSettings: [ + { + type: 'post', + settings: [ + { + name: 'commentOnObservedPost', + value: true, + }, + { + name: 'mention', + value: true, + }, + ], + }, + { + type: 'chat', + settings: [ + { + name: 'chatMessage', + value: true, + }, + ], + }, + { + type: 'group', + settings: [ + { + name: 'groupMemberJoined', + value: true, + }, + { + name: 'groupMemberLeft', + value: true, + }, + { + name: 'groupMemberRemoved', + value: true, + }, + { + name: 'groupMemberRoleChanged', + value: true, + }, + ], + }, + ], + }, + ], + }, + }), + ) + }) + }) + }) + + describe('mutate the field', () => { + const emailNotificationSettings = [{ name: 'mention', value: false }] + + describe('as another user', () => { + it('throws an error', async () => { + authenticatedUser = await anotherUser.toJson() + const targetUser = await user.toJson() + await expect( + mutate({ + mutation: emailNotificationSettingsMutation, + variables: { id: targetUser.id, emailNotificationSettings }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('as self', () => { + it('updates the emailNotificationSettings', async () => { + authenticatedUser = await user.toJson() + await expect( + mutate({ + mutation: emailNotificationSettingsMutation, + variables: { id: authenticatedUser.id, emailNotificationSettings }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + UpdateUser: { + emailNotificationSettings: [ + { + type: 'post', + settings: [ + { + name: 'commentOnObservedPost', + value: true, + }, + { + name: 'mention', + value: false, + }, + ], + }, + { + type: 'chat', + settings: [ + { + name: 'chatMessage', + value: true, + }, + ], + }, + { + type: 'group', + settings: [ + { + name: 'groupMemberJoined', + value: true, + }, + { + name: 'groupMemberLeft', + value: true, + }, + { + name: 'groupMemberRemoved', + value: true, + }, + { + name: 'groupMemberRoleChanged', + value: true, + }, + ], + }, + ], + }, + }, + }), + ) + }) + }) + }) +}) + describe('save category settings', () => { beforeEach(async () => { await Promise.all( diff --git a/backend/src/schema/resolvers/users.ts b/backend/src/schema/resolvers/users.ts index cab0bc8a3..cca8e1278 100644 --- a/backend/src/schema/resolvers/users.ts +++ b/backend/src/schema/resolvers/users.ts @@ -1,9 +1,11 @@ -import { neo4jgraphql } from 'neo4j-graphql-js' -import { getNeode } from '../../db/neo4j' import { UserInputError, ForbiddenError } from 'apollo-server' -import { mergeImage, deleteImage } from './images/images' -import Resolver from './helpers/Resolver' +import { neo4jgraphql } from 'neo4j-graphql-js' + +import { getNeode } from '@db/neo4j' + import log from './helpers/databaseLogger' +import Resolver from './helpers/Resolver' +import { mergeImage, deleteImage } from './images/images' import { createOrUpdateLocations } from './users/location' const neode = getNeode() @@ -150,6 +152,19 @@ export default { } params.termsAndConditionsAgreedAt = new Date().toISOString() } + + const { + emailNotificationSettings, + }: { emailNotificationSettings: { name: string; value: boolean }[] | undefined } = params + delete params.emailNotificationSettings + if (emailNotificationSettings) { + emailNotificationSettings.forEach((setting) => { + params[ + 'emailNotifications' + setting.name.charAt(0).toUpperCase() + setting.name.slice(1) + ] = setting.value + }) + } + const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -355,6 +370,53 @@ export default { const [{ email }] = result.records.map((r) => r.get('e').properties) return email }, + emailNotificationSettings: async (parent, params, context, resolveInfo) => { + return [ + { + type: 'post', + settings: [ + { + name: 'commentOnObservedPost', + value: parent.emailNotificationsCommentOnObservedPost ?? true, + }, + { + name: 'mention', + value: parent.emailNotificationsMention ?? true, + }, + ], + }, + { + type: 'chat', + settings: [ + { + name: 'chatMessage', + value: parent.emailNotificationsChatMessage ?? true, + }, + ], + }, + { + type: 'group', + settings: [ + { + name: 'groupMemberJoined', + value: parent.emailNotificationsGroupMemberJoined ?? true, + }, + { + name: 'groupMemberLeft', + value: parent.emailNotificationsGroupMemberLeft ?? true, + }, + { + name: 'groupMemberRemoved', + value: parent.emailNotificationsGroupMemberRemoved ?? true, + }, + { + name: 'groupMemberRoleChanged', + value: parent.emailNotificationsGroupMemberRoleChanged ?? true, + }, + ], + }, + ] + }, ...Resolver('User', { undefinedToNull: [ 'actorId', @@ -366,7 +428,6 @@ export default { 'termsAndConditionsAgreedAt', 'allowEmbedIframes', 'showShoutsPublicly', - 'sendNotificationEmails', 'locale', ], boolean: { diff --git a/backend/src/schema/resolvers/users/location.spec.ts b/backend/src/schema/resolvers/users/location.spec.ts index f77f8d7f0..4f54dcc06 100644 --- a/backend/src/schema/resolvers/users/location.spec.ts +++ b/backend/src/schema/resolvers/users/location.spec.ts @@ -1,8 +1,9 @@ -import gql from 'graphql-tag' -import Factory, { cleanDatabase } from '../../../db/factories' -import { getNeode, getDriver } from '../../../db/neo4j' import { createTestClient } from 'apollo-server-testing' -import createServer from '../../../server' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const neode = getNeode() const driver = getDriver() diff --git a/backend/src/schema/resolvers/users/location.ts b/backend/src/schema/resolvers/users/location.ts index 49b7c6b1c..b663eebdf 100644 --- a/backend/src/schema/resolvers/users/location.ts +++ b/backend/src/schema/resolvers/users/location.ts @@ -1,12 +1,13 @@ /* eslint-disable promise/avoid-new */ /* eslint-disable promise/prefer-await-to-callbacks */ /* eslint-disable import/no-named-as-default */ -import request from 'request' import { UserInputError } from 'apollo-server' // eslint-disable-next-line import/no-extraneous-dependencies import Debug from 'debug' -import asyncForEach from '../../../helpers/asyncForEach' -import CONFIG from '../../../config' +import request from 'request' + +import CONFIG from '@config/index' +import asyncForEach from '@helpers/asyncForEach' const debug = Debug('human-connection:location') diff --git a/backend/src/schema/resolvers/users/mutedUsers.spec.ts b/backend/src/schema/resolvers/users/mutedUsers.spec.ts index 762893af0..1fda2b392 100644 --- a/backend/src/schema/resolvers/users/mutedUsers.spec.ts +++ b/backend/src/schema/resolvers/users/mutedUsers.spec.ts @@ -1,8 +1,9 @@ import { createTestClient } from 'apollo-server-testing' -import createServer from '../../../server' -import { cleanDatabase } from '../../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../../db/neo4j' + +import { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const driver = getDriver() const neode = getNeode() diff --git a/backend/src/schema/resolvers/viewedTeaserCount.spec.ts b/backend/src/schema/resolvers/viewedTeaserCount.spec.ts index ee90d1a08..ebcb19c4e 100644 --- a/backend/src/schema/resolvers/viewedTeaserCount.spec.ts +++ b/backend/src/schema/resolvers/viewedTeaserCount.spec.ts @@ -1,8 +1,9 @@ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const driver = getDriver() const neode = getNeode() diff --git a/backend/src/schema/types/enum/EmailNotificationSettingsName.gql b/backend/src/schema/types/enum/EmailNotificationSettingsName.gql new file mode 100644 index 000000000..fa1d5846e --- /dev/null +++ b/backend/src/schema/types/enum/EmailNotificationSettingsName.gql @@ -0,0 +1,9 @@ +enum EmailNotificationSettingsName { + commentOnObservedPost + mention + chatMessage + groupMemberJoined + groupMemberLeft + groupMemberRemoved + groupMemberRoleChanged +} \ No newline at end of file diff --git a/backend/src/schema/types/enum/EmailNotificationSettingsType.gql b/backend/src/schema/types/enum/EmailNotificationSettingsType.gql new file mode 100644 index 000000000..70128a6b2 --- /dev/null +++ b/backend/src/schema/types/enum/EmailNotificationSettingsType.gql @@ -0,0 +1,5 @@ +enum EmailNotificationSettingsType { + post + chat + group +} \ No newline at end of file diff --git a/backend/src/schema/types/index.ts b/backend/src/schema/types/index.ts index fe8a6315e..42d813ae4 100644 --- a/backend/src/schema/types/index.ts +++ b/backend/src/schema/types/index.ts @@ -1,4 +1,5 @@ import path from 'node:path' + import { mergeTypes, fileLoader } from 'merge-graphql-schemas' const typeDefs = fileLoader(path.join(__dirname, './**/*.gql')) diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 70b10aa42..37281d6bb 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -19,6 +19,21 @@ enum _UserOrdering { locale_desc } +input EmailNotificationSettingsInput { + name: EmailNotificationSettingsName + value: Boolean +} + +type EmailNotificationSettings { + type: EmailNotificationSettingsType + settings: [EmailNotificationSettingsOption] @neo4j_ignore +} + +type EmailNotificationSettingsOption { + name: EmailNotificationSettingsName + value: Boolean +} + type User { id: ID! actorId: String @@ -46,7 +61,7 @@ type User { allowEmbedIframes: Boolean showShoutsPublicly: Boolean - sendNotificationEmails: Boolean + emailNotificationSettings: [EmailNotificationSettings]! @neo4j_ignore locale: String friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)") @@ -206,7 +221,7 @@ type Mutation { termsAndConditionsAgreedAt: String allowEmbedIframes: Boolean showShoutsPublicly: Boolean - sendNotificationEmails: Boolean + emailNotificationSettings: [EmailNotificationSettingsInput] locale: String ): User diff --git a/backend/src/server.spec.ts b/backend/src/server.spec.ts index 6d4ef546d..1d5c5aca8 100644 --- a/backend/src/server.spec.ts +++ b/backend/src/server.spec.ts @@ -1,4 +1,5 @@ import { createTestClient } from 'apollo-server-testing' + import createServer from './server' /** diff --git a/backend/src/server.ts b/backend/src/server.ts index 86c0c3658..117e0c3b6 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -1,19 +1,21 @@ /* eslint-disable import/no-named-as-default-member */ -import express from 'express' import http from 'node:http' -import helmet from 'helmet' + import { ApolloServer } from 'apollo-server-express' -import CONFIG from './config' -// eslint-disable-next-line import/no-cycle -import middleware from './middleware' -import { getNeode, getDriver } from './db/neo4j' -import decode from './jwt/decode' -import schema from './schema' +import bodyParser from 'body-parser' +import express from 'express' import { RedisPubSub } from 'graphql-redis-subscriptions' import { PubSub } from 'graphql-subscriptions' -import Redis from 'ioredis' -import bodyParser from 'body-parser' import { graphqlUploadExpress } from 'graphql-upload' +import helmet from 'helmet' +import Redis from 'ioredis' + +import CONFIG from './config' +import { getNeode, getDriver } from './db/neo4j' +import decode from './jwt/decode' +// eslint-disable-next-line import/no-cycle +import middleware from './middleware' +import schema from './schema' export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED' export const CHAT_MESSAGE_ADDED = 'CHAT_MESSAGE_ADDED' diff --git a/backend/tsconfig.json b/backend/tsconfig.json index b6f3526a3..7de3aad0c 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -29,7 +29,19 @@ // "rootDir": "./", /* Specify the root folder within your source files. */ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ + "@config/*": ["./src/config/*"], + "@constants/*": ["./src/constants/*"], + "@db/*": ["./src/db/*"], + "@graphql/*": ["./src/graphql/*"], + "@helpers/*": ["./src/helpers/*"], + "@jwt/*": ["./src/jwt/*"], + "@middleware/*": ["./src/middleware/*"], + "@models/*": ["./src/models/*"], + "@schema/*": ["./src/schema/*"], + "@src/*": ["./src/*"], + "@root/*": ["./*"] + }, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ diff --git a/backend/yarn.lock b/backend/yarn.lock index 204fe73bd..3588f5f26 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -3471,7 +3471,7 @@ cheerio@~1.0.0: undici "^6.19.5" whatwg-mimetype "^4.0.0" -chokidar@^3.5.2, chokidar@^3.6.0: +chokidar@^3.5.2, chokidar@^3.5.3, chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -3614,6 +3614,11 @@ commander@^6.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== +commander@^9.0.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -5475,7 +5480,7 @@ globby@11.0.0: merge2 "^1.3.0" slash "^3.0.0" -globby@^11.1.0: +globby@^11.0.4, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -6989,7 +6994,7 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" -json5@^2.2.3: +json5@^2.2.2, json5@^2.2.3, json5@^2.x: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -7686,6 +7691,11 @@ mustache@^4.2.0: resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== +mylas@^2.1.9: + version "2.1.13" + resolved "https://registry.yarnpkg.com/mylas/-/mylas-2.1.13.tgz#1e23b37d58fdcc76e15d8a5ed23f9ae9fc0cbdf4" + integrity sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg== + n-gram@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/n-gram/-/n-gram-1.1.1.tgz#a374dc176a9063a2388d1be18ed7c35828be2a97" @@ -8370,6 +8380,13 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +plimit-lit@^1.2.6: + version "1.6.1" + resolved "https://registry.yarnpkg.com/plimit-lit/-/plimit-lit-1.6.1.tgz#a34594671b31ee8e93c72d505dfb6852eb72374a" + integrity sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA== + dependencies: + queue-lit "^1.5.1" + possible-typed-array-names@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" @@ -8523,6 +8540,11 @@ querystring@0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= +queue-lit@^1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/queue-lit/-/queue-lit-1.5.2.tgz#83c24d4f4764802377b05a6e5c73017caf3f8747" + integrity sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw== + quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" @@ -8743,6 +8765,13 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-json5@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/require-json5/-/require-json5-1.3.0.tgz#b47d236194e458f144c615dd061bdca085628474" + integrity sha512-FkOrdR0kqHFwIqrlifaXNg6fdg2YcUL5lX9bYlaENKLlWp+g0GO/tRMAvoWIM2pYzAGp57oF/jgkVLwxGk7KyQ== + dependencies: + json5 "^2.x" + resolve-alpn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.0.tgz#058bb0888d1cd4d12474e9a4b6eb17bdd5addc44" @@ -9779,6 +9808,19 @@ 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== + dependencies: + chokidar "^3.5.3" + commander "^9.0.0" + get-tsconfig "^4.10.0" + globby "^11.0.4" + mylas "^2.1.9" + normalize-path "^3.0.0" + plimit-lit "^1.2.6" + tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" @@ -9789,6 +9831,15 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" +tsconfig-paths@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + tslib@1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" diff --git a/cypress/e2e/User.SettingNotifications.feature b/cypress/e2e/User.SettingNotifications.feature.broken similarity index 100% rename from cypress/e2e/User.SettingNotifications.feature rename to cypress/e2e/User.SettingNotifications.feature.broken diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js index 4b743a0e3..8ad247ad1 100644 --- a/webapp/graphql/User.js +++ b/webapp/graphql/User.js @@ -46,7 +46,6 @@ export const profileUserQuery = (i18n) => { url } showShoutsPublicly - sendNotificationEmails } } ` @@ -335,7 +334,7 @@ export const updateUserMutation = () => { $about: String $allowEmbedIframes: Boolean $showShoutsPublicly: Boolean - $sendNotificationEmails: Boolean + $emailNotificationSettings: [EmailNotificationSettingsInput] $termsAndConditionsAgreedVersion: String $avatar: ImageInput $locationName: String # empty string '' sets it to null @@ -347,7 +346,7 @@ export const updateUserMutation = () => { about: $about allowEmbedIframes: $allowEmbedIframes showShoutsPublicly: $showShoutsPublicly - sendNotificationEmails: $sendNotificationEmails + emailNotificationSettings: $emailNotificationSettings termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion avatar: $avatar locationName: $locationName @@ -359,7 +358,13 @@ export const updateUserMutation = () => { about allowEmbedIframes showShoutsPublicly - sendNotificationEmails + emailNotificationSettings { + type + settings { + name + value + } + } locale termsAndConditionsAgreedVersion avatar { @@ -390,7 +395,13 @@ export const currentUserQuery = gql` locale allowEmbedIframes showShoutsPublicly - sendNotificationEmails + emailNotificationSettings { + type + settings { + name + value + } + } termsAndConditionsAgreedVersion socialMedia { id diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 518ba99a9..42f6ab74f 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -1039,9 +1039,23 @@ }, "name": "Einstellungen", "notifications": { - "name": "Benachrichtigungen", + "chat": "Chat", + "chatMessage": "Nachricht erhalten während Abwesenheit", + "checkAll": "Alle auswählen", + "commentOnObservedPost": "Kommentare zu beobachteten Beiträgen", + "group": "Gruppen", + "groupMemberJoined": "Ein Mitglied ist deiner Gruppe beigetreten", + "groupMemberLeft": "Ein Mitglied hat deine Gruppe verlassen", + "groupMemberRemoved": "Du wurdest aus einer Gruppe entfernt", + "groupMemberRoleChanged": "Deine Rolle in einer Gruppe wurde geändert", + "mention": "Ich wurde erwähnt", + "name": "Benachrichtigungen per Email", + "post": "Beiträge und Kommentare", + "postByFollowedUser": "Beitrag von einem Nutzer, dem ich folge", + "postInGroup": "Beitrag in einer Gruppe, die ich beobachte", "send-email-notifications": "Sende E-Mail-Benachrichtigungen", - "success-update": "Benachrichtigungs-Einstellungen gespeichert!" + "success-update": "Benachrichtigungs-Einstellungen gespeichert!", + "uncheckAll": "Alle abwählen" }, "organizations": { "name": "Meine Organisationen" diff --git a/webapp/locales/en.json b/webapp/locales/en.json index f78728c4f..714c3f3c0 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -1039,9 +1039,23 @@ }, "name": "Settings", "notifications": { - "name": "Notifications", + "chat": "Chat", + "chatMessage": "Message received while absent", + "checkAll": "Check all", + "commentOnObservedPost": "Comments on observed posts", + "group": "Groups", + "groupMemberJoined": "Member joined a group I own", + "groupMemberLeft": "Member left a group I own", + "groupMemberRemoved": "I was removed from a group", + "groupMemberRoleChanged": "My role in a group was changed", + "mention": "I was mentioned", + "name": "Email Notifications", + "post": "Posts and comments", + "postByFollowedUser": "Posts by users I follow", + "postInGroup": "Post in a group I am a member of", "send-email-notifications": "Send e-mail notifications", - "success-update": "Notifications settings saved!" + "success-update": "Notifications settings saved!", + "uncheckAll": "Uncheck all" }, "organizations": { "name": "My Organizations" diff --git a/webapp/locales/es.json b/webapp/locales/es.json index a085a53e0..f0a1a866b 100644 --- a/webapp/locales/es.json +++ b/webapp/locales/es.json @@ -1039,9 +1039,23 @@ }, "name": "Configuración", "notifications": { - "name": null, - "send-email-notifications": null, - "success-update": null + "chat": "Chat", + "chatMessage": "Mensaje recibido mientras estaba ausente", + "checkAll": "Seleccionar todo", + "commentOnObservedPost": "Comentario en una contribución que estoy observando", + "group": "Grupos", + "groupMemberJoined": "Un nuevo miembro se unió a un grupo mio", + "groupMemberLeft": "Un miembro dejó un grupo mio", + "groupMemberRemoved": "Fui eliminado de un grupo", + "groupMemberRoleChanged": "Mi rol en un grupo ha cambiado", + "mention": "Mencionado en una contribución", + "name": "Notificaciones por correo electrónico", + "post": "Entradas y comentarios", + "postByFollowedUser": "Posts by users I follow", + "postInGroup": "Post en un grupo del que soy miembro", + "send-email-notifications": "Enviar notificaciones por correo electrónico", + "success-update": "¡Configuración de notificaciones guardada!", + "uncheckAll": "Deseleccionar todo" }, "organizations": { "name": "Mis organizaciones" diff --git a/webapp/locales/fr.json b/webapp/locales/fr.json index f1b5642de..a31e197a1 100644 --- a/webapp/locales/fr.json +++ b/webapp/locales/fr.json @@ -1039,9 +1039,23 @@ }, "name": "Paramètres", "notifications": { - "name": null, - "send-email-notifications": null, - "success-update": null + "chat": "Chat", + "chatMessage": "Message reçu pendant l'absence", + "checkAll": "Tout cocher", + "commentOnObservedPost": "Commentez une contribution que je suis", + "group": "Groups", + "groupMemberJoined": "Un nouveau membre a rejoint un de mes groupes", + "groupMemberLeft": "Un membre a quitté un de mes groupes", + "groupMemberRemoved": "J'ai été retiré d'un groupe", + "groupMemberRoleChanged": "Mon rôle au sein d'un groupe a changé", + "mention": "Mentionné dans une contribution", + "name": "Notifications par mail", + "post": "Messages et commentaires", + "postByFollowedUser": "Messages des utilisateurs que je suis", + "postInGroup": "Message dans un groupe dont je suis membre", + "send-email-notifications": "Envoyer des notifications par courrier électronique", + "success-update": "Paramètres de notification sauvegardés ! ", + "uncheckAll": "Tout décocher" }, "organizations": { "name": "Mes organisations" diff --git a/webapp/locales/it.json b/webapp/locales/it.json index 54248e6ee..8f14dfda2 100644 --- a/webapp/locales/it.json +++ b/webapp/locales/it.json @@ -1039,9 +1039,23 @@ }, "name": "Impostazioni", "notifications": { - "name": null, - "send-email-notifications": null, - "success-update": null + "chat": "Chat", + "chatMessage": "Messaggio ricevuto durante l'assenza", + "checkAll": "Seleziona tutto", + "commentOnObservedPost": "Commenta un contributo che sto guardando", + "group": "Gruppi", + "groupMemberJoined": "Un nuovo membro si è unito a un mio gruppo", + "groupMemberLeft": "Un membro ha lasciato un mio gruppo", + "groupMemberRemoved": "Sono stato rimosso da un gruppo", + "groupMemberRoleChanged": "Il mio ruolo in un gruppo è cambiato", + "mention": "Menzionato in un contributo", + "name": "Notifiche via e-mail", + "post": "Messaggi e commenti", + "postByFollowedUser": "Messaggi di utenti che seguo", + "postInGroup": "Post in un gruppo di cui sono membro", + "send-email-notifications": "Invia notifiche via e-mail", + "success-update": "Impostazioni di notifica salvate! ", + "uncheckAll": "Deseleziona tutto" }, "organizations": { "name": "Mie organizzazioni" diff --git a/webapp/locales/nl.json b/webapp/locales/nl.json index 7907ce052..e332d38dc 100644 --- a/webapp/locales/nl.json +++ b/webapp/locales/nl.json @@ -1039,9 +1039,23 @@ }, "name": "Instellingen", "notifications": { - "name": null, - "send-email-notifications": null, - "success-update": null + "chat": "Chat", + "chatMessage": "Bericht ontvangen tijdens afwezigheid", + "checkAll": "Vink alles aan", + "commentOnObservedPost": "Geef commentaar op een bijdrage die ik volg", + "group": "Groepen", + "groupMemberJoined": "Een nieuw lid is lid geworden van een groep van mij", + "groupMemberLeft": "Een lid heeft een groep van mij verlaten", + "groupMemberRemoved": "Ik ben verwijderd uit een groep", + "groupMemberRoleChanged": "Mijn rol in een groep is veranderd", + "mention": "Genoemd in een bijdrage", + "name": "Email Meldingen", + "post": "Berichten en reacties", + "postByFollowedUser": "Berichten van gebruikers die ik volg", + "postInGroup": "Bericht in een groep waar ik lid van ben", + "send-email-notifications": "E-mailmeldingen verzenden", + "success-update": "Meldingsinstellingen opgeslagen! ", + "uncheckAll": "Vink alles uit" }, "organizations": { "name": "Mijn Organisaties" diff --git a/webapp/locales/pl.json b/webapp/locales/pl.json index 7a800b3d0..5c636dfab 100644 --- a/webapp/locales/pl.json +++ b/webapp/locales/pl.json @@ -1039,9 +1039,23 @@ }, "name": "Ustawienia", "notifications": { - "name": null, - "send-email-notifications": null, - "success-update": null + "chat": "Chat", + "chatMessage": "Wiadomość otrzymana podczas nieobecności", + "checkAll": "Wybierz wszystko", + "commentOnObservedPost": "Skomentuj wpis, który obserwuję", + "group": "Grupy", + "groupMemberJoined": "Nowy członek dołączył do mojej grupy", + "groupMemberLeft": "Członek opuścił moją grupę", + "groupMemberRemoved": "Zostałem usunięty z grupy", + "groupMemberRoleChanged": "Moja rola w grupie uległa zmianie", + "mention": "Mentioned in a contribution", + "name": "Powiadomienia e-mail", + "post": "Posty", + "postByFollowedUser": "Posty użytkowników, których obserwuję", + "postInGroup": "Posty w grupie, której jestem członkiem", + "send-email-notifications": "Wyślij powiadomienia e-mail", + "success-update": "Ustawienia powiadomień zapisane! ", + "uncheckAll": "Odznacz wszystko" }, "organizations": { "name": "My Organizations" diff --git a/webapp/locales/pt.json b/webapp/locales/pt.json index c0bb8a500..c00acbf0a 100644 --- a/webapp/locales/pt.json +++ b/webapp/locales/pt.json @@ -1039,9 +1039,23 @@ }, "name": "Configurações", "notifications": { - "name": null, - "send-email-notifications": null, - "success-update": null + "chat": "Chat", + "chatMessage": "Mensagem recebida durante a ausência", + "checkAll": "Marcar tudo", + "commentOnObservedPost": "Comentários sobre as mensagens observadas", + "group": "Grupos", + "groupMemberJoined": "Member joined a group I own", + "groupMemberLeft": "Membro saiu de um grupo de que sou proprietário", + "groupMemberRemoved": "Fui removido de um grupo", + "groupMemberRoleChanged": "O meu papel num grupo foi alterado", + "mention": "Fui mencionado", + "name": "Notificações por correio eletrónico", + "post": "Posts e comentários", + "postByFollowedUser": "Publicações de utilizadores que sigo", + "postInGroup": "Postar num grupo de que sou membro", + "send-email-notifications": "Enviar notificações por correio eletrónico", + "success-update": "Definições de notificações guardadas!", + "uncheckAll": "Desmarcar tudo" }, "organizations": { "name": "Minhas Organizações" diff --git a/webapp/locales/ru.json b/webapp/locales/ru.json index bebae1012..5775264fa 100644 --- a/webapp/locales/ru.json +++ b/webapp/locales/ru.json @@ -1039,9 +1039,23 @@ }, "name": "Настройки", "notifications": { - "name": null, - "send-email-notifications": null, - "success-update": null + "chat": "Чат", + "chatMessage": "Сообщение, полученное во время отсутствия", + "checkAll": "Отметить все", + "commentOnObservedPost": "Комментарии по поводу замеченных сообщений", + "group": "Группы", + "groupMemberJoined": "Участник присоединился к группе, которой я владею", + "groupMemberLeft": "Участник вышел из группы, которой владею", + "groupMemberRemoved": "Был удален из группы", + "groupMemberRoleChanged": "Моя роль в группе была изменена", + "mention": "Упоминание в вкладе", + "name": "Уведомления", + "post": "Сообщения и комментарии", + "postByFollowedUser": "Сообщения пользователей, за которыми я слежу", + "postInGroup": "Сообщение в группе, членом которой я являюсь", + "send-email-notifications": "Отправлять уведомления по электронной почте", + "success-update": "Настройки уведомлений сохранены! ", + "uncheckAll": "Снимите все флажки" }, "organizations": { "name": "Мои организации" diff --git a/webapp/package.json b/webapp/package.json index 8ae97ec3e..f1c3778d0 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -18,6 +18,7 @@ "locales:normalize": "../scripts/translations/normalize.sh", "precommit": "yarn lint", "test": "cross-env NODE_ENV=test jest --coverage --forceExit --detectOpenHandles", + "test:unit:update": "yarn test -- --updateSnapshot", "test:unit:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --no-cache --runInBand" }, "dependencies": { @@ -78,6 +79,7 @@ "@storybook/addon-actions": "^5.3.21", "@storybook/addon-notes": "^5.3.18", "@storybook/vue": "~7.4.0", + "@testing-library/vue": "5", "@vue/cli-shared-utils": "~4.3.1", "@vue/eslint-config-prettier": "~6.0.0", "@vue/server-test-utils": "~1.0.0-beta.31", diff --git a/webapp/pages/settings/__snapshots__/notifications.spec.js.snap b/webapp/pages/settings/__snapshots__/notifications.spec.js.snap new file mode 100644 index 000000000..0b70393ee --- /dev/null +++ b/webapp/pages/settings/__snapshots__/notifications.spec.js.snap @@ -0,0 +1,197 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`notifications.vue mount renders 1`] = ` +
+

+ settings.notifications.name +

+ +
+
+

+ settings.notifications.post +

+
+ +
+
+ + + +
+
+ + + +
+
+
+
+
+

+ settings.notifications.group +

+
+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + + + + + + + +
+`; diff --git a/webapp/pages/settings/notifications.spec.js b/webapp/pages/settings/notifications.spec.js index 855505fe2..a16e99ed4 100644 --- a/webapp/pages/settings/notifications.spec.js +++ b/webapp/pages/settings/notifications.spec.js @@ -1,5 +1,6 @@ import Vuex from 'vuex' import { mount } from '@vue/test-utils' +import { render, fireEvent, screen } from '@testing-library/vue' import Notifications from './notifications.vue' const localVue = global.localVue @@ -11,7 +12,7 @@ describe('notifications.vue', () => { beforeEach(() => { mocks = { - $t: jest.fn(), + $t: jest.fn((v) => v), $apollo: { mutate: jest.fn(), }, @@ -26,7 +27,42 @@ describe('notifications.vue', () => { return { id: 'u343', name: 'MyAccount', - sendNotificationEmails: true, + emailNotificationSettings: [ + { + type: 'post', + settings: [ + { + name: 'commentOnObservedPost', + value: true, + }, + { + name: 'mention', + value: false, + }, + ], + }, + { + type: 'group', + settings: [ + { + name: 'groupMemberJoined', + value: true, + }, + { + name: 'groupMemberLeft', + value: true, + }, + { + name: 'groupMemberRemoved', + value: false, + }, + { + name: 'groupMemberRoleChanged', + value: true, + }, + ], + }, + ], } }, }, @@ -47,21 +83,116 @@ describe('notifications.vue', () => { }) it('renders', () => { - expect(wrapper.classes('base-card')).toBe(true) + expect(wrapper.element).toMatchSnapshot() + }) + }) + + describe('Notifications', () => { + beforeEach(() => { + render(Notifications, { + store, + mocks, + localVue, + }) }) - it('clicking on submit changes notifyByEmail to false', async () => { - await wrapper.find('#send-email').setChecked(false) - await wrapper.find('.base-button').trigger('click') - expect(wrapper.vm.notifyByEmail).toBe(false) + it('check all button works', async () => { + const button = screen.getByText('settings.notifications.checkAll') + await fireEvent.click(button) + + const checkboxes = screen.getAllByRole('checkbox') + for (const checkbox of checkboxes) { + expect(checkbox.checked).toEqual(true) + } + + // Check that the button is disabled + expect(button.disabled).toBe(true) }) - it('clicking on submit with a server error shows a toast and notifyByEmail is still true', async () => { + it('uncheck all button works', async () => { + const button = screen.getByText('settings.notifications.uncheckAll') + await fireEvent.click(button) + + const checkboxes = screen.getAllByRole('checkbox') + for (const checkbox of checkboxes) { + expect(checkbox.checked).toEqual(false) + } + + // Check that the button is disabled + expect(button.disabled).toBe(true) + }) + + it('clicking on submit keeps set values and shows success message', async () => { + mocks.$apollo.mutate = jest.fn().mockResolvedValue({ + data: { + UpdateUser: { + emailNotificationSettings: [ + { + type: 'post', + settings: [ + { + name: 'commentOnObservedPost', + value: false, + }, + { + name: 'mention', + value: false, + }, + ], + }, + { + type: 'group', + settings: [ + { + name: 'groupMemberJoined', + value: true, + }, + { + name: 'groupMemberLeft', + value: true, + }, + { + name: 'groupMemberRemoved', + value: false, + }, + { + name: 'groupMemberRoleChanged', + value: true, + }, + ], + }, + ], + }, + }, + }) + + // Change some value to enable save button + const checkbox = screen.getAllByRole('checkbox')[0] + await fireEvent.click(checkbox) + + const newValue = checkbox.checked + + // Click save button + const button = screen.getByText('actions.save') + await fireEvent.click(button) + + expect(checkbox.checked).toEqual(newValue) + + expect(mocks.$toast.success).toHaveBeenCalledWith('settings.notifications.success-update') + }) + + it('clicking on submit with a server error shows a toast', async () => { mocks.$apollo.mutate = jest.fn().mockRejectedValue({ message: 'Ouch!' }) - await wrapper.find('#send-email').setChecked(false) - await wrapper.find('.base-button').trigger('click') + + // Change some value to enable save button + const checkbox = screen.getAllByRole('checkbox')[0] + await fireEvent.click(checkbox) + + // Click save button + const button = screen.getByText('actions.save') + await fireEvent.click(button) + expect(mocks.$toast.error).toHaveBeenCalledWith('Ouch!') - expect(wrapper.vm.notifyByEmail).toBe(true) }) }) }) diff --git a/webapp/pages/settings/notifications.vue b/webapp/pages/settings/notifications.vue index a2828a1a9..35249a37d 100644 --- a/webapp/pages/settings/notifications.vue +++ b/webapp/pages/settings/notifications.vue @@ -1,11 +1,26 @@