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 <lenzmath@posteo.de>
This commit is contained in:
Robert Schäfer 2025-05-04 07:44:31 +08:00 committed by GitHub
parent e4ae0dfe50
commit fac818a3e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 77 additions and 46 deletions

View File

@ -95,6 +95,7 @@
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/node": "^22.15.3", "@types/node": "^22.15.3",
"@types/slug": "^5.0.9",
"@types/uuid": "~9.0.1", "@types/uuid": "~9.0.1",
"@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0", "@typescript-eslint/parser": "^5.62.0",

View File

@ -12,6 +12,7 @@ import CONFIG from '@config/index'
import { CATEGORIES_MIN, CATEGORIES_MAX } from '@constants/categories' import { CATEGORIES_MIN, CATEGORIES_MAX } from '@constants/categories'
import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '@constants/groups' import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '@constants/groups'
import { removeHtmlTags } from '@middleware/helpers/cleanHtml' import { removeHtmlTags } from '@middleware/helpers/cleanHtml'
import type { Context } from '@src/server'
import Resolver, { import Resolver, {
removeUndefinedNullValuesFromObject, removeUndefinedNullValuesFromObject,
@ -22,7 +23,7 @@ import { createOrUpdateLocations } from './users/location'
export default { export default {
Query: { Query: {
Group: async (_object, params, context, _resolveInfo) => { Group: async (_object, params, context: Context, _resolveInfo) => {
const { isMember, id, slug, first, offset } = params const { isMember, id, slug, first, offset } = params
let pagination = '' let pagination = ''
const orderBy = 'ORDER BY group.createdAt DESC' const orderBy = 'ORDER BY group.createdAt DESC'
@ -75,10 +76,10 @@ export default {
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } finally {
session.close() await session.close()
} }
}, },
GroupMembers: async (_object, params, context, _resolveInfo) => { GroupMembers: async (_object, params, context: Context, _resolveInfo) => {
const { id: groupId } = params const { id: groupId } = params
const session = context.driver.session() const session = context.driver.session()
const readTxResultPromise = session.readTransaction(async (txc) => { const readTxResultPromise = session.readTransaction(async (txc) => {
@ -96,7 +97,7 @@ export default {
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } finally {
session.close() await session.close()
} }
}, },
GroupCount: async (_object, params, context, _resolveInfo) => { GroupCount: async (_object, params, context, _resolveInfo) => {
@ -134,7 +135,7 @@ export default {
}, },
}, },
Mutation: { Mutation: {
CreateGroup: async (_parent, params, context, _resolveInfo) => { CreateGroup: async (_parent, params, context: Context, _resolveInfo) => {
const { categoryIds } = params const { categoryIds } = params
delete params.categoryIds delete params.categoryIds
params.locationName = params.locationName === '' ? null : params.locationName params.locationName = params.locationName === '' ? null : params.locationName
@ -182,7 +183,7 @@ export default {
`, `,
{ userId: context.user.id, categoryIds, params }, { userId: context.user.id, categoryIds, params },
) )
const [group] = await ownerCreateGroupTransactionResponse.records.map((record) => const [group] = ownerCreateGroupTransactionResponse.records.map((record) =>
record.get('group'), record.get('group'),
) )
return group return group
@ -197,10 +198,10 @@ export default {
throw new UserInputError('Group with this slug already exists!') throw new UserInputError('Group with this slug already exists!')
throw new Error(error) throw new Error(error)
} finally { } finally {
session.close() await session.close()
} }
}, },
UpdateGroup: async (_parent, params, context, _resolveInfo) => { UpdateGroup: async (_parent, params, context: Context, _resolveInfo) => {
const { categoryIds } = params const { categoryIds } = params
delete params.categoryIds delete params.categoryIds
const { id: groupId, avatar: avatarInput } = params const { id: groupId, avatar: avatarInput } = params
@ -257,7 +258,7 @@ export default {
categoryIds, categoryIds,
params, params,
}) })
const [group] = await transactionResponse.records.map((record) => record.get('group')) const [group] = transactionResponse.records.map((record) => record.get('group'))
if (avatarInput) { if (avatarInput) {
await mergeImage(group, 'AVATAR_IMAGE', avatarInput, { transaction }) await mergeImage(group, 'AVATAR_IMAGE', avatarInput, { transaction })
} }
@ -273,10 +274,10 @@ export default {
throw new UserInputError('Group with this slug already exists!') throw new UserInputError('Group with this slug already exists!')
throw new Error(error) throw new Error(error)
} finally { } finally {
session.close() await session.close()
} }
}, },
JoinGroup: async (_parent, params, context, _resolveInfo) => { JoinGroup: async (_parent, params, context: Context, _resolveInfo) => {
const { groupId, userId } = params const { groupId, userId } = params
const session = context.driver.session() const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => { const writeTxResultPromise = session.writeTransaction(async (transaction) => {
@ -294,7 +295,7 @@ export default {
RETURN member {.*, myRoleInGroup: membership.role} RETURN member {.*, myRoleInGroup: membership.role}
` `
const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId }) 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 return member
}) })
try { try {
@ -302,10 +303,10 @@ export default {
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } finally {
session.close() await session.close()
} }
}, },
LeaveGroup: async (_parent, params, context, _resolveInfo) => { LeaveGroup: async (_parent, params, context: Context, _resolveInfo) => {
const { groupId, userId } = params const { groupId, userId } = params
const session = context.driver.session() const session = context.driver.session()
try { try {
@ -313,10 +314,10 @@ export default {
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } 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 { groupId, userId, roleInGroup } = params
const session = context.driver.session() const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => { const writeTxResultPromise = session.writeTransaction(async (transaction) => {
@ -353,7 +354,7 @@ export default {
userId, userId,
roleInGroup, roleInGroup,
}) })
const [member] = await transactionResponse.records.map((record) => record.get('member')) const [member] = transactionResponse.records.map((record) => record.get('member'))
return member return member
}) })
try { try {
@ -361,10 +362,10 @@ export default {
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } finally {
session.close() await session.close()
} }
}, },
RemoveUserFromGroup: async (_parent, params, context, _resolveInfo) => { RemoveUserFromGroup: async (_parent, params, context: Context, _resolveInfo) => {
const { groupId, userId } = params const { groupId, userId } = params
const session = context.driver.session() const session = context.driver.session()
try { try {
@ -372,10 +373,10 @@ export default {
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } finally {
session.close() await session.close()
} }
}, },
muteGroup: async (_parent, params, context, _resolveInfo) => { muteGroup: async (_parent, params, context: Context, _resolveInfo) => {
const { groupId } = params const { groupId } = params
const userId = context.user.id const userId = context.user.id
const session = context.driver.session() const session = context.driver.session()
@ -393,7 +394,7 @@ export default {
userId, userId,
}, },
) )
const [group] = await transactionResponse.records.map((record) => record.get('group')) const [group] = transactionResponse.records.map((record) => record.get('group'))
return group return group
}) })
try { try {
@ -401,10 +402,10 @@ export default {
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } finally {
session.close() await session.close()
} }
}, },
unmuteGroup: async (_parent, params, context, _resolveInfo) => { unmuteGroup: async (_parent, params, context: Context, _resolveInfo) => {
const { groupId } = params const { groupId } = params
const userId = context.user.id const userId = context.user.id
const session = context.driver.session() const session = context.driver.session()
@ -422,7 +423,7 @@ export default {
userId, userId,
}, },
) )
const [group] = await transactionResponse.records.map((record) => record.get('group')) const [group] = transactionResponse.records.map((record) => record.get('group'))
return group return group
}) })
try { try {
@ -430,7 +431,7 @@ export default {
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } finally {
session.close() await session.close()
} }
}, },
}, },

View File

@ -1,18 +1,18 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
import type { Context } from '@src/server'
import uniqueSlug from './slugify/uniqueSlug' import uniqueSlug from './slugify/uniqueSlug'
const isUniqueFor = (context, type) => { const isUniqueFor = (context: Context, type: string) => {
return async (slug) => { return async (slug: string) => {
const session = context.driver.session() const session = context.driver.session()
try { try {
const existingSlug = await session.readTransaction((transaction) => { const existingSlug = await session.readTransaction((transaction) => {
return transaction.run( return transaction.run(
` `
MATCH(p:${type} {slug: $slug }) MATCH(p:${type} {slug: $slug })
RETURN p.slug RETURN p.slug
`, `,
{ slug }, { slug },
@ -20,26 +20,50 @@ const isUniqueFor = (context, type) => {
}) })
return existingSlug.records.length === 0 return existingSlug.records.length === 0
} finally { } finally {
session.close() await session.close()
} }
} }
} }
export default { export default {
Mutation: { 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'))) args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
return resolve(root, args, context, info) 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'))) args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group')))
return resolve(root, args, context, info) 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'))) args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info) 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? // 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'))) args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info) return resolve(root, args, context, info)

View File

@ -14,9 +14,11 @@ describe('uniqueSlug', () => {
}) })
it('slugify null string', async () => { it('slugify null string', async () => {
const string = null const nullString = null
const isUnique = jest.fn().mockResolvedValue(true) 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 () => { it('Converts umlaut to a two letter equivalent', async () => {

View File

@ -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' import slugify from 'slug'
export default async function uniqueSlug(string, isUnique) { type IsUnique = (slug: string) => Promise<boolean>
const slug = slugify(string || 'anonymous', { export default async function uniqueSlug(str: string, isUnique: IsUnique) {
const slug = slugify(str || 'anonymous', {
lower: true, lower: true,
multicharmap: { Ä: 'AE', ä: 'ae', Ö: 'OE', ö: 'oe', Ü: 'UE', ü: 'ue', ß: 'ss' }, multicharmap: { Ä: 'AE', ä: 'ae', Ö: 'OE', ö: 'oe', Ü: 'UE', ü: 'ue', ß: 'ss' },
}) })
if (await isUnique(slug)) return slug if (await isUnique(slug)) return slug
let count = 0 let count = 0
let uniqueSlug let uniqueSlug: string
do { do {
count += 1 count += 1
uniqueSlug = `${slug}-${count}` uniqueSlug = `${slug}-${count}`

View File

@ -102,3 +102,4 @@ const createServer = (options?) => {
} }
export default createServer export default createServer
export type Context = Awaited<ReturnType<ReturnType<typeof getContext>>>

View File

@ -1533,6 +1533,11 @@
"@types/express-serve-static-core" "*" "@types/express-serve-static-core" "*"
"@types/mime" "*" "@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": "@types/stack-utils@^2.0.0":
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"