diff --git a/backend/src/db/factories.ts b/backend/src/db/factories.ts index a5237dada..eef970aba 100644 --- a/backend/src/db/factories.ts +++ b/backend/src/db/factories.ts @@ -11,6 +11,9 @@ import slugify from 'slug' import { v4 as uuid } from 'uuid' import { generateInviteCode } from '@graphql/resolvers/inviteCodes' +import { isUniqueFor } from '@middleware/sluggifyMiddleware' +import uniqueSlug from '@middleware/slugify/uniqueSlug' +import { Context } from '@src/server' import { getDriver, getNeode } from './neo4j' @@ -22,8 +25,9 @@ const uniqueImageUrl = (imageUrl) => { return newUrl.toString() } +const driver = getDriver() + export const cleanDatabase = async ({ withMigrations } = { withMigrations: false }) => { - const driver = getDriver() const session = driver.session() const clean = ` @@ -89,9 +93,7 @@ Factory.define('basicUser') showShoutsPublicly: false, locale: 'en', }) - .attr('slug', ['slug', 'name'], (slug, name) => { - return slug || slugify(name, { lower: true }) - }) + .attr('slug', null) .attr('encryptedPassword', ['password'], (password) => { // eslint-disable-next-line n/no-sync return hashSync(password, 10) @@ -121,13 +123,24 @@ Factory.define('userWithAboutEmpty') Factory.define('user') .extend('basicUser') .option('about', faker.lorem.paragraph) - .option('email', faker.internet.exampleEmail) + .option('email', null) .option('avatar', () => Factory.build('image', { url: faker.image.avatar(), }), ) .after(async (buildObject, options) => { + // Ensure unique slug + if (!buildObject.slug) { + buildObject.slug = await uniqueSlug( + buildObject.name, + isUniqueFor({ driver } as unknown as Context, 'User'), + ) + } + // Ensure unique email + if (!options.email) { + options.email = `${buildObject.slug as string}@example.org` + } const [user, email, avatar] = await Promise.all([ neode.create('User', buildObject), neode.create('EmailAddress', { email: options.email }), diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index e7f5b23c5..f93bb6b98 100644 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -1197,11 +1197,22 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] // eslint-disable-next-line @typescript-eslint/no-explicit-any const additionalUsers: any[] = [] - for (let i = 0; i < 30; i++) { + for (let i = 0; i < 3000; i++) { const user = await Factory.build('user') await jennyRostock.relateTo(user, 'following') await user.relateTo(jennyRostock, 'following') additionalUsers.push(user) + + const userObj = await user.toJson() + authenticatedUser = userObj + + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g2', + userId: userObj.id, + }, + }) } // Jenny users diff --git a/backend/src/graphql/resolvers/groups.ts b/backend/src/graphql/resolvers/groups.ts index 9e330bade..9efa8e6af 100644 --- a/backend/src/graphql/resolvers/groups.ts +++ b/backend/src/graphql/resolvers/groups.ts @@ -80,15 +80,18 @@ export default { } }, GroupMembers: async (_object, params, context: Context, _resolveInfo) => { - const { id: groupId } = params + const { id: groupId, first = 25, offset = 0 } = params const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (txc) => { const groupMemberCypher = ` MATCH (user:User)-[membership:MEMBER_OF]->(:Group {id: $groupId}) RETURN user {.*, myRoleInGroup: membership.role} + SKIP toInteger($offset) LIMIT toInteger($first) ` const transactionResponse = await txc.run(groupMemberCypher, { groupId, + first, + offset, }) return transactionResponse.records.map((record) => record.get('user')) }) @@ -468,6 +471,9 @@ export default { isMutedByMe: 'MATCH (this) RETURN EXISTS( (this)<-[:MUTED]-(:User {id: $cypherParams.currentUserId}) )', }, + count: { + membersCount: '<-[:MEMBER_OF]-(related:User)', + }, }), name: async (parent, _args, context: Context, _resolveInfo) => { if (!context.user) { diff --git a/backend/src/graphql/types/type/Group.gql b/backend/src/graphql/types/type/Group.gql index 0adc7853b..03b3e0eee 100644 --- a/backend/src/graphql/types/type/Group.gql +++ b/backend/src/graphql/types/type/Group.gql @@ -38,6 +38,8 @@ type Group { categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT") + membersCount: Int! @cypher(statement: "MATCH (this)<-[:MEMBER_OF]-(r:User) RETURN COUNT(DISTINCT r)") + myRole: GroupMemberRole # if 'null' then the current user is no member posts: [Post] @relation(name: "IN", direction: "IN") @@ -78,8 +80,8 @@ type Query { GroupMembers( id: ID! - # first: Int # not implemented yet - # offset: Int # not implemented yet + first: Int + offset: Int # orderBy: [_UserOrdering] # not implemented yet # filter: _UserFilter # not implemented yet ): [User] diff --git a/backend/src/middleware/sluggifyMiddleware.ts b/backend/src/middleware/sluggifyMiddleware.ts index 0a45521f0..fc38a5bfb 100644 --- a/backend/src/middleware/sluggifyMiddleware.ts +++ b/backend/src/middleware/sluggifyMiddleware.ts @@ -5,7 +5,7 @@ import type { Context } from '@src/server' import uniqueSlug from './slugify/uniqueSlug' -const isUniqueFor = (context: Context, type: string) => { +export const isUniqueFor = (context: Context, type: string) => { return async (slug: string) => { const session = context.driver.session() try { diff --git a/webapp/components/features/ProfileList/ProfileList.vue b/webapp/components/features/ProfileList/ProfileList.vue index 29fdb2872..e7db8b3b4 100644 --- a/webapp/components/features/ProfileList/ProfileList.vue +++ b/webapp/components/features/ProfileList/ProfileList.vue @@ -4,24 +4,44 @@
{{ title }}
-