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/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",

View File

@ -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()
}
},
},

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-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)

View File

@ -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 () => {

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'
export default async function uniqueSlug(string, isUnique) {
const slug = slugify(string || 'anonymous', {
type IsUnique = (slug: string) => Promise<boolean>
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}`

View File

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

View File

@ -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"