From fac818a3e4bb7768be1cb2d6343d907a4e0df819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Sun, 4 May 2025 07:44:31 +0800 Subject: [PATCH] refactor(backend): types for context + `slug` (#8486) Also these changes saw merge conflicts in #8463 so let's get them merged already. Co-authored-by: mahula --- backend/package.json | 1 + backend/src/graphql/resolvers/groups.ts | 53 ++++++++++--------- backend/src/middleware/sluggifyMiddleware.ts | 46 ++++++++++++---- .../src/middleware/slugify/uniqueSlug.spec.ts | 6 ++- backend/src/middleware/slugify/uniqueSlug.ts | 11 ++-- backend/src/server.ts | 1 + backend/yarn.lock | 5 ++ 7 files changed, 77 insertions(+), 46 deletions(-) diff --git a/backend/package.json b/backend/package.json index 76b0a30b6..5dc8ca81f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -95,6 +95,7 @@ "@types/jest": "^29.5.14", "@types/lodash": "^4.17.16", "@types/node": "^22.15.3", + "@types/slug": "^5.0.9", "@types/uuid": "~9.0.1", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", diff --git a/backend/src/graphql/resolvers/groups.ts b/backend/src/graphql/resolvers/groups.ts index 96d806bf8..8e24117e1 100644 --- a/backend/src/graphql/resolvers/groups.ts +++ b/backend/src/graphql/resolvers/groups.ts @@ -12,6 +12,7 @@ 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 type { Context } from '@src/server' import Resolver, { removeUndefinedNullValuesFromObject, @@ -22,7 +23,7 @@ import { createOrUpdateLocations } from './users/location' export default { Query: { - Group: async (_object, params, context, _resolveInfo) => { + Group: async (_object, params, context: Context, _resolveInfo) => { const { isMember, id, slug, first, offset } = params let pagination = '' const orderBy = 'ORDER BY group.createdAt DESC' @@ -75,10 +76,10 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, - GroupMembers: async (_object, params, context, _resolveInfo) => { + GroupMembers: async (_object, params, context: Context, _resolveInfo) => { const { id: groupId } = params const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (txc) => { @@ -96,7 +97,7 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, GroupCount: async (_object, params, context, _resolveInfo) => { @@ -134,7 +135,7 @@ export default { }, }, Mutation: { - CreateGroup: async (_parent, params, context, _resolveInfo) => { + CreateGroup: async (_parent, params, context: Context, _resolveInfo) => { const { categoryIds } = params delete params.categoryIds params.locationName = params.locationName === '' ? null : params.locationName @@ -182,7 +183,7 @@ export default { `, { userId: context.user.id, categoryIds, params }, ) - const [group] = await ownerCreateGroupTransactionResponse.records.map((record) => + const [group] = ownerCreateGroupTransactionResponse.records.map((record) => record.get('group'), ) return group @@ -197,10 +198,10 @@ export default { throw new UserInputError('Group with this slug already exists!') throw new Error(error) } finally { - session.close() + await session.close() } }, - UpdateGroup: async (_parent, params, context, _resolveInfo) => { + UpdateGroup: async (_parent, params, context: Context, _resolveInfo) => { const { categoryIds } = params delete params.categoryIds const { id: groupId, avatar: avatarInput } = params @@ -257,7 +258,7 @@ export default { categoryIds, params, }) - const [group] = await transactionResponse.records.map((record) => record.get('group')) + const [group] = transactionResponse.records.map((record) => record.get('group')) if (avatarInput) { await mergeImage(group, 'AVATAR_IMAGE', avatarInput, { transaction }) } @@ -273,10 +274,10 @@ export default { throw new UserInputError('Group with this slug already exists!') throw new Error(error) } finally { - session.close() + await session.close() } }, - JoinGroup: async (_parent, params, context, _resolveInfo) => { + JoinGroup: async (_parent, params, context: Context, _resolveInfo) => { const { groupId, userId } = params const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -294,7 +295,7 @@ export default { RETURN member {.*, myRoleInGroup: membership.role} ` const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId }) - const [member] = await transactionResponse.records.map((record) => record.get('member')) + const [member] = transactionResponse.records.map((record) => record.get('member')) return member }) try { @@ -302,10 +303,10 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, - LeaveGroup: async (_parent, params, context, _resolveInfo) => { + LeaveGroup: async (_parent, params, context: Context, _resolveInfo) => { const { groupId, userId } = params const session = context.driver.session() try { @@ -313,10 +314,10 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, - ChangeGroupMemberRole: async (_parent, params, context, _resolveInfo) => { + ChangeGroupMemberRole: async (_parent, params, context: Context, _resolveInfo) => { const { groupId, userId, roleInGroup } = params const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -353,7 +354,7 @@ export default { userId, roleInGroup, }) - const [member] = await transactionResponse.records.map((record) => record.get('member')) + const [member] = transactionResponse.records.map((record) => record.get('member')) return member }) try { @@ -361,10 +362,10 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, - RemoveUserFromGroup: async (_parent, params, context, _resolveInfo) => { + RemoveUserFromGroup: async (_parent, params, context: Context, _resolveInfo) => { const { groupId, userId } = params const session = context.driver.session() try { @@ -372,10 +373,10 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, - muteGroup: async (_parent, params, context, _resolveInfo) => { + muteGroup: async (_parent, params, context: Context, _resolveInfo) => { const { groupId } = params const userId = context.user.id const session = context.driver.session() @@ -393,7 +394,7 @@ export default { userId, }, ) - const [group] = await transactionResponse.records.map((record) => record.get('group')) + const [group] = transactionResponse.records.map((record) => record.get('group')) return group }) try { @@ -401,10 +402,10 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, - unmuteGroup: async (_parent, params, context, _resolveInfo) => { + unmuteGroup: async (_parent, params, context: Context, _resolveInfo) => { const { groupId } = params const userId = context.user.id const session = context.driver.session() @@ -422,7 +423,7 @@ export default { userId, }, ) - const [group] = await transactionResponse.records.map((record) => record.get('group')) + const [group] = transactionResponse.records.map((record) => record.get('group')) return group }) try { @@ -430,7 +431,7 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, }, diff --git a/backend/src/middleware/sluggifyMiddleware.ts b/backend/src/middleware/sluggifyMiddleware.ts index 92c2c1367..0a45521f0 100644 --- a/backend/src/middleware/sluggifyMiddleware.ts +++ b/backend/src/middleware/sluggifyMiddleware.ts @@ -1,18 +1,18 @@ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + /* eslint-disable @typescript-eslint/no-unsafe-return */ +import type { Context } from '@src/server' + import uniqueSlug from './slugify/uniqueSlug' -const isUniqueFor = (context, type) => { - return async (slug) => { +const isUniqueFor = (context: Context, type: string) => { + return async (slug: string) => { const session = context.driver.session() try { const existingSlug = await session.readTransaction((transaction) => { return transaction.run( ` - MATCH(p:${type} {slug: $slug }) + MATCH(p:${type} {slug: $slug }) RETURN p.slug `, { slug }, @@ -20,26 +20,50 @@ const isUniqueFor = (context, type) => { }) return existingSlug.records.length === 0 } finally { - session.close() + await session.close() } } } export default { Mutation: { - SignupVerification: async (resolve, root, args, context, info) => { + SignupVerification: async ( + resolve, + root, + args: { slug: string; name: string }, + context: Context, + info, + ) => { args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User'))) return resolve(root, args, context, info) }, - CreateGroup: async (resolve, root, args, context, info) => { + CreateGroup: async ( + resolve, + root, + args: { slug: string; name: string }, + context: Context, + info, + ) => { args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group'))) return resolve(root, args, context, info) }, - CreatePost: async (resolve, root, args, context, info) => { + CreatePost: async ( + resolve, + root, + args: { slug: string; title: string }, + context: Context, + info, + ) => { args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) return resolve(root, args, context, info) }, - UpdatePost: async (resolve, root, args, context, info) => { + UpdatePost: async ( + resolve, + root, + args: { slug: string; title: string }, + context: Context, + info, + ) => { // TODO: is this absolutely correct? what happens if "args.title" is not defined? may it works accidentally, because "args.title" or "args.slug" is always send? args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) return resolve(root, args, context, info) diff --git a/backend/src/middleware/slugify/uniqueSlug.spec.ts b/backend/src/middleware/slugify/uniqueSlug.spec.ts index 659a439c2..8259583cd 100644 --- a/backend/src/middleware/slugify/uniqueSlug.spec.ts +++ b/backend/src/middleware/slugify/uniqueSlug.spec.ts @@ -14,9 +14,11 @@ describe('uniqueSlug', () => { }) it('slugify null string', async () => { - const string = null + const nullString = null const isUnique = jest.fn().mockResolvedValue(true) - await expect(uniqueSlug(string, isUnique)).resolves.toEqual('anonymous') + await expect(uniqueSlug(nullString as unknown as string, isUnique)).resolves.toEqual( + 'anonymous', + ) }) it('Converts umlaut to a two letter equivalent', async () => { diff --git a/backend/src/middleware/slugify/uniqueSlug.ts b/backend/src/middleware/slugify/uniqueSlug.ts index e24b15eb3..8f540a6ab 100644 --- a/backend/src/middleware/slugify/uniqueSlug.ts +++ b/backend/src/middleware/slugify/uniqueSlug.ts @@ -1,18 +1,15 @@ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import slugify from 'slug' -export default async function uniqueSlug(string, isUnique) { - const slug = slugify(string || 'anonymous', { +type IsUnique = (slug: string) => Promise +export default async function uniqueSlug(str: string, isUnique: IsUnique) { + const slug = slugify(str || 'anonymous', { lower: true, multicharmap: { Ä: 'AE', ä: 'ae', Ö: 'OE', ö: 'oe', Ü: 'UE', ü: 'ue', ß: 'ss' }, }) if (await isUnique(slug)) return slug let count = 0 - let uniqueSlug + let uniqueSlug: string do { count += 1 uniqueSlug = `${slug}-${count}` diff --git a/backend/src/server.ts b/backend/src/server.ts index 457ea3684..1f98aab2d 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -102,3 +102,4 @@ const createServer = (options?) => { } export default createServer +export type Context = Awaited>> diff --git a/backend/yarn.lock b/backend/yarn.lock index 209c482e4..688f5faad 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1533,6 +1533,11 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@types/slug@^5.0.9": + version "5.0.9" + resolved "https://registry.yarnpkg.com/@types/slug/-/slug-5.0.9.tgz#e5b213a9d7797d40d362ba85e2a7bbcd4df4ed40" + integrity sha512-6Yp8BSplP35Esa/wOG1wLNKiqXevpQTEF/RcL/NV6BBQaMmZh4YlDwCgrrFSoUE4xAGvnKd5c+lkQJmPrBAzfQ== + "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"