REST Service for POPCORN - ILIAS
alex
2025-11-27 a8c152f255665bc4eae00cc5a7f266cc9e7ffa51
lib/db.js
@@ -1,12 +1,16 @@
const mysql = require("mysql2/promise")
const dayjs = require("dayjs")
const _ = require("lodash")
const log = require("../logger")
const searchLib = require("./search")
const {host, port, user, database, password} = require("./../settings").db
/////////////////////////////////////////////////////////////////////////
let poolP = initPool()
async function initPool () {
async function initPool() {
   return mysql.createPool({
      host,
      port,
@@ -33,52 +37,106 @@
   getKurse,
   getKurs,
   getKursItems,
   getKursItems2,
   // getKursByObjId,
   // getKursByRefId,
   getKursTeilnehmer,
   getSingleKursTeilnehmer,
   getKursTeilnehmerCount,
   getKursLp,
   getKursUnterobjektLp,
   getKursOffline,
   setKursOffline,
   getUdf,
   getKursTeilnehmerRolle,
   getKursTeilnehmerByRole,
   getKursRoles,
   setStatus,
   getCourseAdmins,
   getCoursesWithNoAdmins,
}
/////////////////////////////////////////////////////////////////////////
async function getUsers (offset = 0, limit = 10) {
   limit = Number(limit) || 10
   offset = Number(offset) || 0
async function getUserSearchQuery(offset, limit, search) {
   const sel = `usr_id, login, firstname, lastname, gender, email, institution, street, city, zipcode, country, department, active`
   // TODO user defined fields
   // TODO check args for SQL Injection
   const pool = await poolP
   const [results, fields] = await pool.query(
      `SELECT ${sel}
       FROM ${database}.usr_data AS ud
       WHERE login REGEXP '^[0-9]+$'
       LIMIT ${limit}
       OFFSET ${offset}
   if (!search || search === '') {
      return `SELECT ${sel}
              FROM ${database}.usr_data AS ud
              WHERE login REGEXP '^[0-9]+$'
              LIMIT ${limit}
              OFFSET ${offset}
      `
   )
   const count = await getUserCount()
   return {
      total: count,
      offset, limit,
      data: results,
   } else {
      const ids = await searchLib.search(search)
      if (!ids.length) {
         throw "nothing found"
         return await getUserSearchQuery(offset, limit)
      } // nothing found
      return `SELECT ${sel}
              FROM ${database}.usr_data AS ud
              WHERE login REGEXP '^[0-9]+$'
              AND usr_id IN (${ids.join(",")})
                  LIMIT ${limit}
              OFFSET ${offset}
      `
   }
}
async function getUserCount () {
async function getUsers(offset = 0, limit = 10, search = null) {
   // log.info("++++++++++ get users", offset, limit, search)
   limit = Number(limit) || 10
   offset = Number(offset) || 0
   // TODO check args for SQL Injection
   const pool = await poolP
   const [results, fields] = await pool.query(
      `SELECT COUNT(*)
       FROM ${database}.usr_data AS ud
       WHERE login REGEXP '^[0-9]+$'`
   )
   return results[0]["COUNT(*)"]
   try {
      let userSearchQuery = await getUserSearchQuery(offset, limit, search)
      // log.info(userSearchQuery)
      const [results, fields] = await pool.query(userSearchQuery)
      // console.log(results, fields)
      const count = await getUserCount(offset, limit, search)
      return {
         total: count,
         offset, limit,
         data: results,
      }
   } catch (ex) {
      return {
         total: 0,
         offset: 0,
         limit: 0,
         data: [],
      }
   }
}
async function getUserByLogin (login) {
async function getUserCount(offset, limit, search) {
   const pool = await poolP
   // const q = getUserSearchQuery(offset, limit, search)
   const q = await getUserSearchQuery(0, 1000000, search) // hier darf kein Limit sein, offset=0
   const q2 = `SELECT COUNT(*) AS count
               FROM (${q}) AS X`
   const [results, fields] = await pool.query(q2)
   return results[0].count
   // const [results, fields] = await pool.query(
   //    `SELECT COUNT(*)
   //     FROM ${database}.usr_data AS ud
   //     WHERE login REGEXP '^[0-9]+$'`
   // )
   // return results[0]["COUNT(*)"]
}
async function getUserByLogin(login) {
   const sel = `usr_id, login, firstname, lastname, gender, email, institution, street, city, zipcode, country, department, active`
   // TODO user defined fields
   // TODO check args for SQL Injection
   const pool = await poolP
@@ -87,13 +145,11 @@
       FROM ${database}.usr_data AS ud
       WHERE login = '${login}'`
   )
   console.log(results)
   return joinUDF(results[0])
}
async function getUserByUserId (userId) {
async function getUserByUserId(userId) {
   const sel = `usr_id, login, firstname, lastname, gender, email, institution, street, city, zipcode, country, department, active`
   // TODO user defined fields
   // TODO check args for SQL Injection
   const pool = await poolP
@@ -105,7 +161,7 @@
   return joinUDF(results[0])
}
async function getUserDefinedFields () {
async function getUserDefinedFields() {
   const pool = await poolP
   const [results] = await pool.query(
      `SELECT ut.usr_id, ud.field_name, ut.value
@@ -115,7 +171,7 @@
   return results
}
async function getUserDefinedField (usr_id) {
async function getUserDefinedField(usr_id) {
   const pool = await poolP
   const [results] = await pool.query(
      `SELECT ut.usr_id, ud.field_name, ut.value
@@ -129,7 +185,11 @@
/////// obj_id / ref_id ////////////////////////////////////////////////////////////////
async function getObjIdFromRefId (refId) {
/**
 * @param refId
 * @returns {Promise<{ref_id,obj_id}|undefined>}
 */
async function getObjIdFromRefId(refId) {
   const pool = await poolP
   const [results] = await pool.query(
      `SELECT ref_id, obj_id
@@ -140,7 +200,7 @@
   return results.length ? results[0] : undefined
}
async function getRefIdFromObjId (objId) {
async function getRefIdFromObjId(objId) {
   const pool = await poolP
   const [results] = await pool.query(
      `SELECT ref_id, obj_id
@@ -153,11 +213,11 @@
/////////////////////////////////////////////////////////////////////////
async function promiseDelay (ms) {
async function promiseDelay(ms) {
   return new Promise(resolve => setTimeout(resolve, ms))
}
async function joinUDF (user) {
async function joinUDF(user) {
   if (!user) return user
   const fields = await getUserDefinedField(user.usr_id)
   for (const field of fields) {
@@ -168,9 +228,9 @@
/////// Kurs ////////////////////////////////////////////////////////////////
async function getKurse () {
async function getKurse() {
   const pool = await poolP
   const q = `SELECT or2.ref_id, or2.obj_id, od.title, od.description, od.type
   const q = `SELECT or2.ref_id, or2.obj_id, od.title, od.description, od.type, od.offline
              FROM ${database}.object_reference or2
                       INNER JOIN ${database}.object_data od ON od.obj_id = or2.obj_id
              WHERE od.type = 'crs'
@@ -185,19 +245,20 @@
//    return getKursByObjId(obj_id)
// }
async function getKurs (ref_id) {
async function getKurs(ref_id) {
   const pool = await poolP
   const q = `SELECT or2.ref_id, or2.obj_id, od.title, od.description, od.type
   const q = `SELECT or2.ref_id, or2.obj_id, od.title, od.description, od.type, od.create_date, od.offline
              FROM ${database}.object_reference or2
                       INNER JOIN ${database}.object_data od ON od.obj_id = or2.obj_id
              WHERE or2.ref_id = '${ref_id}'
                AND or2.deleted IS NULL
   `
   let [results] = await pool.query(q)
   results = results.length ? results[0] : undefined
   return results
}
async function getKursItems (ref_id) {
async function getKursItems(ref_id) {
   const pool = await poolP
   // const q = `SELECT or2.ref_id, or2.obj_id, od.title, od.description, od.type
   //            FROM ${database}.object_reference or2
@@ -216,7 +277,42 @@
   return results
}
async function getKursTeilnehmer (ref_id) {
async function getKursItems2(ref_id) {
   const pool = await poolP
   const q = `
       WITH RECURSIVE tree (parent_id, obj_id, ref_id, title, type) AS
                          (SELECT ci.parent_id,
                                  or2.obj_id,
                                  ci.obj_id as ref_id,
                                  od.title,
                                  od.type
                           FROM ${database}.crs_items ci
                                    INNER JOIN ${database}.object_reference or2 ON or2.ref_id = ci.obj_id
                                    INNER JOIN ${database}.object_data od ON od.obj_id = or2.obj_id
                           WHERE ci.obj_id = ${ref_id} -- Kurs ref_id
                             AND or2.deleted is NULL
                           UNION ALL
                           SELECT child.parent_id,
                                  or2.obj_id,
                                  child.obj_id as ref_id,
                                  od.title,
                                  od.type
                           FROM ${database}.crs_items child
                                    INNER JOIN ${database}.object_reference or2 ON or2.ref_id = child.obj_id
                                    INNER JOIN ${database}.object_data od ON od.obj_id = or2.obj_id
                                    JOIN tree ON child.parent_id = tree.ref_id
                           WHERE or2.deleted is NULL)
       SELECT *
       FROM tree
       ORDER BY tree.ref_id
   `
   const [results] = await pool.query(q)
   return results
}
async function getKursTeilnehmer(ref_id) {
   const pool = await poolP
   const q = `SELECT ci.parent_id,
                     or2.ref_id,
@@ -238,13 +334,46 @@
                       INNER JOIN ${database}.usr_data ud ON ud.usr_id = om.usr_id
                       LEFT JOIN ${database}.ut_lp_marks ulm ON ulm.obj_id = or2.obj_id AND ud.usr_id = ulm.usr_id
              WHERE (or2.ref_id = ${ref_id} OR parent_id = ${ref_id})
                AND or2.deleted IS NULL
              ORDER BY usr_id
   `
   // log.info(q)
   const [results] = await pool.query(q)
   return results
}
async function getKursTeilnehmerCount (ref_id) {
async function getSingleKursTeilnehmer(ref_id, usr_id) {
   const pool = await poolP
   const q = `SELECT ci.parent_id,
                     or2.ref_id,
                     or2.obj_id,
                     od.title,
                     od.type,
                     om.usr_id,
                     ud.login,
                     ud.firstname,
                     ud.lastname,
                     ud.active,
                     om.passed,
                     ulm.status,
                     ulm.status_changed
              FROM ${database}.crs_items ci
                       INNER JOIN ${database}.object_reference or2 ON or2.ref_id = ci.obj_id
                       INNER JOIN ${database}.object_data od ON od.obj_id = or2.obj_id
                       INNER JOIN ${database}.obj_members om ON om.obj_id = or2.obj_id AND om.member = 1
                       INNER JOIN ${database}.usr_data ud ON ud.usr_id = om.usr_id
                       LEFT JOIN ${database}.ut_lp_marks ulm ON ulm.obj_id = or2.obj_id AND ud.usr_id = ulm.usr_id
              WHERE (or2.ref_id = ${ref_id}
                  OR parent_id = ${ref_id})
                AND om.usr_id = ${usr_id}
              ORDER BY usr_id
   `
   // log.info(q)
   const [results] = await pool.query(q)
   return results[0]
}
async function getKursTeilnehmerCount(ref_id) {
   const pool = await poolP
   const q = `SELECT COUNT(*) as count
              FROM ${database}.crs_items ci
@@ -263,9 +392,9 @@
   return {ref_id, count: results.count}
}
async function getUserTeilnahmen (usr_id) {
async function getUserTeilnahmen(usr_id) {
   const pool = await poolP
   const q = `SELECT om.obj_id, or2.ref_id , om.usr_id, od.title , ulm.status, om.passed, ulm.status_changed
   const q = `SELECT om.obj_id, or2.ref_id, om.usr_id, od.title, ulm.status, om.passed, ulm.status_changed
              FROM ${database}.obj_members om
                       INNER JOIN ${database}.object_reference or2 ON or2.obj_id = om.obj_id
                       INNER JOIN ${database}.usr_data ud ON ud.usr_id = om.usr_id
@@ -275,7 +404,326 @@
              WHERE om.usr_id = ${usr_id}
                AND om.member = 1
   `
   console.log(q)
   // log.info(q).catch(console.error)
   const [results] = await pool.query(q)
   return results
}
async function getKursLp(obj_id, raw = false) {
   const {ref_id} = await getRefIdFromObjId(obj_id)
   const tnUnter = await getKursUnterobjektLp(obj_id)
   if (raw) {
      return tnUnter
   } else { // LP aller Unterobjekte zusammenfassen
      const libLp = require("../lib/libLp")
      const data = libLp.alleAuswerten(tnUnter)
      return data
   }
   // const teilnehmer = await getKursTeilnehmer(ref_id)
   // const tnUnter = await getKursUnterobjektLp(obj_id)
   // console.table(teilnehmer)
   // console.table(tnUnter)
   //
   // /**
   //  * Die beiden Datensätze mergen
   //  * NEIN - es reicht der
   //  *
   //  */
   //
   // const idx = _.groupBy(tnUnter, "usr_id")
   // for (const tn of teilnehmer) {
   //     delete tn.parent_id
   //     delete tn.type
   //     delete tn.active
   //
   //     /** @type Array */
   //     const unter = idx[tn.usr_id]
   //     if (!unter) continue
   //
   //     /**
   //      * wenn unter.status_changed neuer, wird unter tn.status_changed vorgezogen
   //      * wenn unter.status neuer, überscheibt es tn.status
   //      * tn.passed muss zurückgesetzt werden wenn tn.status überschrieben wird
   //      */
   //     tn.status_overwrite = false
   //
   //     // max unter status_changed finden
   //     const unterStatusChanged = _.max(unter.map(u => u.status_changed))
   //
   //     // es muss nur überschrieben werden wenn das Unterdatum größer ist
   //     if (unterStatusChanged > tn.status_changed) {
   //         // unter Status auswerten
   //         // 0 = noch nicht bearbeitet
   //         // 1 = in Bearbeitung
   //         // 2 = bestanden
   //         // 3 = nicht bestanden
   //
   //         /** @type Array */
   //         const unterStatusse = unter.map(u => u.status)
   //         let newStatus = tn.status
   //         const allSame = function () {
   //             if (!unterStatusse.length) return false
   //             const first = unterStatusse[0]
   //             return unterStatusse.every(it => it === first)
   //         }()
   //         console.log({unterStatusse})
   //         // Fall 1: keine Unterstatussse vorhanden -> status vom Kurs
   //         if (!unterStatusse.length) {
   //             newStatus = tn.status
   //         }
   //         // Fall 2: eines nicht bestanden -> nicht bestanden // 0,1,2,3 -> 3
   //         if (unterStatusse.some(u => u === 3)) {
   //             newStatus = 3
   //         }
   //         // Fall 3: alle statusse gleich -> status // 0,0,0 1,1,1 2,2,2 3,3,3
   //         else if (allSame) {
   //             newStatus = unterStatusse[0]
   //         }
   //         // Fall 4: wenn eines in Bearbeitung -> in Bearbeitung // 0,1,0 2,1,2
   //         else if (unterStatusse.some(u => u === 1)) {
   //             newStatus = 1
   //         }
   //         // Fall 5: sonst in Bearbeitung
   //         else {
   //             // newStatus = Math.max.apply(this, unterStatusse)
   //             newStatus = 1
   //         }
   //         tn.status_changed = unterStatusChanged
   //         tn.status = newStatus
   //         tn.status_overwrite = true
   //     }
   // }
   // return teilnehmer
}
async function getKursUnterobjektLp(obj_id) {
   const pool = await poolP
   const q = `SELECT ud.usr_id,
                     ud.login,
                     ud.firstname,
                     ud.lastname,
                     ulc.obj_id,
                     ulc.item_id,
                     ulc.lpmode,
                     t.obj_id as item_obj_id,
                     od.type,
                     ulm.status,
                     ulm.status_changed,
                     ulm.percentage,
                     ulm.completed
              FROM ${database}.ut_lp_collections ulc
                       INNER JOIN ${database}.object_reference t ON t.ref_id = ulc.item_id
                       INNER JOIN ${database}.ut_lp_marks ulm ON ulm.obj_id = t.obj_id
                       INNER JOIN ${database}.usr_data ud ON ud.usr_id = ulm.usr_id
                       INNER JOIN ${database}.object_data od ON od.obj_id = t.obj_id
              WHERE ulc.obj_id = ${obj_id} # obj_id Kurs
                    AND ulc.active = 1
                    AND ulc.lpmode = 5  # nur mode 5
              ORDER BY ud.usr_id, ulc.item_id
   `
   const [results] = await pool.query(q)
   return results
}
async function getKursOffline(obj_id) {
   const pool = await poolP
   const q = `SELECT offline
              FROM ${database}.object_data
              WHERE obj_id = ${obj_id}
   `
   const [results] = await pool.query(q)
   return results[0]
}
async function setKursOffline(isOffline, obj_id) {
   const pool = await poolP
   const q = `UPDATE ${database}.object_data
              SET offline = ${isOffline}
              WHERE obj_id = ${obj_id}
   `
   const [results] = await pool.query(q)
   // return results
   return {offline: isOffline}
}
/////// UDF ////////////////////////////////////////////////////////////////
async function getUdf() {
   const pool = await poolP
   const q = `SELECT field_id, field_name, field_type
              from ${database}.udf_definition;`
   const [results] = await pool.query(q)
   return results
}
/////// ROLLEN ////////////////////////////////////////////////////////////////
async function getKursTeilnehmerRolle(obj_id) {
   const pool = await poolP
   const q = `SELECT obj_id, title, description
              from object_data od
              WHERE type = "role" #AND title LIKE 'il_crs_member_157'
                AND od.description LIKE 'Member%${obj_id}'
   `
   const [results] = await pool.query(q)
   // darf nur einen Treffer ergeben
   if (results.length) {
      const {obj_id, title, description} = results[0]
      return obj_id
   } else {
      return null
   }
}
async function getKursTeilnehmerByRole(obj_id) {
   const pool = await poolP
   const q = `
       SELECT obj_id as role_id, ru.usr_id, ud.firstname, ud.lastname
       FROM ${database}.object_data od
                INNER JOIN ${database}.rbac_ua ru ON ru.rol_id = od.obj_id
                INNER JOIN ${database}.usr_data ud ON ud.usr_id = ru.usr_id
       WHERE type = "role" #AND title LIKE 'il_crs_member_157'
                AND od.description LIKE 'Member%${obj_id}'
   `
   const [results] = await pool.query(q)
   return results
}
async function getKursRoles(ref_id) {
   const pool = await poolP
   const q = `
       SELECT pa.rol_id, or2.ref_id, or2.obj_id, od2.type, od2.title, od2.description
       FROM ${database}.rbac_pa pa
                INNER JOIN ${database}.object_reference or2 ON or2.ref_id = pa.ref_id
                INNER JOIN ${database}.object_data od ON od.obj_id = or2.obj_id
                INNER JOIN ${database}.object_data od2 ON od2.obj_id = pa.rol_id
       WHERE pa.ref_id = ${ref_id}
   `
   const [results] = await pool.query(q)
   return results
}
/////// STATUS ////////////////////////////////////////////////////////////////
/**
 * Zwei Möglichkeiten:
 * 1. es gibt schon einen Eintrag in ut_lp_marks, dann muss man diesen updaten
 * 2. es gibt noch keinen Eintrag in ut_lp_marks, dann muss erst einer erstellt werden (NEIN! Für neuen TN sind bereits beide Einträge vorhanden, kann also wegfallen)
 *
 * Weiterhin haben wir es mit zwei Tabellen zu tun:
 *    - obj_members (passed)
 *    - ut_lp_marks (status)
 *
 * UPDATE: Wir nehmen an, dass beide Einträge schon vorhanden sind!
 *
 * @param courseId
 * @param userId
 * @param {Number} passed
 * @param {Number} status
 * @returns {Promise<{status: string}>}
 */
async function setStatus(courseId, userId, passed = null, status = null) {
   const pool = await poolP
   log.info(`------------setStatus----- courseId=${courseId}  userId=${userId}  passed=${passed}  status=${status}`)
   passed = isNaN(Number(passed)) ? passed : Number(passed)
   status = isNaN(Number(status)) ? status : Number(status)
   // ACHTUNG Transactions funktioneren so nicht, erst mal ohne machen...
   const q = `
START TRANSACTION;
UPDATE ${database}.ut_lp_marks ulm
SET status = ${status}
WHERE ulm.usr_id = 31793 AND ulm.obj_id = 32212;
UPDATE ${database}.obj_members om
SET passed = ${passed}
WHERE om.usr_id = ${userId} AND om.obj_id = ${courseId};
COMMIT;
`
   let date = dayjs().format('YYYY-MM-DD HH:mm:ss')
   const q1 = `
       UPDATE ${database}.ut_lp_marks ulm
       SET status         = ${status},
           status_changed = "${date}"
       WHERE ulm.usr_id = ${userId}
         AND ulm.obj_id = ${courseId};
   `
   const q2 = `
       UPDATE ${database}.obj_members om
       SET passed = ${passed}
       WHERE om.usr_id = ${userId}
         AND om.obj_id = ${courseId};
   `
   const [results1] = await pool.query(q1)
   const [results2] = await pool.query(q2)
   const {affectedRows: affectedRows1} = results1
   const {affectedRows: affectedRows2} = results2
   if (affectedRows1 && affectedRows2) {
      return {status: "ok"}
   } else {
      throw {
         status: "error",
         reason: {affectedRows1, affectedRows2}
      }
   }
}
/////// ADMINS ////////////////////////////////////////////////////////////////
async function getCourseAdmins() {
   const pool = await poolP
   const q = `SELECT om.obj_id as kurs_obj_id,
                     t.ref_id  as kurs_ref_id,
                     om.usr_id,
                     ud.login,
                     om.admin,
                     ud.firstname,
                     ud.lastname,
                     od2.title
              FROM ${database}.obj_members om
                       INNER JOIN ${database}.usr_data ud ON ud.usr_id = om.usr_id
                       INNER JOIN ${database}.object_data od2 ON od2.obj_id = om.obj_id
                       INNER JOIN ${database}.object_reference t ON t.obj_id = om.obj_id
              WHERE om.admin = 1
   `
   const [results] = await pool.query(q)
   return results
}
/**
 * Liefert die Kurse ohne Admins
 * Admins hier definiert als Einträge in obj_members wo admin==1
 * Darüber hinaus gibt es offenbar noch einen anderen Mechanismus über die Rolle.
 * Denn ein Kurs ohne Admin (z.B. lokal Fliesenratgeber ref_id=88) hat in ILIAS
 * trotzdem einen Admin im Screen "Members".
 * Dort wird wohl über die Rolle zugeordnet.
 *
 * Die Frage ist wo der Fehler GS-2333 auftritt.
 * Bei obj_members oder bei fehlender Rolle.
 * @return {Promise<*>}
 */
async function getCoursesWithNoAdmins() {
   const pool = await poolP
   const q = `
       SELECT asdf.obj_id, t.ref_id, asdf.numTn, asdf.title
       FROM (SELECT om.obj_id, COUNT(*) as numTn, od.title, MAX(om.admin) as maxAdmin
             FROM ${database}.obj_members om
                      INNER JOIN ${database}.object_data od ON od.obj_id = om.obj_id
             GROUP by om.obj_id
             ORDER BY numTn DESC) asdf
                INNER JOIN ${database}.object_reference t ON t.obj_id = asdf.obj_id
       WHERE asdf.maxAdmin = 0
   `
   const [results] = await pool.query(q)
   return results
}