diff --git a/backend/src/graphql/queries/statistics.ts b/backend/src/graphql/queries/statistics.ts new file mode 100644 index 000000000..0463b63a4 --- /dev/null +++ b/backend/src/graphql/queries/statistics.ts @@ -0,0 +1,29 @@ +import gql from 'graphql-tag' + +export const statistics = gql` + query statistics { + statistics { + users + usersDeleted + posts + comments + notifications + emails + follows + shouts + invites + chatMessages + chatRooms + tags + locations + groups + inviteCodes + inviteCodesExpired + inviteCodesRedeemed + badgesRewarded + badgesDisplayed + usersVerified + reports + } + } +` diff --git a/backend/src/graphql/resolvers/statistics.spec.ts b/backend/src/graphql/resolvers/statistics.spec.ts index 50f124ac9..f67552f39 100644 --- a/backend/src/graphql/resolvers/statistics.spec.ts +++ b/backend/src/graphql/resolvers/statistics.spec.ts @@ -2,52 +2,39 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ApolloServer } from 'apollo-server-express' import { createTestClient } from 'apollo-server-testing' -import gql from 'graphql-tag' +import databaseContext from '@context/database' import Factory, { cleanDatabase } from '@db/factories' -import { getNeode, getDriver } from '@db/neo4j' -import createServer from '@src/server' +import { statistics } from '@graphql/queries/statistics' +import createServer, { getContext } from '@src/server' +const database = databaseContext() + +let server: ApolloServer let query, authenticatedUser -const instance = getNeode() -const driver = getDriver() -const statisticsQuery = gql` - query { - statistics { - countUsers - countPosts - countComments - countNotifications - countInvites - countFollows - countShouts - } - } -` beforeAll(async () => { await cleanDatabase() - authenticatedUser = undefined - const { server } = createServer({ - context: () => { - return { - driver, - neode: instance, - user: authenticatedUser, - } - }, - }) - query = createTestClient(server).query + // eslint-disable-next-line @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query }) afterAll(async () => { await cleanDatabase() - await driver.close() + void server.stop() + void database.driver.close() + database.neode.close() }) -// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 afterEach(async () => { await cleanDatabase() }) @@ -63,8 +50,8 @@ describe('statistics', () => { }) it('returns the count of all users', async () => { - await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ - data: { statistics: { countUsers: 6 } }, + await expect(query({ query: statistics })).resolves.toMatchObject({ + data: { statistics: { users: 6 } }, errors: undefined, }) }) @@ -80,8 +67,8 @@ describe('statistics', () => { }) it('returns the count of all posts', async () => { - await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ - data: { statistics: { countPosts: 3 } }, + await expect(query({ query: statistics })).resolves.toMatchObject({ + data: { statistics: { posts: 3 } }, errors: undefined, }) }) @@ -97,8 +84,8 @@ describe('statistics', () => { }) it('returns the count of all comments', async () => { - await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ - data: { statistics: { countComments: 2 } }, + await expect(query({ query: statistics })).resolves.toMatchObject({ + data: { statistics: { comments: 2 } }, errors: undefined, }) }) @@ -116,8 +103,8 @@ describe('statistics', () => { }) it('returns the count of all follows', async () => { - await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ - data: { statistics: { countFollows: 1 } }, + await expect(query({ query: statistics })).resolves.toMatchObject({ + data: { statistics: { follows: 1 } }, errors: undefined, }) }) @@ -143,8 +130,8 @@ describe('statistics', () => { }) it('returns the count of all shouts', async () => { - await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ - data: { statistics: { countShouts: 2 } }, + await expect(query({ query: statistics })).resolves.toMatchObject({ + data: { statistics: { shouts: 2 } }, errors: undefined, }) }) diff --git a/backend/src/graphql/resolvers/statistics.ts b/backend/src/graphql/resolvers/statistics.ts index f7af390bf..e8ffc156c 100644 --- a/backend/src/graphql/resolvers/statistics.ts +++ b/backend/src/graphql/resolvers/statistics.ts @@ -1,48 +1,83 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable security/detect-object-injection */ +/* eslint-disable @typescript-eslint/dot-notation */ +import { Context } from '@src/server' + export default { Query: { - statistics: async (_parent, _args, { driver }) => { - const session = driver.session() - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const counts: any = {} - try { - const mapping = { - countUsers: 'User', - countPosts: 'Post', - countComments: 'Comment', - countNotifications: 'NOTIFIED', - countEmails: 'EmailAddress', - countFollows: 'FOLLOWS', - countShouts: 'SHOUTED', - } - const statisticsReadTxResultPromise = session.readTransaction(async (transaction) => { - const statisticsTransactionResponse = await transaction.run( - ` - CALL apoc.meta.stats() YIELD labels, relTypesCount - RETURN labels, relTypesCount - `, - ) - return statisticsTransactionResponse.records.map((record) => { - return { - ...record.get('labels'), - ...record.get('relTypesCount'), - } - }) - }) - const [statistics] = await statisticsReadTxResultPromise - Object.keys(mapping).forEach((key) => { - const stat = statistics[mapping[key]] - counts[key] = stat ? stat.toNumber() : 0 - }) - counts.countInvites = counts.countEmails - counts.countUsers - return counts - } finally { - session.close() + statistics: async (_parent, _args, context: Context) => { + const statistics = { + users: 0, + usersDeleted: 0, + posts: 0, + comments: 0, + notifications: 0, + emails: 0, + follows: 0, + shouts: 0, + invites: 0, + chatMessages: 0, + chatRooms: 0, + tags: 0, + locations: 0, + groups: 0, + inviteCodes: 0, + inviteCodesExpired: 0, + inviteCodesRedeemed: 0, + badgesRewarded: 0, + badgesDisplayed: 0, + usersVerified: 0, + reports: 0, } + const [metaStats] = ( + await context.database.query({ + query: `CALL apoc.meta.stats() YIELD labels, relTypesCount + RETURN labels, relTypesCount`, + }) + ).records.map((record) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { ...record.get('labels'), ...record.get('relTypesCount') } + }) + + const deletedUsers = parseInt( + ( + await context.database.query({ + query: `MATCH (u:User) WHERE NOT (u)-[:PRIMARY_EMAIL]->(:EmailAddress) RETURN toString(count(u)) AS count`, + }) + ).records[0].get('count') as string, + ) + + const invalidInviteCodes = parseInt( + ( + await context.database.query({ + query: `MATCH (i:InviteCode) WHERE NOT i.expiresAt IS NULL OR i.expiresAt >= datetime() RETURN toString(count(i)) AS count`, + }) + ).records[0].get('count') as string, + ) + + statistics.users = (metaStats['User']?.toNumber() ?? 0) - deletedUsers + statistics.usersDeleted = deletedUsers + statistics.posts = metaStats['Post']?.toNumber() ?? 0 + statistics.comments = metaStats['Comment']?.toNumber() ?? 0 + statistics.notifications = metaStats['NOTIFIED']?.toNumber() ?? 0 + statistics.emails = metaStats['EmailAddress']?.toNumber() ?? 0 + statistics.follows = metaStats['FOLLOWS']?.toNumber() ?? 0 + statistics.shouts = metaStats['SHOUTED']?.toNumber() ?? 0 + statistics.invites = statistics.emails - statistics.users + statistics.chatMessages = metaStats['Message']?.toNumber() ?? 0 + statistics.chatRooms = metaStats['Room']?.toNumber() ?? 0 + statistics.tags = metaStats['Tag']?.toNumber() ?? 0 + statistics.locations = metaStats['Location']?.toNumber() ?? 0 + statistics.groups = metaStats['Group']?.toNumber() ?? 0 + statistics.inviteCodes = (metaStats['InviteCode']?.toNumber() ?? 0) - invalidInviteCodes + statistics.inviteCodesExpired = invalidInviteCodes + statistics.inviteCodesRedeemed = metaStats['REDEEMED']?.toNumber() ?? 0 + statistics.badgesRewarded = metaStats['REWARDED']?.toNumber() ?? 0 + statistics.badgesDisplayed = metaStats['SELECTED']?.toNumber() ?? 0 + statistics.usersVerified = metaStats['VERIFIES']?.toNumber() ?? 0 + statistics.reports = metaStats['InviteCode']?.toNumber() ?? 0 + return statistics }, }, } diff --git a/backend/src/graphql/types/type/Statistics.gql b/backend/src/graphql/types/type/Statistics.gql index 3963a3e50..d01ff194b 100644 --- a/backend/src/graphql/types/type/Statistics.gql +++ b/backend/src/graphql/types/type/Statistics.gql @@ -3,12 +3,26 @@ type Query { } type Statistics { - countUsers: Int! - countPosts: Int! - countComments: Int! - countNotifications: Int! - countInvites: Int! - countFollows: Int! - countShouts: Int! + users: Int! + usersDeleted: Int! + posts: Int! + comments: Int! + notifications: Int! + emails: Int! + follows: Int! + shouts: Int! + invites: Int! + chatMessages: Int! + chatRooms: Int! + tags: Int! + locations: Int! + groups: Int! + inviteCodes: Int! + inviteCodesExpired: Int! + inviteCodesRedeemed: Int! + badgesRewarded: Int! + badgesDisplayed: Int! + usersVerified: Int! + reports: Int! } diff --git a/webapp/graphql/admin/Statistics.js b/webapp/graphql/admin/Statistics.js index 94c3f91f0..f21af88ba 100644 --- a/webapp/graphql/admin/Statistics.js +++ b/webapp/graphql/admin/Statistics.js @@ -3,13 +3,27 @@ import gql from 'graphql-tag' export const Statistics = gql` query { statistics { - countUsers - countPosts - countComments - countNotifications - countInvites - countFollows - countShouts + users + usersDeleted + posts + comments + notifications + emails + follows + shouts + invites + chatMessages + chatRooms + tags + locations + groups + inviteCodes + inviteCodesExpired + inviteCodesRedeemed + badgesRewarded + badgesDisplayed + usersVerified + reports } } ` diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 865eefee2..91feee0f6 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -39,16 +39,28 @@ "postCount": "Beiträge" }, "dashboard": { + "badgesDisplayed": "Badges ausgestellt", + "badgesRewarded": "Badges verteilt", + "chatMessages": "Chat-Nachrichten", + "chatRooms": "Chat-Räume", "comments": "Kommentare", - "follows": "Folgen", + "emails": "E-Mails", + "follows": "Follows", + "groups": "Gruppen", + "inviteCodes": "Einladungslinks", + "inviteCodesExpired": "Abgelaufene Einladungslinks", + "inviteCodesRedeemed": "Eingelöste Einladungslinks", "invites": "Einladungen", + "locations": "Orte", "name": "Startzentrale", "notifications": "Benachrichtigungen", - "organizations": "Organisationen", "posts": "Beiträge", - "projects": "Projekte", + "reports": "Gemeldet", "shouts": "Empfehlungen", - "users": "Nutzer" + "tags": "Tags", + "users": "Nutzer", + "usersDeleted": "Gelöschte Nutzer", + "usersVerified": "Verifizierte Nutzer" }, "donations": { "goal": "Monatlich benötigte Spenden", diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 33c3abfd9..7ce98601f 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -39,16 +39,28 @@ "postCount": "Posts" }, "dashboard": { + "badgesDisplayed": "Badges Displayed", + "badgesRewarded": "Badges Rewarded", + "chatMessages": "Chat Messages", + "chatRooms": "Chat Rooms", "comments": "Comments", + "emails": "E-Mails", "follows": "Follows", + "groups": "Groups", + "inviteCodes": "Invite Codes", + "inviteCodesExpired": "Expired Invite Codes", + "inviteCodesRedeemed": "Redeemed Invite Codes", "invites": "Invites", + "locations": "Locations", "name": "Dashboard", "notifications": "Notifications", - "organizations": "Organizations", "posts": "Posts", - "projects": "Projects", + "reports": "Reports", "shouts": "Shouts", - "users": "Users" + "tags": "Tags", + "users": "Users", + "usersDeleted": "Users deleted", + "usersVerified": "Users verified" }, "donations": { "goal": "Monthly donations needed", diff --git a/webapp/locales/es.json b/webapp/locales/es.json index 3c4d06403..28b7b47d1 100644 --- a/webapp/locales/es.json +++ b/webapp/locales/es.json @@ -39,16 +39,28 @@ "postCount": "Contribuciones" }, "dashboard": { + "badgesDisplayed": null, + "badgesRewarded": null, + "chatMessages": null, + "chatRooms": null, "comments": "Comentarios", + "emails": null, "follows": "Sigue", + "groups": null, + "inviteCodes": null, + "inviteCodesExpired": null, + "inviteCodesRedeemed": null, "invites": "Invita", + "locations": null, "name": "Tablero", "notifications": "Notificaciones", - "organizations": "Organizaciones", "posts": "Contribuciones", - "projects": "Proyectos", + "reports": null, "shouts": "Recomendaciones", - "users": "Usuarios" + "tags": null, + "users": "Usuarios", + "usersDeleted": null, + "usersVerified": null }, "donations": { "goal": "Donaciones mensuales necesarias", diff --git a/webapp/locales/fr.json b/webapp/locales/fr.json index 583726c52..2e1520370 100644 --- a/webapp/locales/fr.json +++ b/webapp/locales/fr.json @@ -39,16 +39,28 @@ "postCount": "Postes" }, "dashboard": { + "badgesDisplayed": null, + "badgesRewarded": null, + "chatMessages": null, + "chatRooms": null, "comments": "Commentaires", + "emails": null, "follows": "Suit", + "groups": null, + "inviteCodes": null, + "inviteCodesExpired": null, + "inviteCodesRedeemed": null, "invites": "Invitations", + "locations": null, "name": "Tableau de bord", "notifications": "Notifications", - "organizations": "Organisations", "posts": "Postes", - "projects": "Projets", + "reports": null, "shouts": "Cris", - "users": "Utilisateurs" + "tags": null, + "users": "Utilisateurs", + "usersDeleted": null, + "usersVerified": null }, "donations": { "goal": "Dons mensuels requis", diff --git a/webapp/locales/it.json b/webapp/locales/it.json index 2896c2c1f..ccb253578 100644 --- a/webapp/locales/it.json +++ b/webapp/locales/it.json @@ -39,16 +39,28 @@ "postCount": "Messaggi" }, "dashboard": { + "badgesDisplayed": null, + "badgesRewarded": null, + "chatMessages": null, + "chatRooms": null, "comments": "Commenti", + "emails": null, "follows": "Segue", + "groups": null, + "inviteCodes": null, + "inviteCodesExpired": null, + "inviteCodesRedeemed": null, "invites": "Inviti", + "locations": null, "name": "Cruscotto", - "notifications": "Notifiche", "organizations": "Organizzazioni", "posts": "Messaggi", - "projects": "Progetti", + "reports": null, "shouts": "Gridi", - "users": "Utenti" + "tags": null, + "users": "Utenti", + "usersDeleted": null, + "usersVerified": null }, "donations": { "goal": "Donazioni mensili necessarie", diff --git a/webapp/locales/nl.json b/webapp/locales/nl.json index e4ae47dc4..35344a25f 100644 --- a/webapp/locales/nl.json +++ b/webapp/locales/nl.json @@ -39,16 +39,28 @@ "postCount": "Berichten" }, "dashboard": { + "badgesDisplayed": null, + "badgesRewarded": null, + "chatMessages": null, + "chatRooms": null, "comments": "Opmerkingen", + "emails": null, "follows": "Volgt", + "groups": null, + "inviteCodes": null, + "inviteCodesExpired": null, + "inviteCodesRedeemed": null, "invites": "Uitnodigingen", + "locations": null, "name": "Dashboard", "notifications": "Meldingen", - "organizations": "Organisaties", "posts": "Berichten", - "projects": "Projecten", + "reports": null, "shouts": "Shouts", - "users": "Gebruikers" + "tags": null, + "users": "Gebruikers", + "usersDeleted": null, + "usersVerified": null }, "donations": { "goal": null, diff --git a/webapp/locales/pl.json b/webapp/locales/pl.json index 7783ef93d..1d3e2b781 100644 --- a/webapp/locales/pl.json +++ b/webapp/locales/pl.json @@ -39,16 +39,28 @@ "postCount": "Stanowiska" }, "dashboard": { + "badgesDisplayed": null, + "badgesRewarded": null, + "chatMessages": null, + "chatRooms": null, "comments": "Komentarze", + "emails": null, "follows": "Podąża za", + "groups": null, + "inviteCodes": null, + "inviteCodesExpired": null, + "inviteCodesRedeemed": null, "invites": "Zaprasza", + "locations": null, "name": "Tablica rozdzielcza", "notifications": "Powiadomienia", - "organizations": "Organizacje", "posts": "Stanowiska", - "projects": "Projekty", + "reports": null, "shouts": "Zalecane", - "users": "Użytkownicy" + "tags": null, + "users": "Użytkownicy", + "usersDeleted": null, + "usersVerified": null }, "donations": { "goal": null, diff --git a/webapp/locales/pt.json b/webapp/locales/pt.json index 7cd7b2a4b..2655c4990 100644 --- a/webapp/locales/pt.json +++ b/webapp/locales/pt.json @@ -39,16 +39,28 @@ "postCount": "Postagens" }, "dashboard": { + "badgesDisplayed": null, + "badgesRewarded": null, + "chatMessages": null, + "chatRooms": null, "comments": "Comentários", + "emails": null, "follows": "Segue", + "groups": null, + "inviteCodes": null, + "inviteCodesExpired": null, + "inviteCodesRedeemed": null, "invites": "Convites", + "locations": null, "name": "Painel de controle", "notifications": "Notificações", - "organizations": "Organizações", "posts": "Postagens", - "projects": "Projetos", + "reports": null, "shouts": "Aclamações", - "users": "Usuários" + "tags": null, + "users": "Usuários", + "usersDeleted": null, + "usersVerified": null }, "donations": { "goal": "Doações mensais necessárias", diff --git a/webapp/locales/ru.json b/webapp/locales/ru.json index eb365dcdb..1e3ee21f4 100644 --- a/webapp/locales/ru.json +++ b/webapp/locales/ru.json @@ -39,16 +39,28 @@ "postCount": "Посты" }, "dashboard": { + "badgesDisplayed": null, + "badgesRewarded": null, + "chatMessages": null, + "chatRooms": null, "comments": "Комментарии", + "emails": null, "follows": "Подписки", + "groups": null, + "inviteCodes": null, + "inviteCodesExpired": null, + "inviteCodesRedeemed": null, "invites": "Приглашения", + "locations": null, "name": "Панель управления", "notifications": "Уведомления", - "organizations": "Организации", "posts": "Посты", - "projects": "Проекты", + "reports": null, "shouts": "Выкрики", - "users": "Пользователи" + "tags": null, + "users": "Пользователи", + "usersDeleted": null, + "usersVerified": null }, "donations": { "goal": "Необходимы ежемесячные пожертвования", diff --git a/webapp/pages/admin/index.vue b/webapp/pages/admin/index.vue index 22f0bd678..84cc85199 100644 --- a/webapp/pages/admin/index.vue +++ b/webapp/pages/admin/index.vue @@ -20,100 +20,20 @@