@citolab/qti-backend-firebase 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE.md +674 -0
  2. package/dist/api/app-specific/base/application-specific-base.d.ts +45 -0
  3. package/dist/api/app-specific/base/application-specific-base.d.ts.map +1 -0
  4. package/dist/api/app-specific/base/application-specific-base.js +503 -0
  5. package/dist/api/app-specific/base/application-specific-base.js.map +1 -0
  6. package/dist/api/app-specific/base/cheerio-helper.d.ts +5 -0
  7. package/dist/api/app-specific/base/cheerio-helper.d.ts.map +1 -0
  8. package/dist/api/app-specific/base/cheerio-helper.js +16 -0
  9. package/dist/api/app-specific/base/cheerio-helper.js.map +1 -0
  10. package/dist/api/app-specific/baseImplementation.d.ts +6 -0
  11. package/dist/api/app-specific/baseImplementation.d.ts.map +1 -0
  12. package/dist/api/app-specific/baseImplementation.js +11 -0
  13. package/dist/api/app-specific/baseImplementation.js.map +1 -0
  14. package/dist/api/app-specific/index.d.ts +4 -0
  15. package/dist/api/app-specific/index.d.ts.map +1 -0
  16. package/dist/api/app-specific/index.js +4 -0
  17. package/dist/api/app-specific/index.js.map +1 -0
  18. package/dist/api/app-specific/interface/IApplicationSpecific.d.ts +31 -0
  19. package/dist/api/app-specific/interface/IApplicationSpecific.d.ts.map +1 -0
  20. package/dist/api/app-specific/interface/IApplicationSpecific.js +2 -0
  21. package/dist/api/app-specific/interface/IApplicationSpecific.js.map +1 -0
  22. package/dist/api/app-specific/specific.d.ts +1 -0
  23. package/dist/api/app-specific/specific.d.ts.map +1 -0
  24. package/dist/api/app-specific/specific.js +31 -0
  25. package/dist/api/app-specific/specific.js.map +1 -0
  26. package/dist/api/index.d.ts +3 -0
  27. package/dist/api/index.d.ts.map +1 -0
  28. package/dist/api/index.js +15 -0
  29. package/dist/api/index.js.map +1 -0
  30. package/dist/api/qti-data.d.ts +4 -0
  31. package/dist/api/qti-data.d.ts.map +1 -0
  32. package/dist/api/qti-data.js +622 -0
  33. package/dist/api/qti-data.js.map +1 -0
  34. package/dist/api/qti-resources.d.ts +4 -0
  35. package/dist/api/qti-resources.d.ts.map +1 -0
  36. package/dist/api/qti-resources.js +348 -0
  37. package/dist/api/qti-resources.js.map +1 -0
  38. package/dist/api/qti-teacher.d.ts +6 -0
  39. package/dist/api/qti-teacher.d.ts.map +1 -0
  40. package/dist/api/qti-teacher.js +807 -0
  41. package/dist/api/qti-teacher.js.map +1 -0
  42. package/dist/api/qti-tools.d.ts +3 -0
  43. package/dist/api/qti-tools.d.ts.map +1 -0
  44. package/dist/api/qti-tools.js +450 -0
  45. package/dist/api/qti-tools.js.map +1 -0
  46. package/dist/console.d.ts +2 -0
  47. package/dist/console.d.ts.map +1 -0
  48. package/dist/console.js +30 -0
  49. package/dist/console.js.map +1 -0
  50. package/dist/express-test.d.ts +2 -0
  51. package/dist/express-test.d.ts.map +1 -0
  52. package/dist/express-test.js +19 -0
  53. package/dist/express-test.js.map +1 -0
  54. package/dist/helpers/ci-bootstap.d.ts +4 -0
  55. package/dist/helpers/ci-bootstap.d.ts.map +1 -0
  56. package/dist/helpers/ci-bootstap.js +96 -0
  57. package/dist/helpers/ci-bootstap.js.map +1 -0
  58. package/dist/helpers/database.d.ts +110 -0
  59. package/dist/helpers/database.d.ts.map +1 -0
  60. package/dist/helpers/database.js +151 -0
  61. package/dist/helpers/database.js.map +1 -0
  62. package/dist/helpers/db.d.ts +3 -0
  63. package/dist/helpers/db.d.ts.map +1 -0
  64. package/dist/helpers/db.js +10 -0
  65. package/dist/helpers/db.js.map +1 -0
  66. package/dist/helpers/endpoint-helpers.d.ts +33 -0
  67. package/dist/helpers/endpoint-helpers.d.ts.map +1 -0
  68. package/dist/helpers/endpoint-helpers.js +149 -0
  69. package/dist/helpers/endpoint-helpers.js.map +1 -0
  70. package/dist/helpers/firebase.d.ts +7 -0
  71. package/dist/helpers/firebase.d.ts.map +1 -0
  72. package/dist/helpers/firebase.js +15 -0
  73. package/dist/helpers/firebase.js.map +1 -0
  74. package/dist/helpers/index.d.ts +11 -0
  75. package/dist/helpers/index.d.ts.map +1 -0
  76. package/dist/helpers/index.js +11 -0
  77. package/dist/helpers/index.js.map +1 -0
  78. package/dist/helpers/local-helpers.d.ts +2 -0
  79. package/dist/helpers/local-helpers.d.ts.map +1 -0
  80. package/dist/helpers/local-helpers.js +11 -0
  81. package/dist/helpers/local-helpers.js.map +1 -0
  82. package/dist/helpers/logic.d.ts +73 -0
  83. package/dist/helpers/logic.d.ts.map +1 -0
  84. package/dist/helpers/logic.js +759 -0
  85. package/dist/helpers/logic.js.map +1 -0
  86. package/dist/helpers/package-upload.d.ts +16 -0
  87. package/dist/helpers/package-upload.d.ts.map +1 -0
  88. package/dist/helpers/package-upload.js +160 -0
  89. package/dist/helpers/package-upload.js.map +1 -0
  90. package/dist/helpers/package.d.ts +32 -0
  91. package/dist/helpers/package.d.ts.map +1 -0
  92. package/dist/helpers/package.js +373 -0
  93. package/dist/helpers/package.js.map +1 -0
  94. package/dist/helpers/request-headers.d.ts +27 -0
  95. package/dist/helpers/request-headers.d.ts.map +1 -0
  96. package/dist/helpers/request-headers.js +144 -0
  97. package/dist/helpers/request-headers.js.map +1 -0
  98. package/dist/helpers/storage.d.ts +10 -0
  99. package/dist/helpers/storage.d.ts.map +1 -0
  100. package/dist/helpers/storage.js +132 -0
  101. package/dist/helpers/storage.js.map +1 -0
  102. package/dist/helpers/utils.d.ts +42 -0
  103. package/dist/helpers/utils.d.ts.map +1 -0
  104. package/dist/helpers/utils.js +228 -0
  105. package/dist/helpers/utils.js.map +1 -0
  106. package/dist/main.d.ts +2 -0
  107. package/dist/main.d.ts.map +1 -0
  108. package/dist/main.js +14 -0
  109. package/dist/main.js.map +1 -0
  110. package/package.json +101 -0
  111. package/readme.md +11 -0
@@ -0,0 +1,759 @@
1
+ import { getAuth, getFirestore, getStorage } from "./firebase";
2
+ import fs from "fs/promises";
3
+ import mime from "mime";
4
+ import { createCode, removeDoubleSlashes } from "./utils";
5
+ import { DATABASE } from "./database";
6
+ import { deleteFilesRecursive } from "./storage";
7
+ import NodeCache from "node-cache";
8
+ export async function uploadFolderToStorage(folder, storageDestinationPath, uploadFiles, subfolder) {
9
+ const storage = getStorage();
10
+ const bucket = storage.bucket();
11
+ const fullFolder = subfolder ? `${folder}/${subfolder}` : folder;
12
+ const files = await fs.readdir(fullFolder);
13
+ for (const file of files) {
14
+ if (file === ".DS_Store") {
15
+ continue;
16
+ }
17
+ const filePath = `${fullFolder}/${file}`;
18
+ const stat = await fs.stat(filePath);
19
+ const fileStructure = removeDoubleSlashes(filePath.replace(folder, ""));
20
+ if (stat.isDirectory()) {
21
+ // Recursively upload files in subfolder
22
+ await uploadFolderToStorage(folder, `${storageDestinationPath}/${file}`, uploadFiles, fileStructure);
23
+ }
24
+ else {
25
+ console.log(`uploading ${filePath}`);
26
+ uploadFiles.push(bucket.upload(`${fullFolder}/${file}`, {
27
+ destination: `${storageDestinationPath}/${file}`,
28
+ metadata: {
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ contentType: mime.getType(file),
31
+ },
32
+ }));
33
+ }
34
+ }
35
+ }
36
+ const applicationInfoCache = new NodeCache({
37
+ stdTTL: 60 * 5, // 5 minutes
38
+ });
39
+ const userIdCode = new NodeCache({
40
+ stdTTL: 60 * 20, // 20 minutes
41
+ });
42
+ export const getStudentSessionInfo = async (code, db, userId) => {
43
+ if (!code || !db)
44
+ return null;
45
+ const application = await getApplication(db);
46
+ if ((application?.demoCodes || []).includes(code)) {
47
+ const allAssessments = await getAssessments(db);
48
+ return {
49
+ appId: db.appId,
50
+ code,
51
+ sessions: allAssessments.map((a) => {
52
+ return {
53
+ assessmentId: a.assessmentId,
54
+ assessmentName: a.name,
55
+ packageId: a.packageId,
56
+ sessionState: "not_started",
57
+ };
58
+ }),
59
+ assessment: allAssessments.length ? allAssessments[0] : null,
60
+ state: "not_started",
61
+ isDemo: true,
62
+ teacherId: "demo-teacher",
63
+ };
64
+ }
65
+ const firestore = getFirestore();
66
+ const doc = await firestore.doc(db.STUDENT.DOC(code)).get();
67
+ if (userId && doc.exists) {
68
+ const firestore = getFirestore();
69
+ // store the mapping between the user and the code so on refresh we can get the code by the user
70
+ await firestore.doc(db.STUDENT_USER_ID_MAPPING.DOC(userId)).set({ code });
71
+ }
72
+ return doc.exists ? doc.data() : null;
73
+ };
74
+ export const getStudentGroupSessionInfo = async (groupCode, db, userId, studentIdentification) => {
75
+ if (!groupCode || !db || !userId)
76
+ return null;
77
+ const firestore = getFirestore();
78
+ // Check if the group code exists in the assessment codes collection
79
+ const groupDeliveryDoc = await firestore
80
+ .doc(db.GROUP_DELIVERY.DOC(groupCode))
81
+ .get();
82
+ if (!groupDeliveryDoc.exists) {
83
+ return null;
84
+ }
85
+ const assessment = groupDeliveryDoc.data();
86
+ const fullAssessment = await getAssessmentTest(db, assessment.assessmentId);
87
+ // Create the mapping between user and code
88
+ await firestore.doc(db.STUDENT_USER_ID_MAPPING.DOC(userId)).set({
89
+ code: groupCode,
90
+ });
91
+ // Create/update student info entry
92
+ const studentDocRef = firestore.doc(db.STUDENT.DOC(userId)); // Assuming nested structure
93
+ const studentInfo = {
94
+ appId: db.appId || "unknown",
95
+ code: groupCode,
96
+ currentAssessmentId: "",
97
+ identification: studentIdentification || "",
98
+ sessions: [],
99
+ isDemo: false,
100
+ teacherId: fullAssessment.teacherId || "unknown_teacher",
101
+ userId: userId,
102
+ };
103
+ // Get assessment details
104
+ const assessments = await getStartableAssessments(db);
105
+ if (assessments && assessments.length > 0) {
106
+ if (!studentInfo.currentAssessmentId) {
107
+ studentInfo.currentAssessmentId = assessment.assessmentId;
108
+ }
109
+ studentInfo.sessions = [
110
+ {
111
+ assessmentId: assessment.assessmentId,
112
+ assessmentName: assessment.name,
113
+ packageId: assessment.packageId,
114
+ sessionState: "not_started",
115
+ },
116
+ ];
117
+ }
118
+ await studentDocRef.set(studentInfo);
119
+ return studentInfo;
120
+ };
121
+ export const updateStudentSessionState = async (db, code, assessmentId, state, batch) => {
122
+ if (!code || !db)
123
+ return null;
124
+ const firestore = getFirestore();
125
+ const studentDocRef = firestore.doc(db.STUDENT.DOC(code));
126
+ const student = (await studentDocRef.get())?.data();
127
+ if (student) {
128
+ const updatedStudent = {
129
+ ...student,
130
+ sessions: student.sessions.map((s) => s.assessmentId === assessmentId ? { ...s, sessionState: state } : s),
131
+ };
132
+ batch.set(studentDocRef, updatedStudent);
133
+ }
134
+ };
135
+ export const createAssessmentCode = async (db, charCount, assessmentId, application) => {
136
+ let code = createCode(charCount);
137
+ let exists = !!(await codeExists(code, db));
138
+ while (exists) {
139
+ code = createCode(charCount);
140
+ exists =
141
+ (application?.demoCodes || []).includes(code) ||
142
+ !!(await codeExists(code, db));
143
+ }
144
+ const firestore = getFirestore();
145
+ const assessmentData = await firestore
146
+ .doc(db.ASSESSMENT.DOC(assessmentId))
147
+ .get();
148
+ if (assessmentData.exists) {
149
+ const assessmentInfo = assessmentData.data();
150
+ await firestore.doc(db.GROUP_DELIVERY.DOC(code)).set(assessmentInfo);
151
+ return code;
152
+ }
153
+ else {
154
+ return null;
155
+ }
156
+ };
157
+ export const createDemoCodeForAssessment = async (db, assessmentId, code) => {
158
+ const exists = !!(await codeExists(code, db));
159
+ if (exists) {
160
+ return;
161
+ }
162
+ const firestore = getFirestore();
163
+ const assessmentData = await firestore
164
+ .doc(db.ASSESSMENT.DOC(assessmentId))
165
+ .get();
166
+ const assessmentInfo = assessmentData.data();
167
+ await firestore
168
+ .doc(db.GROUP_DELIVERY.DOC(code))
169
+ .set({ ...assessmentInfo, isDemo: true });
170
+ return code;
171
+ };
172
+ export const checkAssessmentCode = async (db, code) => {
173
+ const firestore = getFirestore();
174
+ if (!code || !db)
175
+ return null;
176
+ const application = await getApplication(db);
177
+ if ((application?.demoCodes || []).includes(code)) {
178
+ return {};
179
+ }
180
+ const assessmentInfo = await firestore.doc(db.GROUP_DELIVERY.DOC(code));
181
+ const doc = await assessmentInfo.get();
182
+ const assessment = doc.data();
183
+ return assessment;
184
+ };
185
+ export const isStartable = (asessment) => {
186
+ if (asessment.startFrom) {
187
+ const startFrom = new Date(asessment.startFrom);
188
+ if (startFrom > new Date()) {
189
+ return false;
190
+ }
191
+ }
192
+ if (asessment.endAt) {
193
+ const startUntil = new Date(asessment.endAt);
194
+ if (startUntil < new Date()) {
195
+ return false;
196
+ }
197
+ }
198
+ return true;
199
+ };
200
+ export const getCodeByUserId = async (db, userId) => {
201
+ const key = `${db.appId}-${userId}`;
202
+ const cached = userIdCode.get(key);
203
+ if (cached) {
204
+ return cached;
205
+ }
206
+ const firestore = getFirestore();
207
+ const doc = await firestore.doc(db.STUDENT_USER_ID_MAPPING.DOC(userId)).get();
208
+ const code = doc.exists ? doc.data().code : null;
209
+ if (code) {
210
+ userIdCode.set(key, code);
211
+ }
212
+ return code;
213
+ };
214
+ export const getTeacherIdByCode = async (db, code) => {
215
+ const key = `${db.appId}-${code}`;
216
+ const cached = userIdCode.get(key);
217
+ if (cached) {
218
+ return cached;
219
+ }
220
+ const firestore = getFirestore();
221
+ const doc = await firestore.doc(db.STUDENT.DOC(code)).get();
222
+ const teacherId = doc.exists
223
+ ? doc.data().teacherId
224
+ : null;
225
+ if (teacherId) {
226
+ userIdCode.set(key, teacherId);
227
+ }
228
+ return teacherId;
229
+ };
230
+ export const getApplicationInfo = async (db) => {
231
+ const cached = applicationInfoCache.get(db.appId || "application");
232
+ if (cached) {
233
+ return cached;
234
+ }
235
+ const firestore = getFirestore();
236
+ const doc = await firestore.doc(db.APPLICATION.DOC()).get();
237
+ if (doc.exists) {
238
+ const applicationInfo = doc.data();
239
+ applicationInfoCache.set(db.appId || "application", applicationInfo);
240
+ return applicationInfo;
241
+ }
242
+ };
243
+ export const addApplicationInfo = async (db, appInfo) => {
244
+ const firestore = getFirestore();
245
+ const docRef = firestore.doc(db.APPLICATION.DOC());
246
+ await docRef.set(appInfo);
247
+ };
248
+ export const getApplication = async (db) => {
249
+ const firestore = getFirestore();
250
+ const docRef = firestore.doc(db.APPLICATION.DOC());
251
+ const applicationRef = await docRef.get();
252
+ const application = applicationRef.exists
253
+ ? applicationRef.data()
254
+ : {
255
+ name: db.appId,
256
+ };
257
+ if (application &&
258
+ (!application.assessments || !application.assessments.length)) {
259
+ const assessments = await getAssessments(db);
260
+ if (!applicationRef.exists && assessments.length === 0) {
261
+ return null;
262
+ }
263
+ application.assessments = assessments;
264
+ }
265
+ return application;
266
+ };
267
+ export const codeExists = async (code, db) => {
268
+ const studentSessions = await getStudentSessionInfo(code, db, undefined);
269
+ return studentSessions;
270
+ };
271
+ export const hasAccess = async (uid, appId) => {
272
+ const firestore = getFirestore();
273
+ const docRef = firestore.doc(new DATABASE().AUTH.CHECK_APP_RIGHTS(uid, appId.toLowerCase()));
274
+ const doc = await docRef.get();
275
+ return doc.exists;
276
+ };
277
+ // TODO: sessionState should be default on not_started. For snelkookpan specific move to specific logig
278
+ export const createSession = async (teacherId, db, count, assessments) => {
279
+ const firestore = getFirestore();
280
+ const application = await getApplication(db);
281
+ const codes = new Array(count).fill(createCode(5));
282
+ const studentAppSessionInfos = [];
283
+ const batch = firestore.batch();
284
+ for (let code of codes) {
285
+ // check if code does not exists twice in codes array
286
+ const isGeneratedTwice = codes.filter((c) => c === code).length > 1;
287
+ let exists = (await codeExists(code, db)) || isGeneratedTwice;
288
+ while (exists) {
289
+ code = createCode(5);
290
+ exists = !!((application?.demoCodes || []).includes(code) ||
291
+ (await codeExists(code, db)));
292
+ }
293
+ const studentSessionInfo = {
294
+ teacherId,
295
+ code,
296
+ appId: db.appId || "application",
297
+ sessions: assessments.map((assessment) => ({
298
+ assessmentId: assessment.assessmentId,
299
+ assessmentName: assessment.name,
300
+ packageId: assessment.packageId,
301
+ sessionState: assessment.assessmentId.startsWith("practice")
302
+ ? "not_generated"
303
+ : "not_available",
304
+ })),
305
+ isDemo: false,
306
+ };
307
+ studentAppSessionInfos.push(studentSessionInfo);
308
+ const teacherResult = {
309
+ code,
310
+ sessions: studentSessionInfo.sessions.map((session) => ({
311
+ code,
312
+ assessmentId: session.assessmentId,
313
+ sessionState: session.sessionState,
314
+ testScore: 0,
315
+ })),
316
+ };
317
+ batch.set(firestore.doc(db.STUDENT.DOC(code)), studentSessionInfo);
318
+ // now add them to the teacher view model
319
+ const teacherDocPath = db.TEACHER.STUDENTS.DOC(teacherId, teacherResult.code);
320
+ batch.set(firestore.doc(teacherDocPath), teacherResult);
321
+ }
322
+ await batch.commit();
323
+ return studentAppSessionInfos;
324
+ };
325
+ export const createSessionByIdentifications = async (teacherId, db, identifications, sessionState, assessments) => {
326
+ const firestore = getFirestore();
327
+ const studentAppSessionInfos = [];
328
+ const batch = firestore.batch();
329
+ for (const identification of identifications) {
330
+ const exists = await codeExists(identification.identifier, db);
331
+ if (!exists) {
332
+ const studentSessionInfo = {
333
+ teacherId,
334
+ code: identification.identifier,
335
+ appId: db.appId || "application",
336
+ sessions: assessments.map((assessment) => ({
337
+ assessmentId: assessment.assessmentId,
338
+ assessmentName: assessment.name,
339
+ packageId: assessment.packageId,
340
+ sessionState,
341
+ })),
342
+ isDemo: false,
343
+ };
344
+ if (identification.password) {
345
+ studentSessionInfo.password = identification.password;
346
+ }
347
+ studentAppSessionInfos.push(studentSessionInfo);
348
+ const teacherResult = {
349
+ code: identification.identifier,
350
+ sessions: studentSessionInfo.sessions.map((session) => ({
351
+ code: identification.identifier,
352
+ assessmentId: session.assessmentId,
353
+ sessionState: session.sessionState,
354
+ testScore: 0,
355
+ })),
356
+ };
357
+ batch.set(firestore.doc(db.STUDENT.DOC(identification.identifier)), studentSessionInfo);
358
+ // now add them to the teacher view model
359
+ const teacherDocPath = db.TEACHER.STUDENTS.DOC(teacherId, teacherResult.code);
360
+ batch.set(firestore.doc(teacherDocPath), teacherResult);
361
+ }
362
+ }
363
+ await batch.commit();
364
+ return studentAppSessionInfos;
365
+ };
366
+ export const getAllStudents = async (db) => {
367
+ const firestore = getFirestore();
368
+ const docs = await firestore
369
+ .collection(db.STUDENT.COLLECTION())
370
+ .listDocuments();
371
+ const studentInfos = [];
372
+ for (const doc of docs) {
373
+ const studentInfo = await doc.get();
374
+ studentInfos.push(studentInfo.data() ||
375
+ { code: doc.id });
376
+ }
377
+ return studentInfos;
378
+ };
379
+ export const getAssessments = async (db) => {
380
+ const firestore = getFirestore();
381
+ const assessmentData = await firestore
382
+ .collection(db.ASSESSMENT.COLLECTION())
383
+ .get();
384
+ const assessments = assessmentData.docs.map((doc) => doc.data());
385
+ return assessments;
386
+ };
387
+ export const getStartableAssessments = async (db) => {
388
+ const application = await getApplication(db);
389
+ if (!application) {
390
+ return [];
391
+ }
392
+ const startableAssessments = application.assessments.filter((a) => {
393
+ if (a.startFrom) {
394
+ const startFrom = new Date(a.startFrom);
395
+ if (startFrom > new Date()) {
396
+ return false;
397
+ }
398
+ }
399
+ if (a.endAt) {
400
+ const startUntil = new Date(a.endAt);
401
+ if (startUntil < new Date()) {
402
+ return false;
403
+ }
404
+ }
405
+ return a.canStart !== false;
406
+ });
407
+ return startableAssessments;
408
+ };
409
+ export const getAssessmentTest = async (db, assessmentId) => {
410
+ const firestore = getFirestore();
411
+ const assessmentDoc = await firestore
412
+ .doc(db.ASSESSMENT.DOC(assessmentId))
413
+ .get();
414
+ if (!assessmentDoc.exists) {
415
+ throw new Error(`Assessment with ID ${assessmentId} does not exist.`);
416
+ }
417
+ const assessmentData = assessmentDoc.data();
418
+ if (!assessmentData) {
419
+ throw new Error(`Assessment with ID ${assessmentId} has no package.`);
420
+ }
421
+ return assessmentData;
422
+ };
423
+ export const getAssessmentItemRefsFromPackage = async ($assessmentTest) => {
424
+ //let regularItemCount = 0;
425
+ let itemCount = 0;
426
+ const assessmentItemRefs = $assessmentTest("qti-assessment-item-ref")
427
+ .map((i, el) => {
428
+ const isInfo = $assessmentTest(el)
429
+ .attr("category")
430
+ ?.includes("dep-informational");
431
+ // if (!isInfo) {
432
+ // regularItemCount++;
433
+ // }
434
+ itemCount++;
435
+ const identifier = $assessmentTest(el).attr("identifier");
436
+ const sequenceNumber = itemCount; // isInfo ? 0 : regularItemCount;
437
+ return { identifier, sequenceNumber, isInfo };
438
+ })
439
+ .toArray();
440
+ return assessmentItemRefs;
441
+ };
442
+ export const getItemsStats = async (db, teacherId, assessmentId, target) => {
443
+ const firestore = getFirestore();
444
+ const assessmentDoc = await getAssessmentTest(db, assessmentId);
445
+ if (!assessmentDoc) {
446
+ throw new Error(`Assessment with ID ${assessmentId} does not exist.`);
447
+ }
448
+ if (!assessmentDoc.items || assessmentDoc.items.length === 0) {
449
+ throw new Error(`Assessment with ID ${assessmentId} has no items.`);
450
+ }
451
+ const itemIdentifiers = assessmentDoc.items?.map((item) => item.identifier) || [];
452
+ const all = await Promise.all(itemIdentifiers.map((id) => firestore
453
+ .collection(target === "teacher"
454
+ ? db.TEACHER.ASSESSMENT_ITEM_STATS.RESPONSES.COLLECTION(teacherId, assessmentId, id)
455
+ : db.REVIEWER.ASSESSMENT_ITEM_STATS.RESPONSES.COLLECTION(teacherId, assessmentId, id))
456
+ .get()
457
+ .then((snapshot) => ({ itemIdentifier: id, docs: snapshot.docs }))));
458
+ const stats = [];
459
+ for (const { itemIdentifier, docs } of all) {
460
+ if (!docs?.length) {
461
+ // skip empty collections
462
+ continue;
463
+ }
464
+ for (const d of docs) {
465
+ if (!d.exists) {
466
+ console.warn(`Item statistics for item ${itemIdentifier} in assessment ${assessmentId} does not exist.`);
467
+ }
468
+ else {
469
+ const data = d.data();
470
+ if (data) {
471
+ let itemStats = stats.find((s) => s.itemId === itemIdentifier);
472
+ if (!itemStats) {
473
+ itemStats = {
474
+ itemId: itemIdentifier,
475
+ count: 0,
476
+ responses: [],
477
+ numberCorrect: 0,
478
+ };
479
+ stats.push(itemStats);
480
+ }
481
+ itemStats.count += data.count;
482
+ itemStats.numberCorrect +=
483
+ !data.score || data.score?.toString() === "0" ? 0 : 1;
484
+ itemStats.responses.push(data);
485
+ }
486
+ }
487
+ }
488
+ }
489
+ return stats;
490
+ };
491
+ export const updateItemStatResponseScore = async (db, teacherId, assessmentId, itemId, responseId, scoreExternal, target) => {
492
+ const firestore = getFirestore();
493
+ await firestore
494
+ .doc(target === "teacher"
495
+ ? db.TEACHER.ASSESSMENT_ITEM_STATS.RESPONSES.DOC(teacherId, assessmentId, itemId, responseId)
496
+ : db.REVIEWER.ASSESSMENT_ITEM_STATS.RESPONSES.DOC(teacherId, assessmentId, itemId, responseId))
497
+ .update({ scoreExternal });
498
+ };
499
+ export const getPlannedStudents = async (db, teacherId) => {
500
+ const firestore = getFirestore();
501
+ const studentResultCollectionRef = await firestore
502
+ .collection(db.TEACHER.STUDENTS.COLLECTION(teacherId))
503
+ .get();
504
+ // StudentResult.
505
+ const studentSessionInfo = studentResultCollectionRef.docs.map((doc) => doc.data());
506
+ const studentsToReturn = [];
507
+ let prevSessionState = "";
508
+ for (const student of studentSessionInfo) {
509
+ const sessionsToReturn = [];
510
+ for (const session of student.sessions) {
511
+ if (session.sessionState === "not_generated" &&
512
+ session.assessmentId === "practice" &&
513
+ prevSessionState === "scored") {
514
+ session.sessionState = "not_available";
515
+ }
516
+ else if (session.assessmentId === "practice-2" &&
517
+ (session.sessionState === "not_generated" ||
518
+ session.sessionState === "not_available") &&
519
+ prevSessionState === "scored") {
520
+ session.sessionState = "not_started";
521
+ }
522
+ prevSessionState = session.sessionState;
523
+ sessionsToReturn.push(session);
524
+ }
525
+ const updatedStudent = { ...student, sessions: sessionsToReturn };
526
+ studentsToReturn.push(updatedStudent);
527
+ }
528
+ return studentsToReturn;
529
+ };
530
+ export const getPlannedStudent = async (db, teacherId, code) => {
531
+ const firestore = getFirestore();
532
+ const studentResultRef = await firestore
533
+ .doc(db.TEACHER.STUDENTS.DOC(teacherId, code))
534
+ .get();
535
+ return studentResultRef.data();
536
+ };
537
+ export const deleteStudent = async (db, teacherId, code) => {
538
+ const firestore = getFirestore();
539
+ const studentRef = await firestore.doc(db.TEACHER.STUDENTS.DOC(teacherId, code));
540
+ const assessments = await firestore
541
+ .collection(db.ASSESSMENT.COLLECTION())
542
+ .get();
543
+ for (const assessment of assessments.docs) {
544
+ const assessmentResultRef = await firestore.doc(db.TEACHER.ASSESSMENT_RESULTS.DOC(teacherId, assessment.id, code));
545
+ await assessmentResultRef.delete();
546
+ const assessmentReportRef = await firestore.doc(db.TEACHER.ASSESSMENT_REPORTS.DOC(teacherId, assessment.id, code));
547
+ await assessmentReportRef.delete();
548
+ }
549
+ await studentRef.delete();
550
+ };
551
+ export const getUser = async (uid) => {
552
+ const firestore = getFirestore();
553
+ const userFromDatabase = (await firestore.doc(new DATABASE().AUTH.DOC(uid)).get()).data();
554
+ return userFromDatabase;
555
+ };
556
+ export const setUser = async (uid, user) => {
557
+ const firestore = getFirestore();
558
+ await firestore.doc(new DATABASE().AUTH.DOC(uid)).set(user);
559
+ };
560
+ export const resetSession = async (db, teacherId, code, assessmentId) => {
561
+ const firestore = getFirestore();
562
+ const studentRef = await firestore
563
+ .doc(db.TEACHER.STUDENTS.DOC(teacherId, code))
564
+ .get();
565
+ const student = studentRef.data();
566
+ const updatedStudent = {
567
+ ...student,
568
+ sessions: student.sessions.map((s) => s.assessmentId === assessmentId
569
+ ? { ...s, sessionState: "not_started" }
570
+ : s),
571
+ };
572
+ await studentRef.ref.set(updatedStudent);
573
+ };
574
+ export const getStudentInfoForTeacher = async (db, teacherId, code) => {
575
+ const firestore = getFirestore();
576
+ const studentRef = await firestore
577
+ .doc(db.TEACHER.STUDENTS.DOC(teacherId, code))
578
+ .get();
579
+ const student = studentRef.data();
580
+ return student;
581
+ };
582
+ export const updateStudent = async (db, teacherId, code, identification) => {
583
+ const firestore = getFirestore();
584
+ const studentRef = await firestore
585
+ .doc(db.TEACHER.STUDENTS.DOC(teacherId, code))
586
+ .get();
587
+ const student = studentRef.data();
588
+ const updatedStudent = {
589
+ ...student,
590
+ identification,
591
+ };
592
+ await studentRef.ref.set(updatedStudent);
593
+ };
594
+ export const getAllApplications = async () => {
595
+ const firestore = getFirestore();
596
+ const applicationCollection = await firestore
597
+ .collection(new DATABASE().APPLICATION.COLLECTION)
598
+ .get();
599
+ const applications = applicationCollection.docs.map((doc) => {
600
+ const app = doc.data();
601
+ if (!app.name) {
602
+ app.name = doc.id;
603
+ }
604
+ return app;
605
+ });
606
+ return applications;
607
+ };
608
+ const createStrongPassword = () => {
609
+ const password = Math.random().toString(36).slice(-8);
610
+ return password;
611
+ };
612
+ export const createUserForApplication = async (email, applicationId) => {
613
+ const auth = getAuth();
614
+ const firestore = getFirestore();
615
+ let password = "";
616
+ let firebaseUser = undefined;
617
+ try {
618
+ firebaseUser = await auth.getUserByEmail(email);
619
+ }
620
+ catch {
621
+ // if the user is not found, then it raises an error.
622
+ }
623
+ if (!firebaseUser) {
624
+ password = createStrongPassword();
625
+ firebaseUser = await auth.createUser({
626
+ email,
627
+ password,
628
+ });
629
+ }
630
+ await firestore
631
+ .doc(new DATABASE().AUTH.CHECK_APP_RIGHTS(firebaseUser.uid, applicationId))
632
+ .set({
633
+ email,
634
+ application: applicationId.toLocaleLowerCase(),
635
+ userId: firebaseUser.uid,
636
+ });
637
+ if (password) {
638
+ return { password, id: firebaseUser.uid };
639
+ }
640
+ else {
641
+ return { id: firebaseUser.uid };
642
+ }
643
+ };
644
+ export const deleteUserForApplication = async (id, applicationId) => {
645
+ const auth = getAuth();
646
+ const firestore = getFirestore();
647
+ // check application right. If the user has rights to just this application then the user can be deleted.
648
+ // otherwise we remove the user from firebase Authentication
649
+ const applicationDocs = await firestore
650
+ .collection(new DATABASE().AUTH.APP_COLLECTION(id))
651
+ .listDocuments();
652
+ const applicationIds = applicationDocs.map((a) => a.id.toLocaleLowerCase());
653
+ if (!applicationIds.includes(applicationId.toLocaleLowerCase())) {
654
+ return;
655
+ }
656
+ await firestore
657
+ .doc(new DATABASE().AUTH.CHECK_APP_RIGHTS(id, applicationId))
658
+ .delete();
659
+ if (applicationIds.length === 1) {
660
+ await auth.deleteUser(id);
661
+ }
662
+ return;
663
+ };
664
+ export const getAllApplicationsWithUsers = async () => {
665
+ const firestore = getFirestore();
666
+ const applications = await getAllApplications();
667
+ const userDocs = await firestore
668
+ .collection(new DATABASE().AUTH.COLLECTION)
669
+ .listDocuments();
670
+ const usersPerApplication = new Map();
671
+ const allUserIds = userDocs.map((u) => u.id);
672
+ const users = await fetchUsersInChunks(allUserIds);
673
+ for (const userId of allUserIds) {
674
+ const applicationsForUser = await firestore
675
+ .collection(new DATABASE().AUTH.APP_COLLECTION(userId))
676
+ .listDocuments();
677
+ for (const applicationId of applicationsForUser.map((a) => a.id.toLowerCase())) {
678
+ const userData = users.find((u) => u.id === userId);
679
+ const user = {
680
+ id: userId,
681
+ email: userData?.email || "",
682
+ disabled: userData?.disabled || false,
683
+ };
684
+ if (usersPerApplication.has(applicationId)) {
685
+ usersPerApplication.get(applicationId)?.push(user);
686
+ }
687
+ else {
688
+ usersPerApplication.set(applicationId, [user]);
689
+ }
690
+ }
691
+ }
692
+ const applicationWithUsers = applications.map((a) => {
693
+ return {
694
+ ...a,
695
+ users: usersPerApplication.get(a.name?.toLocaleLowerCase()) || [],
696
+ };
697
+ });
698
+ return applicationWithUsers;
699
+ };
700
+ export async function deleteDocumentAndSubcollections(docRef) {
701
+ // Delete the document
702
+ await docRef.delete();
703
+ // Get all the subcollections of the document
704
+ const collections = await docRef.listCollections();
705
+ // Delete each subcollection recursively
706
+ for (const collection of collections) {
707
+ const querySnapshot = await collection.listDocuments();
708
+ querySnapshot.forEach(async (doc) => {
709
+ // Delete the subdocument and its subcollections recursively
710
+ await deleteDocumentAndSubcollections(doc);
711
+ });
712
+ }
713
+ }
714
+ export async function checkAndDeletePackages(dateTime) {
715
+ const firestore = getFirestore();
716
+ const storage = getStorage();
717
+ const db = new DATABASE();
718
+ const packageData = await firestore.collection(db.PACKAGE.COLLECTION()).get();
719
+ const packages = packageData.docs.map((doc) => doc.data());
720
+ for (const packageInfo of packages) {
721
+ if (packageInfo.uploaded < dateTime) {
722
+ for (const { assessmentId } of packageInfo.assessmentInfo) {
723
+ await deleteDocumentAndSubcollections(firestore.doc(db.ASSESSMENT.DOC(assessmentId)));
724
+ }
725
+ await deleteFilesRecursive(storage.bucket(), `packages/${packageInfo.packageId}`);
726
+ await deleteDocumentAndSubcollections(firestore.doc(db.PACKAGE.DOC(packageInfo.packageId)));
727
+ }
728
+ }
729
+ }
730
+ export async function fetchUsersInChunks(ids, chunkSize = 100) {
731
+ let results = [];
732
+ const auth = getAuth();
733
+ for (let i = 0; i < ids.length; i += chunkSize) {
734
+ const chunk = ids.slice(i, i + chunkSize);
735
+ try {
736
+ const chunkResults = await auth.getUsers(chunk.map((c) => ({ uid: c })));
737
+ results = results.concat(chunkResults.users.map((u) => {
738
+ return {
739
+ id: u.uid || "",
740
+ disabled: u.disabled,
741
+ email: u.email || "",
742
+ };
743
+ }));
744
+ }
745
+ catch (error) {
746
+ console.error("Error fetching users:", error);
747
+ // Handle error appropriately - possibly rethrow, or continue to next chunk
748
+ }
749
+ }
750
+ return results;
751
+ }
752
+ export const getPackageIdByAssessment = async (db, code, assessmentId) => {
753
+ const fs = getFirestore();
754
+ const docRef = await fs.doc(db.STUDENT.DOC(code)).get();
755
+ const studentInfo = docRef.data();
756
+ const session = studentInfo.sessions.find((s) => s.assessmentId === assessmentId);
757
+ return session?.packageId || null;
758
+ };
759
+ //# sourceMappingURL=logic.js.map