REST Service for POPCORN - ILIAS
alex
2025-11-28 a380a465ce02059a630ef678fecd6666cbdf8f30
GS-2333
2 files added
5 files modified
349 ■■■■ changed files
app.js 23 ●●●●● patch | view | raw | blame | history
bin/courseAdmin.js 101 ●●●●● patch | view | raw | blame | history
lib/db.js 109 ●●●●● patch | view | raw | blame | history
lib/tools.js 11 ●●●●● patch | view | raw | blame | history
package-lock.json 32 ●●●●● patch | view | raw | blame | history
package.json 1 ●●●● patch | view | raw | blame | history
test/testCourseAdmins.js 72 ●●●● patch | view | raw | blame | history
app.js
@@ -316,6 +316,29 @@
      }
   })
   // Kurs Admins - über Rolle
   .get("/api/kurs/rolle/admin", async function (req, res) {
      try {
         const data = await db.getCourseAdminRoles()
         return res.send(data)
      } catch (err) {
         console.error(err)
         return res.code(500).send({status: "error", error: err.toString()})
      }
   })
   // Kurs Admins - über Rolle - FEHLENDE Zuweisung eines tatsächlichen Users
   .get("/api/kurs/rolle/noadmin", async function (req, res) {
      try {
         const data = await db.getCourseWithoutAdminRoles()
         return res.send(data)
      } catch (err) {
         console.error(err)
         return res.code(500).send({status: "error", error: err.toString()})
      }
   })
/////// STATIC / SPA ////////////////////////////////////////////////////////////////
bin/courseAdmin.js
New file
@@ -0,0 +1,101 @@
const yargs = require("yargs")
const csv = require("json-2-csv")
const db = require("../lib/db")
const {exitDelayed} = require("../lib/tools");
/////////////////////////////////////////////////////////////////////////
const argv = yargs
   .command({
      command: "list",
      describe: "list course admin roles",
      builder: {
         format: {
            alias: "f",
            demandOption: false,
            describe: "output format: table(default) | json | csv",
            type: "string",
            default: "table"
         }
      },
      handler: async function ({format}) {
         const data = await db.getCourseAdminRoles()
         await print(data, format)
         exitDelayed()
      }
   })
   .command({
      command: "listn",
      describe: "list courses without user assigned admin role",
      builder: {
         format: {
            alias: "f",
            demandOption: false,
            describe: "output format: table(default) | json | csv",
            type: "string",
            default: "table"
         }
      },
      handler: async function ({format}) {
         const data = await db.getCourseWithoutAdminRoles()
         await print(data, format)
         exitDelayed()
      }
   })
   // .command({
   //    command: "get <item> [_id]",
   //    describe: "get db items - if _id is given, get only one document identified by _id",
   //    builder: {
   //       skip: {
   //          demandOption: false,
   //          describe: "skip this number of items",
   //          type: "number",
   //          default: 0,
   //       },
   //       limit: {
   //          demandOption: false,
   //          describe: "limit to this number of items",
   //          type: "number",
   //          default: 5,
   //       },
   //    },
   //    handler: async function ({item, _id, skip, limit}) {
   //       const db = require("../data/db/db")
   //       if (!_id) {
   //          const res = await db[item].find().skip(skip).limit(limit).lean()
   //          console.log(JSON.stringify(res, null, "   "))
   //       } else {
   //          const res = await db[item].findOne({_id}).lean()
   //          console.log(JSON.stringify(res, null, "   "))
   //       }
   //       exitDelayed()
   //    }
   // })
   .alias("h", "help")
   .demandCommand()
   .version(false)
   // .wrap(yargs.terminalWidth())
   // .wrap(100)
   .strict()
   .argv
/////////////////////////////////////////////////////////////////////////
async function print(data, format) {
   switch (format) {
      case "table":
         console.table(data)
         break;
      case "json":
         console.log(JSON.stringify(data, null, "   "))
         break;
      case "csv":
         const res = await csv.json2csv(data)
         console.log(res)
         break;
      default:
         throw new Error(`unknown format "${format}"`)
   }
}
lib/db.js
@@ -58,8 +58,8 @@
   setStatus,
   getCourseAdmins,
   getCoursesWithNoAdmins,
   getCourseAdminRoles,
   getCourseWithoutAdminRoles,
}
/////////////////////////////////////////////////////////////////////////
@@ -680,50 +680,85 @@
/////// ADMINS ////////////////////////////////////////////////////////////////
async function getCourseAdmins() {
async function getCourseAdminRoles() {
   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 q = `
       SELECT od.obj_id as crs_obj_id, t.ref_id as crs_ref_id, od.title as crs_title, rf.rol_id, od2.title as role
       FROM ${database}.object_data od
                INNER JOIN ${database}.object_reference t ON t.obj_id = od.obj_id
                INNER JOIN ${database}.rbac_fa rf ON rf.parent = t.ref_id
                INNER JOIN ${database}.object_data od2 ON od2.obj_id = rf.rol_id
       WHERE od.\`type\` = "crs"
         AND od2.title LIKE "%admin%"
   `
   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() {
async function getCourseWithoutAdminRoles() {
   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
       SELECT adminRoles.crs_obj_id, adminRoles.crs_ref_id, adminRoles.crs_title, adminRoles.rol_id, adminRoles.role
       FROM (SELECT od.obj_id as crs_obj_id, t.ref_id as crs_ref_id, od.title as crs_title, rf.rol_id, od2.title as role
             FROM ${database}.object_data od
                      INNER JOIN ${database}.object_reference t ON t.obj_id = od.obj_id
                      INNER JOIN ${database}.rbac_fa rf ON rf.parent = t.ref_id
                      INNER JOIN ${database}.object_data od2 ON od2.obj_id = rf.rol_id
             WHERE od.\`type\` = "crs"
               AND od2.title LIKE "%admin%") adminRoles
       WHERE adminRoles.rol_id NOT IN (SELECT ru2.rol_id
                                       FROM ${database}.rbac_ua ru2)
   `
   const [results] = await pool.query(q)
   return results
}
// !! wird nicht gebraucht - Admin Erkennung in ILIAS läuft anders ab - über die Rolle
// 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
// }
lib/tools.js
New file
@@ -0,0 +1,11 @@
module.exports = {
   exitDelayed,
}
/////////////////////////////////////////////////////////////////////////
function exitDelayed (delay = 750) {
   setTimeout(function () {
      process.exit()
   }, delay)
}
package-lock.json
@@ -19,6 +19,7 @@
        "enquirer": "^2.4.1",
        "fastify": "^5.3.3",
        "flexsearch": "^0.8.205",
            "json-2-csv": "^5.5.10",
        "lodash": "^4.17.21",
        "mocha": "^11.6.0",
        "mysql2": "^3.14.1",
@@ -2484,6 +2485,15 @@
        "node": ">=0.10.0"
      }
    },
      "node_modules/deeks": {
         "version": "3.1.0",
         "resolved": "https://registry.npmjs.org/deeks/-/deeks-3.1.0.tgz",
         "integrity": "sha512-e7oWH1LzIdv/prMQ7pmlDlaVoL64glqzvNgkgQNgyec9ORPHrT2jaOqMtRyqJuwWjtfb6v+2rk9pmaHj+F137A==",
         "license": "MIT",
         "engines": {
            "node": ">= 16"
         }
      },
    "node_modules/deep-eql": {
      "version": "5.0.2",
      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
@@ -2570,6 +2580,15 @@
      "license": "BSD-3-Clause",
      "engines": {
        "node": ">=0.3.1"
         }
      },
      "node_modules/doc-path": {
         "version": "4.1.1",
         "resolved": "https://registry.npmjs.org/doc-path/-/doc-path-4.1.1.tgz",
         "integrity": "sha512-h1ErTglQAVv2gCnOpD3sFS6uolDbOKHDU1BZq+Kl3npPqroU3dYL42lUgMfd5UimlwtRgp7C9dLGwqQ5D2HYgQ==",
         "license": "MIT",
         "engines": {
            "node": ">=16"
      }
    },
    "node_modules/dotenv": {
@@ -3520,6 +3539,19 @@
        "node": ">=6"
      }
    },
      "node_modules/json-2-csv": {
         "version": "5.5.10",
         "resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.5.10.tgz",
         "integrity": "sha512-Dep8wO3Fr5wNjQevO2Z8Y7yeee/nYSGRsi7q6zJDKEVHxXkXT+v21vxHmDX923UzmCXXkSo62HaTz6eTWzFLaw==",
         "license": "MIT",
         "dependencies": {
            "deeks": "3.1.0",
            "doc-path": "4.1.1"
         },
         "engines": {
            "node": ">= 16"
         }
      },
    "node_modules/json-schema-ref-resolver": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz",
package.json
@@ -25,6 +25,7 @@
      "enquirer": "^2.4.1",
      "fastify": "^5.3.3",
      "flexsearch": "^0.8.205",
      "json-2-csv": "^5.5.10",
      "lodash": "^4.17.21",
      "mocha": "^11.6.0",
      "mysql2": "^3.14.1",
test/testCourseAdmins.js
@@ -15,37 +15,71 @@
   afterEach(async function () {
   })
   describe("the function getCourseAdmins()", function () {
      it("should get the Admins of all courses", async function () {
         const res = await db.getCourseAdmins()
         console.table(res)
   describe("the function getCourseAdminRoles()", function () {
      it("should return all admin roles for all courses", async function () {
         const res = await db.getCourseAdminRoles()
         // console.table(res)
         // console.log(res)
         expect(res).to.be.a("array")
         for (const item of res) {
            expect(item).to.have.property("kurs_obj_id").and.to.be.a("number")
            expect(item).to.have.property("kurs_ref_id").and.to.be.a("number")
            expect(item).to.have.property("usr_id").and.to.be.a("number")
            expect(item).to.have.property("login").and.to.be.a("string")
            expect(item).to.have.property("firstname").and.to.be.a("string")
            expect(item).to.have.property("lastname").and.to.be.a("string")
            expect(item).to.have.property("title").and.to.be.a("string")
            expect(item).to.have.property("crs_obj_id").and.to.be.a("number")
            expect(item).to.have.property("crs_ref_id").and.to.be.a("number")
            expect(item).to.have.property("crs_title").and.to.be.a("string")
            expect(item).to.have.property("rol_id").and.to.be.a("number")
            expect(item).to.have.property("role").and.to.be.a("string")
         }
      })
   })
   describe("the function getCoursesWithNoAdmins()", function () {
      it("should return all courses without a single admin member", async function () {
         const res = await db.getCoursesWithNoAdmins()
         console.table(res)
   describe("the function getCoursesWithoutAdminRoles()", function () {
      it("should return all admin roles for all courses", async function () {
         const res = await db.getCourseWithoutAdminRoles()
         // console.table(res)
         console.log(res)
         expect(res).to.be.a("array")
         for (const item of res) {
            expect(item).to.have.property("obj_id").and.to.be.a("number")
            expect(item).to.have.property("ref_id").and.to.be.a("number")
            expect(item).to.have.property("numTn").and.to.be.a("number")
            expect(item).to.have.property("title").and.to.be.a("string")
            expect(item).to.have.property("crs_obj_id").and.to.be.a("number")
            expect(item).to.have.property("crs_ref_id").and.to.be.a("number")
            expect(item).to.have.property("crs_title").and.to.be.a("string")
            expect(item).to.have.property("rol_id").and.to.be.a("number")
            expect(item).to.have.property("role").and.to.be.a("string")
         }
      })
   })
   // wird nicht gebraucht - Admin Erkennung in ILIAS läuft anders ab
   // describe("the function getCourseAdmins()", function () {
   //    it("should get the Admins of all courses", async function () {
   //       const res = await db.getCourseAdmins()
   //       console.table(res)
   //       expect(res).to.be.a("array")
   //       for (const item of res) {
   //          expect(item).to.have.property("kurs_obj_id").and.to.be.a("number")
   //          expect(item).to.have.property("kurs_ref_id").and.to.be.a("number")
   //          expect(item).to.have.property("usr_id").and.to.be.a("number")
   //          expect(item).to.have.property("login").and.to.be.a("string")
   //          expect(item).to.have.property("firstname").and.to.be.a("string")
   //          expect(item).to.have.property("lastname").and.to.be.a("string")
   //          expect(item).to.have.property("title").and.to.be.a("string")
   //       }
   //    })
   // })
   //
   // describe("the function getCoursesWithNoAdmins()", function () {
   //    it("should return all courses without a single admin member", async function () {
   //       const res = await db.getCoursesWithNoAdmins()
   //       console.table(res)
   //       expect(res).to.be.a("array")
   //       for (const item of res) {
   //          expect(item).to.have.property("obj_id").and.to.be.a("number")
   //          expect(item).to.have.property("ref_id").and.to.be.a("number")
   //          expect(item).to.have.property("numTn").and.to.be.a("number")
   //          expect(item).to.have.property("title").and.to.be.a("string")
   //       }
   //    })
   // })
})
/////////////////////////////////////////////////////////////////////////