Source: Router/achievement/utilities.js

// @ts-check
/**
 * @import {ExpressRequestAuthorized, ExpressResponse} from '../../types.js'
 */
import {query,transaction,UUID2hex,HEX2uuid,HEX2base64} from '@commtool/sql-query'
import { filterObjects } from '../../utils/objectfilter/filters/index.js'
import {queueAdd} from '../../tree/treeQueue/treeQueue.js'


/**
 * Consolidates all achievement UIDs for a person into their Data object.
 * 
 * This function fetches all achievements linked to a specific person, encodes their UIDs in base64 format,
 * and stores this consolidated array in the person's Data object. This approach enables faster filtering
 * and querying of persons based on their achievements without needing to perform complex joins each time.
 * 
 * The function only updates the person's record if the achievements data has actually changed,
 * minimizing unnecessary database operations.
 * 
 * @param {Buffer} UID - The unique identifier (hex string or Buffer) of the person whose achievements are to be consolidated.
 * @returns {Promise<void>} A promise that resolves when the consolidation is complete.
 * @async
 */
export const updateAchievements=async (UID)=>
    {
        // updates the achievements for a person with the provided UID
        const persons= await query(`
            SELECT person.Data,target.targetData, UNIX_TIMESTAMP(person.validFrom) AS validFrom, person.UID FROM ObjectBase AS person INNER JOIN
            (
             
                SELECT JSON_ARRAYAGG(TO_BASE64(Links.UIDTarget)) AS targetData, ObjectBase.UIDBelongsTo
                FROM ObjectBase INNER JOIN Links  ON (Links.UID=ObjectBase.UID AND Links.Type='achievement') 
                INNER JOIN ObjectBase AS Template ON (Template.UID=Links.UIDTarget)
                WHERE ObjectBase.Type='achievement' 
                AND ( ObjectBase.dindex IS NULL) 
                GROUP BY  ObjectBase.UIDBelongsTo)  AS target
                ON(target.UIDBelongsTo=person.UID)
                WHERE person.UID=? AND person.Type IN ('person','extern')
                            `,[UID],
                            {
                                cast:['json'],
                                log:false
                            }
             )
        const updated=new Set()
        if(persons.length>0)
        {
            const person=persons[0]
            if(!person.Data.achievements || JSON.stringify(person.Data.achievements)!==JSON.stringify(person.targetData))
            {
                await query(`UPDATE ObjectBase SET Data=? WHERE UID=?  `,
                [JSON.stringify({achievements:person.targetData}), person.UID],
                {backDate:person.validFrom})
                updated.add(HEX2base64(person.UID))
    
            }
        }
     
    }


    
/**
 * Expires achievements based on their renewal period
 * 
 * This function checks all achievements linked to a membership organization (root entity) 
 * and expires those whose renewal period has elapsed. For each expired achievement, 
 * it updates the dindex to mark it as expired and then updates the achievement lists 
 * for affected persons.
 * 
 * @param {string|Buffer} root - The UID of the membership organization containing the achievements
 * @param {boolean} [complete=false] - If true, processes all achievements regardless of 
 *                                    current expiration status
 * @returns {Promise<void>} A promise that resolves when all achievements have been processed
 * 
 * @async
 * @throws Will throw an error if database operations fail
 */
export const expireAchievements=async (root,complete=false)=>
{
    root=UUID2hex(root)
    // we need a Buffer version of root for queue operations
    const rootBuf = HEX2uuid(root)
    const [{UID:sysUser}]= await query(`SELECT ObjectBase.UID
        FROM  Member AS SysUser
        INNER JOIN ObjectBase ON (SysUser.UID=ObjectBase.UIDBelongsTo AND ObjectBase.Type='extern')
        INNER JOIN Links ON (ObjectBase.UID= Links.UID AND Links.UIDTarget=? AND Links.Type = 'memberSys')
    `,
    [root],
    )

    const  aas= await query(`SELECT achievement.UID,achievement.UIDBelongsTo,
        achievement.dindex,JSON_VALUE(achievementT.Data,'$.renewal') AS renewal,UNIX_TIMESTAMP(achievement.validFrom) AS validFrom
        FROM ObjectBase AS achievement
        INNER JOIN Links AS aLink ON (aLink.UID=achievement.UID AND aLink.Type='achievement')
        INNER JOIN ObjectBase AS achievementT ON (achievementT.UID=aLink.UIDTarget AND achievementT.Type='achievementT')
        WHERE achievement.Type='achievement'
            AND  achievementT.UIDBelongsTo=?`,
        [root],
        {cast:['json']})
  
    const updatedPersons=new Set()
  
   
    await Promise.all( aas.map( achievement=>
    (
        new Promise(async (resolve,reject)=>
        {
            // do not expire, if renewal =0 or negative
            const expire=parseInt(achievement.renewal) >0 ?
                 Math.floor(parseInt(achievement.renewal)-(Date.now()-achievement.validFrom*1000)/(24*3600000)):
                 Number.MAX_SAFE_INTEGER
            //                       days to expire                  - (actual Date  in ms   - achievemnt date in sec* 1000) / millisceonds for one day
            //                       days to expire                   - days  passed since achievement
            // >0: expired since number of days <0 still valid for result days 
            const dindex=expire<0 ? 1 : null
            // dindex 1 means expired
            if((achievement.dindex !== dindex || (complete && dindex)))
            {
                const pKey=HEX2base64(achievement.UIDBelongsTo)
                        // we have to do it like this to preserve validFrom
                updatedPersons.add(pKey)
                if(achievement.dindex !== dindex)
                {
                    query(`UPDATE ObjectBase SET dindex=${dindex} WHERE UID=?`,
                        [achievement.UID],
                        {backDate:achievement.validFrom}
                    )
                }
            }
            resolve()
        })
    )))
    for(const person64 of updatedPersons)
    {
        const UIDperson= Buffer.from(person64,'base64')
        await updateAchievements( UIDperson)
        // update lists
        await queueAdd(root,sysUser,'listMember',UIDperson,UIDperson,null,UIDperson)
    }
        

}




/**
 * Checks if a user is authorized to manage achievements.
 * 
 * @async
 * @param {string} user - The user's ID to check for authorization.
 * @param {Object|null} filter - The filter to apply on user's jobs.
 *                               If null or undefined, user is automatically authorized.
 * @returns {Promise<boolean>} - Returns true if user is authorized, false otherwise.
 * @description The function retrieves all jobs associated with the user from the database,
 *              applies the provided filter to these jobs, and determines if the user has
 *              the necessary permissions to manage achievements for a person. This is used
 *              to control who can add, modify, or delete achievement records based on their
 *              job roles and permissions.
 */
export const authorizeUser=async (user,filter)=>
{
    // checks if current user is authorized to change this data
    if(!filter)
    {
        return true
    }
    else
    {
        // get all jobs of the user and run them through the authorized filter
        const userJobs= await query(`
            SELECT ObjectBase.UID,ObjectBase.Data AS ExtraData,ObjectBase.Type,
            Main.Data AS MainBaseData, Member.Data
            FROM ObjectBase 
            INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
            INNER JOIN ObjectBase AS Main ON (Main.UID=ObjectBase.UIDBelongsTo)
            WHERE ObjectBase.Type='job' AND ObjectBase.UIDBelongsTo=?`,
            [UUID2hex(user)],
            {cast:['json']}

            )
        const filteredJobs=filterObjects(userJobs,{job:filter})
        if(filteredJobs.length)
            return true
    }
    return false

}