Ulf Gebhardt 2b0d38fdff
feat(backend): autoselect badges when rewarding and the user still have free slots (#8577)
* autoselect badges when rewarding and the suer still have free slots

* improve semantics

---------

Co-authored-by: Hendrik-cpu <62690517+Hendrik-cpu@users.noreply.github.com>
Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
2025-05-25 14:43:38 +02:00

171 lines
5.3 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { neo4jgraphql } from 'neo4j-graphql-js'
import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges'
import { Context } from '@src/server'
export const defaultTrophyBadge = {
id: 'default_trophy',
type: 'trophy',
icon: '/img/badges/default_trophy.svg',
description: '',
createdAt: '',
}
export const defaultVerificationBadge = {
id: 'default_verification',
type: 'verification',
icon: '/img/badges/default_verification.svg',
description: '',
createdAt: '',
}
export default {
Query: {
Badge: async (object, args, context, resolveInfo) =>
neo4jgraphql(object, args, context, resolveInfo),
},
Mutation: {
setVerificationBadge: async (_object, args, context, _resolveInfo) => {
const {
user: { id: currentUserId },
} = context
const { badgeId, userId } = args
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const response = await transaction.run(
`
MATCH (badge:Badge {id: $badgeId, type: 'verification'}), (user:User {id: $userId})
OPTIONAL MATCH (:Badge {type: 'verification'})-[verify:VERIFIES]->(user)
DELETE verify
MERGE (badge)-[relation:VERIFIES {by: $currentUserId}]->(user)
RETURN relation, user {.*}
`,
{
badgeId,
userId,
currentUserId,
},
)
return {
relation: response.records.map((record) => record.get('relation'))[0],
user: response.records.map((record) => record.get('user'))[0],
}
})
try {
const { relation, user } = await writeTxResultPromise
if (!relation) {
throw new Error(
'Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.',
)
}
return user
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
},
rewardTrophyBadge: async (_object, args, context: Context, _resolveInfo) => {
const {
user: { id: currentUserId },
} = context
const { badgeId, userId } = args
// Find used slot
const userBadges = (
await context.database.query({
query: `
MATCH (rewardedBadge:Badge)-[rewarded:REWARDED]->(user:User {id: $userId})
OPTIONAL MATCH (rewardedBadge)<-[selected:SELECTED]-(user)
RETURN collect(rewardedBadge {.*}) AS rewardedBadges, collect(toString(selected.slot)) AS usedSlots
`,
variables: { userId },
})
).records.map((record) => {
return {
rewardedBadges: record.get('rewardedBadges'),
usedSlots: record.get('usedSlots'),
}
})
const { rewardedBadges, usedSlots } = userBadges[0]
let slot
if (
!rewardedBadges.find((item) => item.id === badgeId) && // badge was not rewarded yet
usedSlots.length < TROPHY_BADGES_SELECTED_MAX // there is free slots left
) {
for (slot = 0; slot <= TROPHY_BADGES_SELECTED_MAX; slot++) {
if (!usedSlots.find((item) => parseInt(item) === slot)) {
break
}
}
}
// reward badge and assign slot
const users = (
await context.database.write({
query: `
MATCH (badge:Badge {id: $badgeId, type: 'trophy'}), (user:User {id: $userId})
MERGE (badge)-[:REWARDED {by: $currentUserId}]->(user)
${slot === undefined ? '' : 'MERGE (badge)<-[:SELECTED {slot: $slot}]-(user)'}
RETURN user {.*}
`,
variables: { badgeId, userId, currentUserId, slot },
})
).records.map((record) => record.get('user'))
if (users.length !== 1) {
throw new Error(
'Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.',
)
}
return users[0]
},
revokeBadge: async (_object, args, context, _resolveInfo) => {
const { badgeId, userId } = args
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const response = await transaction.run(
`
MATCH (user:User {id: $userId})
OPTIONAL MATCH (badge:Badge {id: $badgeId})-[rewarded:REWARDED|VERIFIES]->(user)
OPTIONAL MATCH (user)-[selected:SELECTED]->(badge)
DELETE rewarded
DELETE selected
RETURN user {.*}
`,
{
badgeId,
userId,
},
)
return response.records.map((record) => record.get('user'))[0]
})
try {
return await writeTxResultPromise
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
},
},
Badge: {
isDefault: async (parent, _params, _context, _resolveInfo) =>
[defaultTrophyBadge.id, defaultVerificationBadge.id].includes(parent.id),
},
}