@vue-skuilder/db 0.1.4 → 0.1.6
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.
- package/CLAUDE.md +43 -0
- package/dist/SyncStrategy-DnJRj-Xp.d.mts +74 -0
- package/dist/SyncStrategy-DnJRj-Xp.d.ts +74 -0
- package/dist/core/index.d.mts +90 -2
- package/dist/core/index.d.ts +90 -2
- package/dist/core/index.js +856 -6155
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +778 -6097
- package/dist/core/index.mjs.map +1 -1
- package/dist/dataLayerProvider-BZmLyBVw.d.mts +41 -0
- package/dist/dataLayerProvider-BuntXkCs.d.ts +41 -0
- package/dist/impl/couch/index.d.mts +292 -0
- package/dist/impl/couch/index.d.ts +292 -0
- package/dist/impl/couch/index.js +3075 -0
- package/dist/impl/couch/index.js.map +1 -0
- package/dist/impl/couch/index.mjs +3007 -0
- package/dist/impl/couch/index.mjs.map +1 -0
- package/dist/impl/static/index.d.mts +188 -0
- package/dist/impl/static/index.d.ts +188 -0
- package/dist/impl/static/index.js +3055 -0
- package/dist/impl/static/index.js.map +1 -0
- package/dist/impl/static/index.mjs +3025 -0
- package/dist/impl/static/index.mjs.map +1 -0
- package/dist/index.d.mts +13 -4
- package/dist/index.d.ts +13 -4
- package/dist/index.js +2920 -6846
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3567 -7513
- package/dist/index.mjs.map +1 -1
- package/dist/types-D6SnlHPm.d.ts +58 -0
- package/dist/types-DPRvCrIk.d.mts +58 -0
- package/dist/types-legacy-WPe8CtO-.d.mts +139 -0
- package/dist/types-legacy-WPe8CtO-.d.ts +139 -0
- package/dist/{index-QMtzQI65.d.mts → userDB-31gsvxyd.d.mts} +11 -252
- package/dist/{index-QMtzQI65.d.ts → userDB-D9EuWTp1.d.ts} +11 -252
- package/dist/util/packer/index.d.mts +65 -0
- package/dist/util/packer/index.d.ts +65 -0
- package/dist/util/packer/index.js +512 -0
- package/dist/util/packer/index.js.map +1 -0
- package/dist/util/packer/index.mjs +485 -0
- package/dist/util/packer/index.mjs.map +1 -0
- package/package.json +12 -2
- package/src/core/interfaces/contentSource.ts +8 -6
- package/src/core/interfaces/courseDB.ts +1 -1
- package/src/core/interfaces/dataLayerProvider.ts +5 -0
- package/src/core/interfaces/userDB.ts +7 -2
- package/src/core/types/types-legacy.ts +2 -0
- package/src/factory.ts +10 -7
- package/src/impl/{pouch/userDB.ts → common/BaseUserDB.ts} +283 -260
- package/src/impl/common/SyncStrategy.ts +90 -0
- package/src/impl/common/index.ts +23 -0
- package/src/impl/common/types.ts +50 -0
- package/src/impl/common/userDBHelpers.ts +144 -0
- package/src/impl/couch/CouchDBSyncStrategy.ts +209 -0
- package/src/impl/{pouch → couch}/PouchDataLayerProvider.ts +16 -7
- package/src/impl/{pouch → couch}/adminDB.ts +3 -3
- package/src/impl/{pouch → couch}/auth.ts +2 -2
- package/src/impl/{pouch → couch}/classroomDB.ts +6 -6
- package/src/impl/{pouch → couch}/courseAPI.ts +59 -21
- package/src/impl/{pouch → couch}/courseDB.ts +32 -17
- package/src/impl/{pouch → couch}/courseLookupDB.ts +1 -1
- package/src/impl/{pouch → couch}/index.ts +27 -20
- package/src/impl/{pouch → couch}/updateQueue.ts +5 -1
- package/src/impl/{pouch → couch}/user-course-relDB.ts +6 -1
- package/src/impl/static/NoOpSyncStrategy.ts +70 -0
- package/src/impl/static/StaticDataLayerProvider.ts +93 -0
- package/src/impl/static/StaticDataUnpacker.ts +549 -0
- package/src/impl/static/courseDB.ts +275 -0
- package/src/impl/static/coursesDB.ts +37 -0
- package/src/impl/static/index.ts +7 -0
- package/src/index.ts +1 -1
- package/src/study/SessionController.ts +4 -4
- package/src/study/SpacedRepetition.ts +3 -3
- package/src/study/getCardDataShape.ts +2 -2
- package/src/util/index.ts +1 -0
- package/src/util/packer/CouchDBToStaticPacker.ts +620 -0
- package/src/util/packer/index.ts +4 -0
- package/src/util/packer/types.ts +64 -0
- package/tsconfig.json +7 -10
- package/tsup.config.ts +5 -3
- /package/src/impl/{pouch → couch}/clientCache.ts +0 -0
- /package/src/impl/{pouch → couch}/pouchdb-setup.ts +0 -0
- /package/src/impl/{pouch → couch}/types.ts +0 -0
|
@@ -0,0 +1,3075 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __glob = (map) => (path) => {
|
|
9
|
+
var fn = map[path];
|
|
10
|
+
if (fn) return fn();
|
|
11
|
+
throw new Error("Module not found in bundle: " + path);
|
|
12
|
+
};
|
|
13
|
+
var __esm = (fn, res) => function __init() {
|
|
14
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
|
+
};
|
|
16
|
+
var __export = (target, all) => {
|
|
17
|
+
for (var name in all)
|
|
18
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
+
};
|
|
20
|
+
var __copyProps = (to, from, except, desc) => {
|
|
21
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
22
|
+
for (let key of __getOwnPropNames(from))
|
|
23
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
24
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
25
|
+
}
|
|
26
|
+
return to;
|
|
27
|
+
};
|
|
28
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
29
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
30
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
31
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
32
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
33
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
34
|
+
mod
|
|
35
|
+
));
|
|
36
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
|
+
|
|
38
|
+
// src/util/logger.ts
|
|
39
|
+
var isDevelopment, logger;
|
|
40
|
+
var init_logger = __esm({
|
|
41
|
+
"src/util/logger.ts"() {
|
|
42
|
+
"use strict";
|
|
43
|
+
isDevelopment = typeof process !== "undefined" && process.env.NODE_ENV === "development";
|
|
44
|
+
logger = {
|
|
45
|
+
/**
|
|
46
|
+
* Debug-level logging - only shown in development
|
|
47
|
+
*/
|
|
48
|
+
debug: (message, ...args) => {
|
|
49
|
+
if (isDevelopment) {
|
|
50
|
+
console.debug(`[DB:DEBUG] ${message}`, ...args);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* Info-level logging - general information
|
|
55
|
+
*/
|
|
56
|
+
info: (message, ...args) => {
|
|
57
|
+
console.info(`[DB:INFO] ${message}`, ...args);
|
|
58
|
+
},
|
|
59
|
+
/**
|
|
60
|
+
* Warning-level logging - potential issues
|
|
61
|
+
*/
|
|
62
|
+
warn: (message, ...args) => {
|
|
63
|
+
console.warn(`[DB:WARN] ${message}`, ...args);
|
|
64
|
+
},
|
|
65
|
+
/**
|
|
66
|
+
* Error-level logging - serious problems
|
|
67
|
+
*/
|
|
68
|
+
error: (message, ...args) => {
|
|
69
|
+
console.error(`[DB:ERROR] ${message}`, ...args);
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* Log function for backward compatibility with existing log() usage
|
|
73
|
+
*/
|
|
74
|
+
log: (message, ...args) => {
|
|
75
|
+
if (isDevelopment) {
|
|
76
|
+
console.log(`[DB:LOG] ${message}`, ...args);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// src/factory.ts
|
|
84
|
+
function getDataLayer() {
|
|
85
|
+
if (!dataLayerInstance) {
|
|
86
|
+
throw new Error("Data layer not initialized. Call initializeDataLayer first.");
|
|
87
|
+
}
|
|
88
|
+
return dataLayerInstance;
|
|
89
|
+
}
|
|
90
|
+
var ENV, dataLayerInstance;
|
|
91
|
+
var init_factory = __esm({
|
|
92
|
+
"src/factory.ts"() {
|
|
93
|
+
"use strict";
|
|
94
|
+
init_logger();
|
|
95
|
+
ENV = {
|
|
96
|
+
COUCHDB_SERVER_PROTOCOL: "NOT_SET",
|
|
97
|
+
COUCHDB_SERVER_URL: "NOT_SET"
|
|
98
|
+
};
|
|
99
|
+
dataLayerInstance = null;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// src/core/types/types-legacy.ts
|
|
104
|
+
var GuestUsername, log, cardHistoryPrefix;
|
|
105
|
+
var init_types_legacy = __esm({
|
|
106
|
+
"src/core/types/types-legacy.ts"() {
|
|
107
|
+
"use strict";
|
|
108
|
+
init_logger();
|
|
109
|
+
GuestUsername = "Guest";
|
|
110
|
+
log = (message) => {
|
|
111
|
+
logger.log(message);
|
|
112
|
+
};
|
|
113
|
+
cardHistoryPrefix = "cardH";
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// src/impl/couch/pouchdb-setup.ts
|
|
118
|
+
var import_pouchdb, import_pouchdb_find, import_pouchdb_authentication, pouchdb_setup_default;
|
|
119
|
+
var init_pouchdb_setup = __esm({
|
|
120
|
+
"src/impl/couch/pouchdb-setup.ts"() {
|
|
121
|
+
"use strict";
|
|
122
|
+
import_pouchdb = __toESM(require("pouchdb"));
|
|
123
|
+
import_pouchdb_find = __toESM(require("pouchdb-find"));
|
|
124
|
+
import_pouchdb_authentication = __toESM(require("@nilock2/pouchdb-authentication"));
|
|
125
|
+
import_pouchdb.default.plugin(import_pouchdb_find.default);
|
|
126
|
+
import_pouchdb.default.plugin(import_pouchdb_authentication.default);
|
|
127
|
+
import_pouchdb.default.defaults({
|
|
128
|
+
ajax: {
|
|
129
|
+
timeout: 6e4
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
pouchdb_setup_default = import_pouchdb.default;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// src/util/Loggable.ts
|
|
137
|
+
var Loggable;
|
|
138
|
+
var init_Loggable = __esm({
|
|
139
|
+
"src/util/Loggable.ts"() {
|
|
140
|
+
"use strict";
|
|
141
|
+
Loggable = class {
|
|
142
|
+
log(...args) {
|
|
143
|
+
console.log(`LOG-${this._className}@${/* @__PURE__ */ new Date()}:`, ...args);
|
|
144
|
+
}
|
|
145
|
+
error(...args) {
|
|
146
|
+
console.error(`ERROR-${this._className}@${/* @__PURE__ */ new Date()}:`, ...args);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// src/impl/couch/updateQueue.ts
|
|
153
|
+
var UpdateQueue;
|
|
154
|
+
var init_updateQueue = __esm({
|
|
155
|
+
"src/impl/couch/updateQueue.ts"() {
|
|
156
|
+
"use strict";
|
|
157
|
+
init_Loggable();
|
|
158
|
+
init_logger();
|
|
159
|
+
UpdateQueue = class extends Loggable {
|
|
160
|
+
_className = "UpdateQueue";
|
|
161
|
+
pendingUpdates = {};
|
|
162
|
+
inprogressUpdates = {};
|
|
163
|
+
db;
|
|
164
|
+
update(id, update) {
|
|
165
|
+
logger.debug(`Update requested on doc: ${id}`);
|
|
166
|
+
if (this.pendingUpdates[id]) {
|
|
167
|
+
this.pendingUpdates[id].push(update);
|
|
168
|
+
} else {
|
|
169
|
+
this.pendingUpdates[id] = [update];
|
|
170
|
+
}
|
|
171
|
+
return this.applyUpdates(id);
|
|
172
|
+
}
|
|
173
|
+
constructor(db) {
|
|
174
|
+
super();
|
|
175
|
+
this.db = db;
|
|
176
|
+
logger.debug(`UpdateQ initialized...`);
|
|
177
|
+
void this.db.info().then((i) => {
|
|
178
|
+
logger.debug(`db info: ${JSON.stringify(i)}`);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async applyUpdates(id) {
|
|
182
|
+
logger.debug(`Applying updates on doc: ${id}`);
|
|
183
|
+
if (this.inprogressUpdates[id]) {
|
|
184
|
+
await this.db.info();
|
|
185
|
+
return this.applyUpdates(id);
|
|
186
|
+
} else {
|
|
187
|
+
if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
|
|
188
|
+
this.inprogressUpdates[id] = true;
|
|
189
|
+
try {
|
|
190
|
+
let doc = await this.db.get(id);
|
|
191
|
+
logger.debug(`Retrieved doc: ${id}`);
|
|
192
|
+
while (this.pendingUpdates[id].length !== 0) {
|
|
193
|
+
const update = this.pendingUpdates[id].splice(0, 1)[0];
|
|
194
|
+
if (typeof update === "function") {
|
|
195
|
+
doc = { ...doc, ...update(doc) };
|
|
196
|
+
} else {
|
|
197
|
+
doc = {
|
|
198
|
+
...doc,
|
|
199
|
+
...update
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
await this.db.put(doc);
|
|
204
|
+
logger.debug(`Put doc: ${id}`);
|
|
205
|
+
if (this.pendingUpdates[id].length === 0) {
|
|
206
|
+
this.inprogressUpdates[id] = false;
|
|
207
|
+
delete this.inprogressUpdates[id];
|
|
208
|
+
} else {
|
|
209
|
+
return this.applyUpdates(id);
|
|
210
|
+
}
|
|
211
|
+
return doc;
|
|
212
|
+
} catch (e) {
|
|
213
|
+
delete this.inprogressUpdates[id];
|
|
214
|
+
if (this.pendingUpdates[id]) {
|
|
215
|
+
delete this.pendingUpdates[id];
|
|
216
|
+
}
|
|
217
|
+
logger.error(`Error on attemped update: ${JSON.stringify(e)}`);
|
|
218
|
+
throw e;
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
throw new Error(`Empty Updates Queue Triggered`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// src/impl/couch/clientCache.ts
|
|
230
|
+
async function GET_CACHED(k, f) {
|
|
231
|
+
if (CLIENT_CACHE[k]) {
|
|
232
|
+
return CLIENT_CACHE[k];
|
|
233
|
+
}
|
|
234
|
+
CLIENT_CACHE[k] = f ? await f(k) : await GET_ITEM(k);
|
|
235
|
+
return GET_CACHED(k);
|
|
236
|
+
}
|
|
237
|
+
async function GET_ITEM(k) {
|
|
238
|
+
throw new Error(`No implementation found for GET_CACHED(${k})`);
|
|
239
|
+
}
|
|
240
|
+
var CLIENT_CACHE;
|
|
241
|
+
var init_clientCache = __esm({
|
|
242
|
+
"src/impl/couch/clientCache.ts"() {
|
|
243
|
+
"use strict";
|
|
244
|
+
CLIENT_CACHE = {};
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// src/impl/common/SyncStrategy.ts
|
|
249
|
+
var init_SyncStrategy = __esm({
|
|
250
|
+
"src/impl/common/SyncStrategy.ts"() {
|
|
251
|
+
"use strict";
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// src/core/util/index.ts
|
|
256
|
+
function getCardHistoryID(courseID, cardID) {
|
|
257
|
+
return `${cardHistoryPrefix}-${courseID}-${cardID}`;
|
|
258
|
+
}
|
|
259
|
+
var init_util = __esm({
|
|
260
|
+
"src/core/util/index.ts"() {
|
|
261
|
+
"use strict";
|
|
262
|
+
init_types_legacy();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// src/impl/common/userDBHelpers.ts
|
|
267
|
+
function hexEncode(str) {
|
|
268
|
+
let hex;
|
|
269
|
+
let returnStr = "";
|
|
270
|
+
for (let i = 0; i < str.length; i++) {
|
|
271
|
+
hex = str.charCodeAt(i).toString(16);
|
|
272
|
+
returnStr += ("000" + hex).slice(3);
|
|
273
|
+
}
|
|
274
|
+
return returnStr;
|
|
275
|
+
}
|
|
276
|
+
function filterAllDocsByPrefix(db, prefix, opts) {
|
|
277
|
+
const options = {
|
|
278
|
+
startkey: prefix,
|
|
279
|
+
endkey: prefix + "\uFFF0",
|
|
280
|
+
include_docs: true
|
|
281
|
+
};
|
|
282
|
+
if (opts) {
|
|
283
|
+
Object.assign(options, opts);
|
|
284
|
+
}
|
|
285
|
+
return db.allDocs(options);
|
|
286
|
+
}
|
|
287
|
+
function getStartAndEndKeys(key) {
|
|
288
|
+
return {
|
|
289
|
+
startkey: key,
|
|
290
|
+
endkey: key + "\uFFF0"
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function updateGuestAccountExpirationDate(guestDB) {
|
|
294
|
+
const currentTime = import_moment.default.utc();
|
|
295
|
+
const expirationDate = currentTime.add(2, "months").toISOString();
|
|
296
|
+
const expiryDocID2 = "GuestAccountExpirationDate";
|
|
297
|
+
void guestDB.get(expiryDocID2).then((doc) => {
|
|
298
|
+
return guestDB.put({
|
|
299
|
+
_id: expiryDocID2,
|
|
300
|
+
_rev: doc._rev,
|
|
301
|
+
date: expirationDate
|
|
302
|
+
});
|
|
303
|
+
}).catch(() => {
|
|
304
|
+
return guestDB.put({
|
|
305
|
+
_id: expiryDocID2,
|
|
306
|
+
date: expirationDate
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
function getLocalUserDB(username) {
|
|
311
|
+
return new pouchdb_setup_default(`userdb-${username}`, {});
|
|
312
|
+
}
|
|
313
|
+
function scheduleCardReviewLocal(userDB, review) {
|
|
314
|
+
const now = import_moment.default.utc();
|
|
315
|
+
logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
|
|
316
|
+
void userDB.put({
|
|
317
|
+
_id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT),
|
|
318
|
+
cardId: review.card_id,
|
|
319
|
+
reviewTime: review.time,
|
|
320
|
+
courseId: review.course_id,
|
|
321
|
+
scheduledAt: now,
|
|
322
|
+
scheduledFor: review.scheduledFor,
|
|
323
|
+
schedulingAgentId: review.schedulingAgentId
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
|
|
327
|
+
const reviewDoc = await userDB.get(reviewDocID);
|
|
328
|
+
userDB.remove(reviewDoc).then((res) => {
|
|
329
|
+
if (res.ok) {
|
|
330
|
+
log2(`Removed Review Doc: ${reviewDocID}`);
|
|
331
|
+
}
|
|
332
|
+
}).catch((err) => {
|
|
333
|
+
log2(`Failed to remove Review Doc: ${reviewDocID},
|
|
334
|
+
${JSON.stringify(err)}`);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
var import_moment, REVIEW_PREFIX, REVIEW_TIME_FORMAT, log2;
|
|
338
|
+
var init_userDBHelpers = __esm({
|
|
339
|
+
"src/impl/common/userDBHelpers.ts"() {
|
|
340
|
+
"use strict";
|
|
341
|
+
import_moment = __toESM(require("moment"));
|
|
342
|
+
init_logger();
|
|
343
|
+
init_pouchdb_setup();
|
|
344
|
+
REVIEW_PREFIX = "card_review_";
|
|
345
|
+
REVIEW_TIME_FORMAT = "YYYY-MM-DD--kk:mm:ss-SSS";
|
|
346
|
+
log2 = (s) => {
|
|
347
|
+
logger.info(s);
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// src/impl/couch/user-course-relDB.ts
|
|
353
|
+
var import_moment2, UsrCrsData;
|
|
354
|
+
var init_user_course_relDB = __esm({
|
|
355
|
+
"src/impl/couch/user-course-relDB.ts"() {
|
|
356
|
+
"use strict";
|
|
357
|
+
import_moment2 = __toESM(require("moment"));
|
|
358
|
+
init_couch();
|
|
359
|
+
init_courseDB();
|
|
360
|
+
init_logger();
|
|
361
|
+
UsrCrsData = class {
|
|
362
|
+
user;
|
|
363
|
+
course;
|
|
364
|
+
_courseId;
|
|
365
|
+
constructor(user, courseId) {
|
|
366
|
+
this.user = user;
|
|
367
|
+
this.course = new CourseDB(courseId, async () => this.user);
|
|
368
|
+
this._courseId = courseId;
|
|
369
|
+
}
|
|
370
|
+
async getReviewsForcast(daysCount) {
|
|
371
|
+
const time = import_moment2.default.utc().add(daysCount, "days");
|
|
372
|
+
return this.getReviewstoDate(time);
|
|
373
|
+
}
|
|
374
|
+
async getPendingReviews() {
|
|
375
|
+
const now = import_moment2.default.utc();
|
|
376
|
+
return this.getReviewstoDate(now);
|
|
377
|
+
}
|
|
378
|
+
async getScheduledReviewCount() {
|
|
379
|
+
return (await this.getPendingReviews()).length;
|
|
380
|
+
}
|
|
381
|
+
async getCourseSettings() {
|
|
382
|
+
const regDoc = await this.user.getCourseRegistrationsDoc();
|
|
383
|
+
const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
|
|
384
|
+
if (crsDoc && crsDoc.settings) {
|
|
385
|
+
return crsDoc.settings;
|
|
386
|
+
} else {
|
|
387
|
+
logger.warn(`no settings found during lookup on course ${this._courseId}`);
|
|
388
|
+
return {};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
updateCourseSettings(updates) {
|
|
392
|
+
void this.user.updateCourseSettings(this._courseId, updates);
|
|
393
|
+
}
|
|
394
|
+
async getReviewstoDate(targetDate) {
|
|
395
|
+
const keys = getStartAndEndKeys2(REVIEW_PREFIX2);
|
|
396
|
+
const reviews = await this.user.remote().allDocs({
|
|
397
|
+
startkey: keys.startkey,
|
|
398
|
+
endkey: keys.endkey,
|
|
399
|
+
include_docs: true
|
|
400
|
+
});
|
|
401
|
+
logger.debug(
|
|
402
|
+
`Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
|
|
403
|
+
);
|
|
404
|
+
return reviews.rows.filter((r) => {
|
|
405
|
+
if (r.id.startsWith(REVIEW_PREFIX2)) {
|
|
406
|
+
const date = import_moment2.default.utc(r.id.substr(REVIEW_PREFIX2.length), REVIEW_TIME_FORMAT2);
|
|
407
|
+
if (targetDate.isAfter(date)) {
|
|
408
|
+
if (this._courseId === void 0 || r.doc.courseId === this._courseId) {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}).map((r) => r.doc);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// src/impl/common/BaseUserDB.ts
|
|
420
|
+
async function getOrCreateClassroomRegistrationsDoc(user) {
|
|
421
|
+
let ret;
|
|
422
|
+
try {
|
|
423
|
+
ret = await getLocalUserDB(user).get(userClassroomsDoc);
|
|
424
|
+
} catch (e) {
|
|
425
|
+
const err = e;
|
|
426
|
+
if (err.status === 404) {
|
|
427
|
+
await getLocalUserDB(user).put({
|
|
428
|
+
_id: userClassroomsDoc,
|
|
429
|
+
registrations: []
|
|
430
|
+
});
|
|
431
|
+
ret = await getOrCreateClassroomRegistrationsDoc(user);
|
|
432
|
+
} else {
|
|
433
|
+
const errorDetails = {
|
|
434
|
+
name: err.name,
|
|
435
|
+
status: err.status,
|
|
436
|
+
message: err.message,
|
|
437
|
+
reason: err.reason,
|
|
438
|
+
error: err.error
|
|
439
|
+
};
|
|
440
|
+
logger.error(
|
|
441
|
+
"Database error in getOrCreateClassroomRegistrationsDoc (standalone function):",
|
|
442
|
+
errorDetails
|
|
443
|
+
);
|
|
444
|
+
throw new Error(
|
|
445
|
+
`Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return ret;
|
|
450
|
+
}
|
|
451
|
+
async function getOrCreateCourseRegistrationsDoc(user) {
|
|
452
|
+
let ret;
|
|
453
|
+
try {
|
|
454
|
+
ret = await getLocalUserDB(user).get(userCoursesDoc);
|
|
455
|
+
} catch (e) {
|
|
456
|
+
const err = e;
|
|
457
|
+
if (err.status === 404) {
|
|
458
|
+
await getLocalUserDB(user).put({
|
|
459
|
+
_id: userCoursesDoc,
|
|
460
|
+
courses: [],
|
|
461
|
+
studyWeight: {}
|
|
462
|
+
});
|
|
463
|
+
ret = await getOrCreateCourseRegistrationsDoc(user);
|
|
464
|
+
} else {
|
|
465
|
+
throw new Error(
|
|
466
|
+
`Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return ret;
|
|
471
|
+
}
|
|
472
|
+
async function updateUserElo(user, course_id, elo) {
|
|
473
|
+
const regDoc = await getOrCreateCourseRegistrationsDoc(user);
|
|
474
|
+
const course = regDoc.courses.find((c) => c.courseID === course_id);
|
|
475
|
+
course.elo = elo;
|
|
476
|
+
return getLocalUserDB(user).put(regDoc);
|
|
477
|
+
}
|
|
478
|
+
async function registerUserForClassroom(user, classID, registerAs) {
|
|
479
|
+
log3(`Registering user: ${user} in course: ${classID}`);
|
|
480
|
+
return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
|
|
481
|
+
const regItem = {
|
|
482
|
+
classID,
|
|
483
|
+
registeredAs: registerAs
|
|
484
|
+
};
|
|
485
|
+
if (doc.registrations.filter((reg) => {
|
|
486
|
+
return reg.classID === regItem.classID && reg.registeredAs === regItem.registeredAs;
|
|
487
|
+
}).length === 0) {
|
|
488
|
+
doc.registrations.push(regItem);
|
|
489
|
+
} else {
|
|
490
|
+
log3(`User ${user} is already registered for class ${classID}`);
|
|
491
|
+
}
|
|
492
|
+
return getLocalUserDB(user).put(doc);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
async function dropUserFromClassroom(user, classID) {
|
|
496
|
+
return getOrCreateClassroomRegistrationsDoc(user).then((doc) => {
|
|
497
|
+
let index = -1;
|
|
498
|
+
for (let i = 0; i < doc.registrations.length; i++) {
|
|
499
|
+
if (doc.registrations[i].classID === classID) {
|
|
500
|
+
index = i;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (index !== -1) {
|
|
504
|
+
doc.registrations.splice(index, 1);
|
|
505
|
+
}
|
|
506
|
+
return getLocalUserDB(user).put(doc);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
async function getUserClassrooms(user) {
|
|
510
|
+
return getOrCreateClassroomRegistrationsDoc(user);
|
|
511
|
+
}
|
|
512
|
+
var import_common, import_moment3, log3, cardHistoryPrefix2, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
513
|
+
var init_BaseUserDB = __esm({
|
|
514
|
+
"src/impl/common/BaseUserDB.ts"() {
|
|
515
|
+
"use strict";
|
|
516
|
+
init_util();
|
|
517
|
+
import_common = require("@vue-skuilder/common");
|
|
518
|
+
import_moment3 = __toESM(require("moment"));
|
|
519
|
+
init_types_legacy();
|
|
520
|
+
init_logger();
|
|
521
|
+
init_userDBHelpers();
|
|
522
|
+
init_updateQueue();
|
|
523
|
+
init_user_course_relDB();
|
|
524
|
+
init_couch();
|
|
525
|
+
log3 = (s) => {
|
|
526
|
+
logger.info(s);
|
|
527
|
+
};
|
|
528
|
+
cardHistoryPrefix2 = "cardH-";
|
|
529
|
+
BaseUser = class _BaseUser {
|
|
530
|
+
static _instance;
|
|
531
|
+
static _initialized = false;
|
|
532
|
+
static Dummy(syncStrategy) {
|
|
533
|
+
return new _BaseUser("Me", syncStrategy);
|
|
534
|
+
}
|
|
535
|
+
static DOC_IDS = {
|
|
536
|
+
CONFIG: "CONFIG",
|
|
537
|
+
COURSE_REGISTRATIONS: "CourseRegistrations",
|
|
538
|
+
CLASSROOM_REGISTRATIONS: "ClassroomRegistrations"
|
|
539
|
+
};
|
|
540
|
+
// private email: string;
|
|
541
|
+
_username;
|
|
542
|
+
syncStrategy;
|
|
543
|
+
getUsername() {
|
|
544
|
+
return this._username;
|
|
545
|
+
}
|
|
546
|
+
isLoggedIn() {
|
|
547
|
+
return !this._username.startsWith(GuestUsername);
|
|
548
|
+
}
|
|
549
|
+
remoteDB;
|
|
550
|
+
remote() {
|
|
551
|
+
return this.remoteDB;
|
|
552
|
+
}
|
|
553
|
+
localDB;
|
|
554
|
+
updateQueue;
|
|
555
|
+
async createAccount(username, password) {
|
|
556
|
+
if (!this.syncStrategy.canCreateAccount()) {
|
|
557
|
+
throw new Error("Account creation not supported by current sync strategy");
|
|
558
|
+
}
|
|
559
|
+
if (!this._username.startsWith(GuestUsername)) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
`Cannot create a new account while logged in:
|
|
562
|
+
Currently logged-in as ${this._username}.`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
const result = await this.syncStrategy.createAccount(username, password);
|
|
566
|
+
if (result.status === import_common.Status.ok) {
|
|
567
|
+
log3(`Account created successfully, updating username to ${username}`);
|
|
568
|
+
this._username = username;
|
|
569
|
+
localStorage.removeItem("dbUUID");
|
|
570
|
+
await this.init();
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
status: result.status,
|
|
574
|
+
error: result.error || ""
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
async login(username, password) {
|
|
578
|
+
if (!this.syncStrategy.canAuthenticate()) {
|
|
579
|
+
throw new Error("Authentication not supported by current sync strategy");
|
|
580
|
+
}
|
|
581
|
+
if (!this._username.startsWith(GuestUsername)) {
|
|
582
|
+
throw new Error(`Cannot change accounts while logged in.
|
|
583
|
+
Log out of account ${this.getUsername()} before logging in as ${username}.`);
|
|
584
|
+
}
|
|
585
|
+
const loginResult = await this.syncStrategy.authenticate(username, password);
|
|
586
|
+
if (loginResult.ok) {
|
|
587
|
+
log3(`Logged in as ${username}`);
|
|
588
|
+
this._username = username;
|
|
589
|
+
localStorage.removeItem("dbUUID");
|
|
590
|
+
await this.init();
|
|
591
|
+
}
|
|
592
|
+
return loginResult;
|
|
593
|
+
}
|
|
594
|
+
async resetUserData() {
|
|
595
|
+
if (this.syncStrategy.canAuthenticate()) {
|
|
596
|
+
return {
|
|
597
|
+
status: import_common.Status.error,
|
|
598
|
+
error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
const localDB = getLocalUserDB(this._username);
|
|
603
|
+
const allDocs = await localDB.allDocs({ include_docs: false });
|
|
604
|
+
const docsToDelete = allDocs.rows.filter((row) => {
|
|
605
|
+
const id = row.id;
|
|
606
|
+
return id.startsWith(cardHistoryPrefix2) || // Card interaction history
|
|
607
|
+
id.startsWith(REVIEW_PREFIX) || // Scheduled reviews
|
|
608
|
+
id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
609
|
+
id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
610
|
+
id === _BaseUser.DOC_IDS.CONFIG;
|
|
611
|
+
}).map((row) => ({ _id: row.id, _rev: row.value.rev, _deleted: true }));
|
|
612
|
+
if (docsToDelete.length > 0) {
|
|
613
|
+
await localDB.bulkDocs(docsToDelete);
|
|
614
|
+
}
|
|
615
|
+
await this.init();
|
|
616
|
+
return { status: import_common.Status.ok };
|
|
617
|
+
} catch (error) {
|
|
618
|
+
logger.error("Failed to reset user data:", error);
|
|
619
|
+
return {
|
|
620
|
+
status: import_common.Status.error,
|
|
621
|
+
error: error instanceof Error ? error.message : "Unknown error during reset"
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
async logout() {
|
|
626
|
+
if (!this.syncStrategy.canAuthenticate()) {
|
|
627
|
+
this._username = await this.syncStrategy.getCurrentUsername();
|
|
628
|
+
await this.init();
|
|
629
|
+
return { ok: true };
|
|
630
|
+
}
|
|
631
|
+
const ret = await this.syncStrategy.logout();
|
|
632
|
+
this._username = await this.syncStrategy.getCurrentUsername();
|
|
633
|
+
await this.init();
|
|
634
|
+
return ret;
|
|
635
|
+
}
|
|
636
|
+
update(id, update) {
|
|
637
|
+
return this.updateQueue.update(id, update);
|
|
638
|
+
}
|
|
639
|
+
async getCourseRegistrationsDoc() {
|
|
640
|
+
logger.debug(`Fetching courseRegistrations for ${this.getUsername()}`);
|
|
641
|
+
let ret;
|
|
642
|
+
try {
|
|
643
|
+
const regDoc = await this.localDB.get(
|
|
644
|
+
_BaseUser.DOC_IDS.COURSE_REGISTRATIONS
|
|
645
|
+
);
|
|
646
|
+
return regDoc;
|
|
647
|
+
} catch (e) {
|
|
648
|
+
const err = e;
|
|
649
|
+
if (err.status === 404) {
|
|
650
|
+
await this.localDB.put({
|
|
651
|
+
_id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
|
|
652
|
+
courses: [],
|
|
653
|
+
studyWeight: {}
|
|
654
|
+
});
|
|
655
|
+
ret = await this.getCourseRegistrationsDoc();
|
|
656
|
+
} else {
|
|
657
|
+
throw new Error(
|
|
658
|
+
`Unexpected error ${JSON.stringify(e)} in getOrCreateCourseRegistrationDoc...`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return ret;
|
|
663
|
+
}
|
|
664
|
+
async getActiveCourses() {
|
|
665
|
+
const reg = await this.getCourseRegistrationsDoc();
|
|
666
|
+
return reg.courses.filter((c) => {
|
|
667
|
+
return c.status === void 0 || c.status === "active";
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Returns a promise of the card IDs that the user has
|
|
672
|
+
* a scheduled review for.
|
|
673
|
+
*
|
|
674
|
+
*/
|
|
675
|
+
async getActiveCards() {
|
|
676
|
+
const keys = getStartAndEndKeys(REVIEW_PREFIX);
|
|
677
|
+
const reviews = await this.remoteDB.allDocs({
|
|
678
|
+
startkey: keys.startkey,
|
|
679
|
+
endkey: keys.endkey,
|
|
680
|
+
include_docs: true
|
|
681
|
+
});
|
|
682
|
+
return reviews.rows.map((r) => `${r.doc.courseId}-${r.doc.cardId}`);
|
|
683
|
+
}
|
|
684
|
+
async getActivityRecords() {
|
|
685
|
+
try {
|
|
686
|
+
const hist = await this.getHistory();
|
|
687
|
+
const allRecords = [];
|
|
688
|
+
if (!Array.isArray(hist)) {
|
|
689
|
+
logger.error("getHistory did not return an array:", hist);
|
|
690
|
+
return allRecords;
|
|
691
|
+
}
|
|
692
|
+
let sampleCount = 0;
|
|
693
|
+
for (let i = 0; i < hist.length; i++) {
|
|
694
|
+
try {
|
|
695
|
+
if (hist[i] && Array.isArray(hist[i].records)) {
|
|
696
|
+
hist[i].records.forEach((record) => {
|
|
697
|
+
try {
|
|
698
|
+
if (!record.timeStamp) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
let timeStamp;
|
|
702
|
+
if (typeof record.timeStamp === "object") {
|
|
703
|
+
if (typeof record.timeStamp.toDate === "function") {
|
|
704
|
+
timeStamp = record.timeStamp.toISOString();
|
|
705
|
+
} else if (record.timeStamp instanceof Date) {
|
|
706
|
+
timeStamp = record.timeStamp.toISOString();
|
|
707
|
+
} else {
|
|
708
|
+
if (sampleCount < 3) {
|
|
709
|
+
logger.warn("Unknown timestamp object type:", record.timeStamp);
|
|
710
|
+
sampleCount++;
|
|
711
|
+
}
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
} else if (typeof record.timeStamp === "string") {
|
|
715
|
+
const date = new Date(record.timeStamp);
|
|
716
|
+
if (isNaN(date.getTime())) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
timeStamp = record.timeStamp;
|
|
720
|
+
} else if (typeof record.timeStamp === "number") {
|
|
721
|
+
timeStamp = new Date(record.timeStamp).toISOString();
|
|
722
|
+
} else {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
allRecords.push({
|
|
726
|
+
timeStamp,
|
|
727
|
+
courseID: record.courseID || "unknown",
|
|
728
|
+
cardID: record.cardID || "unknown",
|
|
729
|
+
timeSpent: record.timeSpent || 0,
|
|
730
|
+
type: "card_view"
|
|
731
|
+
});
|
|
732
|
+
} catch (err) {
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
} catch (err) {
|
|
737
|
+
logger.error("Error processing history item:", err);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
logger.debug(`Found ${allRecords.length} activity records`);
|
|
741
|
+
return allRecords;
|
|
742
|
+
} catch (err) {
|
|
743
|
+
logger.error("Error in getActivityRecords:", err);
|
|
744
|
+
return [];
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async getReviewstoDate(targetDate, course_id) {
|
|
748
|
+
const keys = getStartAndEndKeys(REVIEW_PREFIX);
|
|
749
|
+
const reviews = await this.remoteDB.allDocs({
|
|
750
|
+
startkey: keys.startkey,
|
|
751
|
+
endkey: keys.endkey,
|
|
752
|
+
include_docs: true
|
|
753
|
+
});
|
|
754
|
+
log3(
|
|
755
|
+
`Fetching ${this._username}'s scheduled reviews${course_id ? ` for course ${course_id}` : ""}.`
|
|
756
|
+
);
|
|
757
|
+
return reviews.rows.filter((r) => {
|
|
758
|
+
if (r.id.startsWith(REVIEW_PREFIX)) {
|
|
759
|
+
const date = import_moment3.default.utc(r.id.substr(REVIEW_PREFIX.length), REVIEW_TIME_FORMAT);
|
|
760
|
+
if (targetDate.isAfter(date)) {
|
|
761
|
+
if (course_id === void 0 || r.doc.courseId === course_id) {
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}).map((r) => r.doc);
|
|
767
|
+
}
|
|
768
|
+
async getReviewsForcast(daysCount) {
|
|
769
|
+
const time = import_moment3.default.utc().add(daysCount, "days");
|
|
770
|
+
return this.getReviewstoDate(time);
|
|
771
|
+
}
|
|
772
|
+
async getPendingReviews(course_id) {
|
|
773
|
+
const now = import_moment3.default.utc();
|
|
774
|
+
return this.getReviewstoDate(now, course_id);
|
|
775
|
+
}
|
|
776
|
+
async getScheduledReviewCount(course_id) {
|
|
777
|
+
return (await this.getPendingReviews(course_id)).length;
|
|
778
|
+
}
|
|
779
|
+
async getRegisteredCourses() {
|
|
780
|
+
const regDoc = await this.getCourseRegistrationsDoc();
|
|
781
|
+
return regDoc.courses.filter((c) => {
|
|
782
|
+
return !c.status || c.status === "active" || c.status === "maintenance-mode";
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
async getCourseRegDoc(courseID) {
|
|
786
|
+
const regDocs = await this.getCourseRegistrationsDoc();
|
|
787
|
+
const ret = regDocs.courses.find((c) => c.courseID === courseID);
|
|
788
|
+
if (ret) {
|
|
789
|
+
return ret;
|
|
790
|
+
} else {
|
|
791
|
+
throw new Error(`Course registration not found for course ID: ${courseID}`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
async registerForCourse(course_id, previewMode = false) {
|
|
795
|
+
return this.getCourseRegistrationsDoc().then((doc) => {
|
|
796
|
+
const status = previewMode ? "preview" : "active";
|
|
797
|
+
logger.debug(`Registering for ${course_id} with status: ${status}`);
|
|
798
|
+
const regItem = {
|
|
799
|
+
status,
|
|
800
|
+
courseID: course_id,
|
|
801
|
+
user: true,
|
|
802
|
+
admin: false,
|
|
803
|
+
moderator: false,
|
|
804
|
+
elo: {
|
|
805
|
+
global: {
|
|
806
|
+
score: 1e3,
|
|
807
|
+
count: 0
|
|
808
|
+
},
|
|
809
|
+
tags: {},
|
|
810
|
+
misc: {}
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
if (doc.courses.filter((course) => {
|
|
814
|
+
return course.courseID === regItem.courseID;
|
|
815
|
+
}).length === 0) {
|
|
816
|
+
log3(`It's a new course registration!`);
|
|
817
|
+
doc.courses.push(regItem);
|
|
818
|
+
doc.studyWeight[course_id] = 1;
|
|
819
|
+
} else {
|
|
820
|
+
doc.courses.forEach((c) => {
|
|
821
|
+
log3(`Found the previously registered course!`);
|
|
822
|
+
if (c.courseID === course_id) {
|
|
823
|
+
c.status = status;
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
return this.localDB.put(doc);
|
|
828
|
+
}).catch((e) => {
|
|
829
|
+
log3(`Registration failed because of: ${JSON.stringify(e)}`);
|
|
830
|
+
throw e;
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
async dropCourse(course_id, dropStatus = "dropped") {
|
|
834
|
+
return this.getCourseRegistrationsDoc().then((doc) => {
|
|
835
|
+
let index = -1;
|
|
836
|
+
for (let i = 0; i < doc.courses.length; i++) {
|
|
837
|
+
if (doc.courses[i].courseID === course_id) {
|
|
838
|
+
index = i;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (index !== -1) {
|
|
842
|
+
delete doc.studyWeight[course_id];
|
|
843
|
+
doc.courses[index].status = dropStatus;
|
|
844
|
+
} else {
|
|
845
|
+
throw new Error(
|
|
846
|
+
`User ${this.getUsername()} is not currently registered for course ${course_id}`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
return this.localDB.put(doc);
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
async getCourseInterface(courseId) {
|
|
853
|
+
return new UsrCrsData(this, courseId);
|
|
854
|
+
}
|
|
855
|
+
async getUserEditableCourses() {
|
|
856
|
+
let courseIDs = [];
|
|
857
|
+
const registeredCourses = await this.getCourseRegistrationsDoc();
|
|
858
|
+
courseIDs = courseIDs.concat(
|
|
859
|
+
registeredCourses.courses.map((course) => {
|
|
860
|
+
return course.courseID;
|
|
861
|
+
})
|
|
862
|
+
);
|
|
863
|
+
const cfgs = await Promise.all(
|
|
864
|
+
courseIDs.map(async (id) => {
|
|
865
|
+
return await getCredentialledCourseConfig(id);
|
|
866
|
+
})
|
|
867
|
+
);
|
|
868
|
+
return cfgs;
|
|
869
|
+
}
|
|
870
|
+
async getConfig() {
|
|
871
|
+
const defaultConfig = {
|
|
872
|
+
_id: _BaseUser.DOC_IDS.CONFIG,
|
|
873
|
+
darkMode: false,
|
|
874
|
+
likesConfetti: false
|
|
875
|
+
};
|
|
876
|
+
try {
|
|
877
|
+
const cfg = await this.localDB.get(_BaseUser.DOC_IDS.CONFIG);
|
|
878
|
+
logger.debug("Raw config from DB:", cfg);
|
|
879
|
+
return cfg;
|
|
880
|
+
} catch (e) {
|
|
881
|
+
const err = e;
|
|
882
|
+
if (err.name && err.name === "not_found") {
|
|
883
|
+
await this.localDB.put(defaultConfig);
|
|
884
|
+
return this.getConfig();
|
|
885
|
+
} else {
|
|
886
|
+
logger.error(`Error setting user default config:`, e);
|
|
887
|
+
throw new Error(`Error returning the user's configuration: ${JSON.stringify(e)}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
async setConfig(items) {
|
|
892
|
+
logger.debug(`Setting Config items ${JSON.stringify(items)}`);
|
|
893
|
+
const c = await this.getConfig();
|
|
894
|
+
const put = await this.localDB.put({
|
|
895
|
+
...c,
|
|
896
|
+
...items
|
|
897
|
+
});
|
|
898
|
+
if (put.ok) {
|
|
899
|
+
logger.debug(`Config items set: ${JSON.stringify(items)}`);
|
|
900
|
+
} else {
|
|
901
|
+
logger.error(`Error setting config items: ${JSON.stringify(put)}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
*
|
|
906
|
+
* This function should be called *only* by the pouchdb datalayer provider
|
|
907
|
+
* auth store.
|
|
908
|
+
*
|
|
909
|
+
*
|
|
910
|
+
* Anyone else seeking the current user should use the auth store's
|
|
911
|
+
* exported `getCurrentUser` method.
|
|
912
|
+
*
|
|
913
|
+
*/
|
|
914
|
+
static async instance(syncStrategy, username) {
|
|
915
|
+
if (username) {
|
|
916
|
+
_BaseUser._instance = new _BaseUser(username, syncStrategy);
|
|
917
|
+
await _BaseUser._instance.init();
|
|
918
|
+
return _BaseUser._instance;
|
|
919
|
+
} else if (_BaseUser._instance && _BaseUser._initialized) {
|
|
920
|
+
return _BaseUser._instance;
|
|
921
|
+
} else if (_BaseUser._instance) {
|
|
922
|
+
return new Promise((resolve) => {
|
|
923
|
+
(function waitForUser() {
|
|
924
|
+
if (_BaseUser._initialized) {
|
|
925
|
+
return resolve(_BaseUser._instance);
|
|
926
|
+
} else {
|
|
927
|
+
setTimeout(waitForUser, 50);
|
|
928
|
+
}
|
|
929
|
+
})();
|
|
930
|
+
});
|
|
931
|
+
} else {
|
|
932
|
+
const guestUsername = await syncStrategy.getCurrentUsername();
|
|
933
|
+
_BaseUser._instance = new _BaseUser(guestUsername, syncStrategy);
|
|
934
|
+
await _BaseUser._instance.init();
|
|
935
|
+
return _BaseUser._instance;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
constructor(username, syncStrategy) {
|
|
939
|
+
_BaseUser._initialized = false;
|
|
940
|
+
this._username = username;
|
|
941
|
+
this.syncStrategy = syncStrategy;
|
|
942
|
+
this.setDBandQ();
|
|
943
|
+
}
|
|
944
|
+
setDBandQ() {
|
|
945
|
+
this.localDB = getLocalUserDB(this._username);
|
|
946
|
+
this.remoteDB = this.syncStrategy.setupRemoteDB(this._username);
|
|
947
|
+
this.updateQueue = new UpdateQueue(this.localDB);
|
|
948
|
+
}
|
|
949
|
+
async init() {
|
|
950
|
+
_BaseUser._initialized = false;
|
|
951
|
+
this.setDBandQ();
|
|
952
|
+
this.syncStrategy.startSync(this.localDB, this.remoteDB);
|
|
953
|
+
void this.applyDesignDocs();
|
|
954
|
+
void this.deduplicateReviews();
|
|
955
|
+
_BaseUser._initialized = true;
|
|
956
|
+
}
|
|
957
|
+
static designDocs = [
|
|
958
|
+
{
|
|
959
|
+
_id: "_design/reviewCards",
|
|
960
|
+
views: {
|
|
961
|
+
reviewCards: {
|
|
962
|
+
map: `function (doc) {
|
|
963
|
+
if (doc._id && doc._id.indexOf('card_review') === 0 && doc.courseId && doc.cardId) {
|
|
964
|
+
emit(doc._id, doc.courseId + '-' + doc.cardId);
|
|
965
|
+
}
|
|
966
|
+
}`
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
];
|
|
971
|
+
async applyDesignDocs() {
|
|
972
|
+
for (const doc of _BaseUser.designDocs) {
|
|
973
|
+
try {
|
|
974
|
+
try {
|
|
975
|
+
const existingDoc = await this.remoteDB.get(doc._id);
|
|
976
|
+
await this.remoteDB.put({
|
|
977
|
+
...doc,
|
|
978
|
+
_rev: existingDoc._rev
|
|
979
|
+
});
|
|
980
|
+
} catch (e) {
|
|
981
|
+
if (e instanceof Error && e.name === "not_found") {
|
|
982
|
+
await this.remoteDB.put(doc);
|
|
983
|
+
} else {
|
|
984
|
+
throw e;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
} catch (error) {
|
|
988
|
+
if (error instanceof Error && error.name === "conflict") {
|
|
989
|
+
logger.warn(`Design doc ${doc._id} update conflict - will retry`);
|
|
990
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
991
|
+
await this.applyDesignDoc(doc);
|
|
992
|
+
} else {
|
|
993
|
+
logger.error(`Failed to apply design doc ${doc._id}:`, error);
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// Helper method for single doc update with retry
|
|
1000
|
+
async applyDesignDoc(doc, retries = 3) {
|
|
1001
|
+
try {
|
|
1002
|
+
const existingDoc = await this.remoteDB.get(doc._id);
|
|
1003
|
+
await this.remoteDB.put({
|
|
1004
|
+
...doc,
|
|
1005
|
+
_rev: existingDoc._rev
|
|
1006
|
+
});
|
|
1007
|
+
} catch (e) {
|
|
1008
|
+
if (e instanceof Error && e.name === "conflict" && retries > 0) {
|
|
1009
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1010
|
+
return this.applyDesignDoc(doc, retries - 1);
|
|
1011
|
+
}
|
|
1012
|
+
throw e;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Logs a record of the user's interaction with the card and returns the card's
|
|
1017
|
+
* up-to-date history
|
|
1018
|
+
*
|
|
1019
|
+
* // [ ] #db-refactor extract to a smaller scope - eg, UserStudySession
|
|
1020
|
+
*
|
|
1021
|
+
* @param record the recent recorded interaction between user and card
|
|
1022
|
+
* @returns The updated state of the card's CardHistory data
|
|
1023
|
+
*/
|
|
1024
|
+
async putCardRecord(record) {
|
|
1025
|
+
const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
|
|
1026
|
+
record.timeStamp = import_moment3.default.utc(record.timeStamp).toString();
|
|
1027
|
+
try {
|
|
1028
|
+
const cardHistory = await this.update(
|
|
1029
|
+
cardHistoryID,
|
|
1030
|
+
function(h) {
|
|
1031
|
+
h.records.push(record);
|
|
1032
|
+
h.bestInterval = h.bestInterval || 0;
|
|
1033
|
+
h.lapses = h.lapses || 0;
|
|
1034
|
+
h.streak = h.streak || 0;
|
|
1035
|
+
return h;
|
|
1036
|
+
}
|
|
1037
|
+
);
|
|
1038
|
+
cardHistory.records = cardHistory.records.map((record2) => {
|
|
1039
|
+
const ret = {
|
|
1040
|
+
...record2
|
|
1041
|
+
};
|
|
1042
|
+
ret.timeStamp = import_moment3.default.utc(record2.timeStamp);
|
|
1043
|
+
return ret;
|
|
1044
|
+
});
|
|
1045
|
+
return cardHistory;
|
|
1046
|
+
} catch (e) {
|
|
1047
|
+
const reason = e;
|
|
1048
|
+
if (reason.status === 404) {
|
|
1049
|
+
const initCardHistory = {
|
|
1050
|
+
_id: cardHistoryID,
|
|
1051
|
+
cardID: record.cardID,
|
|
1052
|
+
courseID: record.courseID,
|
|
1053
|
+
records: [record],
|
|
1054
|
+
lapses: 0,
|
|
1055
|
+
streak: 0,
|
|
1056
|
+
bestInterval: 0
|
|
1057
|
+
};
|
|
1058
|
+
void this.remoteDB.put(initCardHistory);
|
|
1059
|
+
return initCardHistory;
|
|
1060
|
+
} else {
|
|
1061
|
+
throw new Error(`putCardRecord failed because of:
|
|
1062
|
+
name:${reason.name}
|
|
1063
|
+
error: ${reason.error}
|
|
1064
|
+
message: ${reason.message}`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
async deduplicateReviews() {
|
|
1069
|
+
try {
|
|
1070
|
+
log3("Starting deduplication of scheduled reviews...");
|
|
1071
|
+
const reviewsMap = {};
|
|
1072
|
+
const duplicateDocIds = [];
|
|
1073
|
+
const scheduledReviews = await this.remoteDB.query("reviewCards/reviewCards");
|
|
1074
|
+
log3(`Found ${scheduledReviews.rows.length} scheduled reviews to process`);
|
|
1075
|
+
scheduledReviews.rows.forEach((r) => {
|
|
1076
|
+
const qualifiedCardId = r.value;
|
|
1077
|
+
const docId = r.key;
|
|
1078
|
+
if (reviewsMap[qualifiedCardId]) {
|
|
1079
|
+
log3(`Found duplicate scheduled review for card: ${qualifiedCardId}`);
|
|
1080
|
+
log3(
|
|
1081
|
+
`Marking earlier review ${reviewsMap[qualifiedCardId]} for deletion, keeping ${docId}`
|
|
1082
|
+
);
|
|
1083
|
+
duplicateDocIds.push(reviewsMap[qualifiedCardId]);
|
|
1084
|
+
reviewsMap[qualifiedCardId] = docId;
|
|
1085
|
+
} else {
|
|
1086
|
+
reviewsMap[qualifiedCardId] = docId;
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
if (duplicateDocIds.length > 0) {
|
|
1090
|
+
log3(`Removing ${duplicateDocIds.length} duplicate reviews...`);
|
|
1091
|
+
const deletePromises = duplicateDocIds.map(async (docId) => {
|
|
1092
|
+
try {
|
|
1093
|
+
const doc = await this.remoteDB.get(docId);
|
|
1094
|
+
await this.remoteDB.remove(doc);
|
|
1095
|
+
log3(`Successfully removed duplicate review: ${docId}`);
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
log3(`Failed to remove duplicate review ${docId}: ${error}`);
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
await Promise.all(deletePromises);
|
|
1101
|
+
log3(`Deduplication complete. Processed ${duplicateDocIds.length} duplicates`);
|
|
1102
|
+
} else {
|
|
1103
|
+
log3("No duplicate reviews found");
|
|
1104
|
+
}
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
log3(`Error during review deduplication: ${error}`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Returns a promise of the card IDs that the user has
|
|
1111
|
+
* encountered in the past.
|
|
1112
|
+
*
|
|
1113
|
+
* @param course_id optional specification of individual course
|
|
1114
|
+
*/
|
|
1115
|
+
async getSeenCards(course_id) {
|
|
1116
|
+
let prefix = cardHistoryPrefix2;
|
|
1117
|
+
if (course_id) {
|
|
1118
|
+
prefix += course_id;
|
|
1119
|
+
}
|
|
1120
|
+
const docs = await filterAllDocsByPrefix(this.localDB, prefix, {
|
|
1121
|
+
include_docs: false
|
|
1122
|
+
});
|
|
1123
|
+
const ret = [];
|
|
1124
|
+
docs.rows.forEach((row) => {
|
|
1125
|
+
if (row.id.startsWith(cardHistoryPrefix2)) {
|
|
1126
|
+
ret.push(row.id.substr(cardHistoryPrefix2.length));
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
return ret;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
*
|
|
1133
|
+
* @returns A promise of the cards that the user has seen in the past.
|
|
1134
|
+
*/
|
|
1135
|
+
async getHistory() {
|
|
1136
|
+
const cards = await filterAllDocsByPrefix(
|
|
1137
|
+
this.remoteDB,
|
|
1138
|
+
cardHistoryPrefix2,
|
|
1139
|
+
{
|
|
1140
|
+
include_docs: true,
|
|
1141
|
+
attachments: false
|
|
1142
|
+
}
|
|
1143
|
+
);
|
|
1144
|
+
return cards.rows.map((r) => r.doc);
|
|
1145
|
+
}
|
|
1146
|
+
async updateCourseSettings(course_id, settings) {
|
|
1147
|
+
void this.getCourseRegistrationsDoc().then((doc) => {
|
|
1148
|
+
const crs = doc.courses.find((c) => c.courseID === course_id);
|
|
1149
|
+
if (crs) {
|
|
1150
|
+
if (crs.settings === null || crs.settings === void 0) {
|
|
1151
|
+
crs.settings = {};
|
|
1152
|
+
}
|
|
1153
|
+
settings.forEach((setting) => {
|
|
1154
|
+
crs.settings[setting.key] = setting.value;
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
return this.localDB.put(doc);
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
async getCourseSettings(course_id) {
|
|
1161
|
+
const regDoc = await this.getCourseRegistrationsDoc();
|
|
1162
|
+
const crsDoc = regDoc.courses.find((c) => c.courseID === course_id);
|
|
1163
|
+
if (crsDoc) {
|
|
1164
|
+
return crsDoc.settings;
|
|
1165
|
+
} else {
|
|
1166
|
+
throw new Error(`getCourseSettings Failed:
|
|
1167
|
+
User is not registered for course ${course_id}`);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
async getOrCreateClassroomRegistrationsDoc() {
|
|
1171
|
+
let ret;
|
|
1172
|
+
try {
|
|
1173
|
+
ret = await this.remoteDB.get(
|
|
1174
|
+
_BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS
|
|
1175
|
+
);
|
|
1176
|
+
} catch (e) {
|
|
1177
|
+
const err = e;
|
|
1178
|
+
if (err.status === 404) {
|
|
1179
|
+
await this.remoteDB.put({
|
|
1180
|
+
_id: _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS,
|
|
1181
|
+
registrations: []
|
|
1182
|
+
});
|
|
1183
|
+
ret = await this.getOrCreateClassroomRegistrationsDoc();
|
|
1184
|
+
} else {
|
|
1185
|
+
const errorDetails = {
|
|
1186
|
+
name: err.name,
|
|
1187
|
+
status: err.status,
|
|
1188
|
+
message: err.message,
|
|
1189
|
+
reason: err.reason,
|
|
1190
|
+
error: err.error
|
|
1191
|
+
};
|
|
1192
|
+
logger.error(
|
|
1193
|
+
"Database error in getOrCreateClassroomRegistrationsDoc (private method):",
|
|
1194
|
+
errorDetails
|
|
1195
|
+
);
|
|
1196
|
+
throw new Error(
|
|
1197
|
+
`Database error accessing classroom registrations: ${err.message || err.name || "Unknown error"} (status: ${err.status})`
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
logger.debug(`Returning classroom registrations doc: ${JSON.stringify(ret)}`);
|
|
1202
|
+
return ret;
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Retrieves the list of active classroom IDs where the user is registered as a student.
|
|
1206
|
+
*
|
|
1207
|
+
* @returns Promise<string[]> - Array of classroom IDs, or empty array if classroom
|
|
1208
|
+
* registration document is unavailable due to database errors
|
|
1209
|
+
*
|
|
1210
|
+
* @description This method gracefully handles database connectivity issues by returning
|
|
1211
|
+
* an empty array when the classroom registrations document cannot be accessed.
|
|
1212
|
+
* This ensures that users can still access other application features even
|
|
1213
|
+
* when classroom functionality is temporarily unavailable.
|
|
1214
|
+
*/
|
|
1215
|
+
async getActiveClasses() {
|
|
1216
|
+
try {
|
|
1217
|
+
return (await this.getOrCreateClassroomRegistrationsDoc()).registrations.filter((c) => c.registeredAs === "student").map((c) => c.classID);
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
logger.warn(
|
|
1220
|
+
"Failed to load classroom registrations, continuing without classroom data:",
|
|
1221
|
+
error
|
|
1222
|
+
);
|
|
1223
|
+
return [];
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
async scheduleCardReview(review) {
|
|
1227
|
+
return scheduleCardReviewLocal(this.remoteDB, review);
|
|
1228
|
+
}
|
|
1229
|
+
async removeScheduledCardReview(reviewId) {
|
|
1230
|
+
return removeScheduledCardReviewLocal(this.remoteDB, reviewId);
|
|
1231
|
+
}
|
|
1232
|
+
async registerForClassroom(_classId, _registerAs) {
|
|
1233
|
+
return registerUserForClassroom(this._username, _classId, _registerAs);
|
|
1234
|
+
}
|
|
1235
|
+
async dropFromClassroom(classId) {
|
|
1236
|
+
return dropUserFromClassroom(this._username, classId);
|
|
1237
|
+
}
|
|
1238
|
+
async getUserClassrooms() {
|
|
1239
|
+
return getUserClassrooms(this._username);
|
|
1240
|
+
}
|
|
1241
|
+
async updateUserElo(courseId, elo) {
|
|
1242
|
+
return updateUserElo(this._username, courseId, elo);
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
userCoursesDoc = "CourseRegistrations";
|
|
1246
|
+
userClassroomsDoc = "ClassroomRegistrations";
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
// src/impl/common/index.ts
|
|
1251
|
+
var init_common = __esm({
|
|
1252
|
+
"src/impl/common/index.ts"() {
|
|
1253
|
+
"use strict";
|
|
1254
|
+
init_SyncStrategy();
|
|
1255
|
+
init_BaseUserDB();
|
|
1256
|
+
init_userDBHelpers();
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
// src/impl/couch/courseAPI.ts
|
|
1261
|
+
async function addNote55(courseID, codeCourse, shape, data, author, tags, uploads, elo = (0, import_common3.blankCourseElo)()) {
|
|
1262
|
+
const db = getCourseDB(courseID);
|
|
1263
|
+
const payload = (0, import_common4.prepareNote55)(courseID, codeCourse, shape, data, author, tags, uploads);
|
|
1264
|
+
const result = await db.post(payload);
|
|
1265
|
+
const dataShapeId = import_common2.NameSpacer.getDataShapeString({
|
|
1266
|
+
course: codeCourse,
|
|
1267
|
+
dataShape: shape.name
|
|
1268
|
+
});
|
|
1269
|
+
if (result.ok) {
|
|
1270
|
+
try {
|
|
1271
|
+
await createCards(courseID, dataShapeId, result.id, tags, elo, author);
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
let errorMessage = "Unknown error";
|
|
1274
|
+
if (error instanceof Error) {
|
|
1275
|
+
errorMessage = error.message;
|
|
1276
|
+
} else if (error && typeof error === "object" && "reason" in error) {
|
|
1277
|
+
errorMessage = error.reason;
|
|
1278
|
+
} else if (error && typeof error === "object" && "message" in error) {
|
|
1279
|
+
errorMessage = error.message;
|
|
1280
|
+
} else {
|
|
1281
|
+
errorMessage = String(error);
|
|
1282
|
+
}
|
|
1283
|
+
logger.error(`[addNote55] Failed to create cards for note ${result.id}: ${errorMessage}`);
|
|
1284
|
+
result.cardCreationFailed = true;
|
|
1285
|
+
result.cardCreationError = errorMessage;
|
|
1286
|
+
}
|
|
1287
|
+
} else {
|
|
1288
|
+
logger.error(`[addNote55] Error adding note. Result: ${JSON.stringify(result)}`);
|
|
1289
|
+
}
|
|
1290
|
+
return result;
|
|
1291
|
+
}
|
|
1292
|
+
async function createCards(courseID, datashapeID, noteID, tags, elo = (0, import_common3.blankCourseElo)(), author) {
|
|
1293
|
+
const cfg = await getCredentialledCourseConfig(courseID);
|
|
1294
|
+
const dsDescriptor = import_common2.NameSpacer.getDataShapeDescriptor(datashapeID);
|
|
1295
|
+
let questionViewTypes = [];
|
|
1296
|
+
for (const ds of cfg.dataShapes) {
|
|
1297
|
+
if (ds.name === datashapeID) {
|
|
1298
|
+
questionViewTypes = ds.questionTypes;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
if (questionViewTypes.length === 0) {
|
|
1302
|
+
const errorMsg = `No questionViewTypes found for datashapeID: ${datashapeID} in course config. Cards cannot be created.`;
|
|
1303
|
+
logger.error(errorMsg);
|
|
1304
|
+
throw new Error(errorMsg);
|
|
1305
|
+
}
|
|
1306
|
+
for (const questionView of questionViewTypes) {
|
|
1307
|
+
await createCard(questionView, courseID, dsDescriptor, noteID, tags, elo, author);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
async function createCard(questionViewName, courseID, dsDescriptor, noteID, tags, elo = (0, import_common3.blankCourseElo)(), author) {
|
|
1311
|
+
const qDescriptor = import_common2.NameSpacer.getQuestionDescriptor(questionViewName);
|
|
1312
|
+
const cfg = await getCredentialledCourseConfig(courseID);
|
|
1313
|
+
for (const rQ of cfg.questionTypes) {
|
|
1314
|
+
if (rQ.name === questionViewName) {
|
|
1315
|
+
for (const view of rQ.viewList) {
|
|
1316
|
+
await addCard(
|
|
1317
|
+
courseID,
|
|
1318
|
+
dsDescriptor.course,
|
|
1319
|
+
[noteID],
|
|
1320
|
+
import_common2.NameSpacer.getViewString({
|
|
1321
|
+
course: qDescriptor.course,
|
|
1322
|
+
questionType: qDescriptor.questionType,
|
|
1323
|
+
view
|
|
1324
|
+
}),
|
|
1325
|
+
elo,
|
|
1326
|
+
tags,
|
|
1327
|
+
author
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
async function addCard(courseID, course, id_displayable_data, id_view, elo, tags, author) {
|
|
1334
|
+
const db = getCourseDB(courseID);
|
|
1335
|
+
const card = await db.post({
|
|
1336
|
+
course,
|
|
1337
|
+
id_displayable_data,
|
|
1338
|
+
id_view,
|
|
1339
|
+
docType: "CARD" /* CARD */,
|
|
1340
|
+
elo: elo || (0, import_common3.toCourseElo)(990 + Math.round(20 * Math.random())),
|
|
1341
|
+
author
|
|
1342
|
+
});
|
|
1343
|
+
for (const tag of tags) {
|
|
1344
|
+
logger.info(`adding tag: ${tag} to card ${card.id}`);
|
|
1345
|
+
await addTagToCard(courseID, card.id, tag, author, false);
|
|
1346
|
+
}
|
|
1347
|
+
return card;
|
|
1348
|
+
}
|
|
1349
|
+
async function getCredentialledCourseConfig(courseID) {
|
|
1350
|
+
try {
|
|
1351
|
+
const db = getCourseDB(courseID);
|
|
1352
|
+
const ret = await db.get("CourseConfig");
|
|
1353
|
+
ret.courseID = courseID;
|
|
1354
|
+
logger.info(`Returning course config: ${JSON.stringify(ret)}`);
|
|
1355
|
+
return ret;
|
|
1356
|
+
} catch (e) {
|
|
1357
|
+
logger.error(`Error fetching config for ${courseID}:`, e);
|
|
1358
|
+
throw e;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
async function addTagToCard(courseID, cardID, tagID, author, updateELO = true) {
|
|
1362
|
+
const prefixedTagID = getTagID(tagID);
|
|
1363
|
+
const courseDB = getCourseDB(courseID);
|
|
1364
|
+
const courseApi = new CourseDB(courseID, async () => {
|
|
1365
|
+
const dummySyncStrategy = {
|
|
1366
|
+
setupRemoteDB: () => null,
|
|
1367
|
+
startSync: () => {
|
|
1368
|
+
},
|
|
1369
|
+
canCreateAccount: () => false,
|
|
1370
|
+
canAuthenticate: () => false,
|
|
1371
|
+
getCurrentUsername: async () => "DummyUser"
|
|
1372
|
+
};
|
|
1373
|
+
return BaseUser.Dummy(dummySyncStrategy);
|
|
1374
|
+
});
|
|
1375
|
+
try {
|
|
1376
|
+
logger.info(`Applying tag ${tagID} to card ${courseID + "-" + cardID}...`);
|
|
1377
|
+
const tag = await courseDB.get(prefixedTagID);
|
|
1378
|
+
if (!tag.taggedCards.includes(cardID)) {
|
|
1379
|
+
tag.taggedCards.push(cardID);
|
|
1380
|
+
if (updateELO) {
|
|
1381
|
+
try {
|
|
1382
|
+
const eloData = await courseApi.getCardEloData([cardID]);
|
|
1383
|
+
const elo = eloData[0];
|
|
1384
|
+
elo.tags[tagID] = {
|
|
1385
|
+
count: 0,
|
|
1386
|
+
score: elo.global.score
|
|
1387
|
+
// todo: or 1000?
|
|
1388
|
+
};
|
|
1389
|
+
await updateCardElo(courseID, cardID, elo);
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
logger.error("Failed to update ELO data for card:", cardID, error);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
return courseDB.put(tag);
|
|
1395
|
+
} else throw new AlreadyTaggedErr(`Card ${cardID} is already tagged with ${tagID}`);
|
|
1396
|
+
} catch (e) {
|
|
1397
|
+
if (e instanceof AlreadyTaggedErr) {
|
|
1398
|
+
throw e;
|
|
1399
|
+
}
|
|
1400
|
+
await createTag(courseID, tagID, author);
|
|
1401
|
+
return addTagToCard(courseID, cardID, tagID, author, updateELO);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
async function updateCardElo(courseID, cardID, elo) {
|
|
1405
|
+
if (elo) {
|
|
1406
|
+
const cDB = getCourseDB(courseID);
|
|
1407
|
+
const card = await cDB.get(cardID);
|
|
1408
|
+
logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
|
|
1409
|
+
card.elo = elo;
|
|
1410
|
+
return cDB.put(card);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
function getTagID(tagName) {
|
|
1414
|
+
const tagPrefix = "TAG" /* TAG */.valueOf() + "-";
|
|
1415
|
+
if (tagName.indexOf(tagPrefix) === 0) {
|
|
1416
|
+
return tagName;
|
|
1417
|
+
} else {
|
|
1418
|
+
return tagPrefix + tagName;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function getCourseDB(courseID) {
|
|
1422
|
+
const dbName = `coursedb-${courseID}`;
|
|
1423
|
+
return new pouchdb_setup_default(
|
|
1424
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
1425
|
+
pouchDBincludeCredentialsConfig
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
var import_common2, import_common3, import_common4, AlreadyTaggedErr;
|
|
1429
|
+
var init_courseAPI = __esm({
|
|
1430
|
+
"src/impl/couch/courseAPI.ts"() {
|
|
1431
|
+
"use strict";
|
|
1432
|
+
init_pouchdb_setup();
|
|
1433
|
+
init_couch();
|
|
1434
|
+
init_factory();
|
|
1435
|
+
import_common2 = require("@vue-skuilder/common");
|
|
1436
|
+
import_common3 = require("@vue-skuilder/common");
|
|
1437
|
+
init_courseDB();
|
|
1438
|
+
init_types_legacy();
|
|
1439
|
+
import_common4 = require("@vue-skuilder/common");
|
|
1440
|
+
init_common();
|
|
1441
|
+
init_logger();
|
|
1442
|
+
AlreadyTaggedErr = class extends Error {
|
|
1443
|
+
constructor(message) {
|
|
1444
|
+
super(message);
|
|
1445
|
+
this.name = "AlreadyTaggedErr";
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
// src/impl/couch/courseLookupDB.ts
|
|
1452
|
+
var courseLookupDBTitle, CourseLookup;
|
|
1453
|
+
var init_courseLookupDB = __esm({
|
|
1454
|
+
"src/impl/couch/courseLookupDB.ts"() {
|
|
1455
|
+
"use strict";
|
|
1456
|
+
init_pouchdb_setup();
|
|
1457
|
+
init_factory();
|
|
1458
|
+
init_logger();
|
|
1459
|
+
courseLookupDBTitle = "coursedb-lookup";
|
|
1460
|
+
logger.debug(`COURSELOOKUP FILE RUNNING`);
|
|
1461
|
+
CourseLookup = class _CourseLookup {
|
|
1462
|
+
// [ ] this db should be read only for public, admin-only for write
|
|
1463
|
+
// Cache for the PouchDB instance
|
|
1464
|
+
static _dbInstance = null;
|
|
1465
|
+
/**
|
|
1466
|
+
* Static getter for the PouchDB database instance.
|
|
1467
|
+
* Connects using ENV variables and caches the instance.
|
|
1468
|
+
* Throws an error if required ENV variables are not set.
|
|
1469
|
+
*/
|
|
1470
|
+
static get _db() {
|
|
1471
|
+
if (this._dbInstance) {
|
|
1472
|
+
return this._dbInstance;
|
|
1473
|
+
}
|
|
1474
|
+
if (ENV.COUCHDB_SERVER_URL === "NOT_SET" || !ENV.COUCHDB_SERVER_URL) {
|
|
1475
|
+
throw new Error(
|
|
1476
|
+
"CourseLookup.db: COUCHDB_SERVER_URL is not set. Ensure initializeDataLayer has been called with valid configuration."
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
if (ENV.COUCHDB_SERVER_PROTOCOL === "NOT_SET" || !ENV.COUCHDB_SERVER_PROTOCOL) {
|
|
1480
|
+
throw new Error(
|
|
1481
|
+
"CourseLookup.db: COUCHDB_SERVER_PROTOCOL is not set. Ensure initializeDataLayer has been called with valid configuration."
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
const dbUrl = `${ENV.COUCHDB_SERVER_PROTOCOL}://${ENV.COUCHDB_SERVER_URL}/${courseLookupDBTitle}`;
|
|
1485
|
+
const options = {
|
|
1486
|
+
skip_setup: true
|
|
1487
|
+
// Keep the original option
|
|
1488
|
+
// fetch: (url, opts) => { // Optional: Add for debugging network requests
|
|
1489
|
+
// console.log('PouchDB fetch:', url, opts);
|
|
1490
|
+
// return pouch.fetch(url, opts);
|
|
1491
|
+
// }
|
|
1492
|
+
};
|
|
1493
|
+
if (ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD) {
|
|
1494
|
+
options.auth = {
|
|
1495
|
+
username: ENV.COUCHDB_USERNAME,
|
|
1496
|
+
password: ENV.COUCHDB_PASSWORD
|
|
1497
|
+
};
|
|
1498
|
+
logger.info(`CourseLookup: Connecting to ${dbUrl} with authentication.`);
|
|
1499
|
+
} else {
|
|
1500
|
+
logger.info(`CourseLookup: Connecting to ${dbUrl} without authentication.`);
|
|
1501
|
+
}
|
|
1502
|
+
try {
|
|
1503
|
+
this._dbInstance = new pouchdb_setup_default(dbUrl, options);
|
|
1504
|
+
logger.info(`CourseLookup: Database instance created for ${courseLookupDBTitle}.`);
|
|
1505
|
+
return this._dbInstance;
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
logger.error(`CourseLookup: Failed to create PouchDB instance for ${dbUrl}`, error);
|
|
1508
|
+
this._dbInstance = null;
|
|
1509
|
+
throw new Error(
|
|
1510
|
+
`CourseLookup: Failed to initialize database connection: ${error instanceof Error ? error.message : String(error)}`
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Adds a new course to the lookup database, and returns the courseID
|
|
1516
|
+
* @param courseName
|
|
1517
|
+
* @returns
|
|
1518
|
+
*/
|
|
1519
|
+
static async add(courseName) {
|
|
1520
|
+
const resp = await _CourseLookup._db.post({
|
|
1521
|
+
name: courseName
|
|
1522
|
+
});
|
|
1523
|
+
return resp.id;
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Removes a course from the index
|
|
1527
|
+
* @param courseID
|
|
1528
|
+
*/
|
|
1529
|
+
static async delete(courseID) {
|
|
1530
|
+
const doc = await _CourseLookup._db.get(courseID);
|
|
1531
|
+
return await _CourseLookup._db.remove(doc);
|
|
1532
|
+
}
|
|
1533
|
+
static async allCourses() {
|
|
1534
|
+
const resp = await _CourseLookup._db.allDocs({
|
|
1535
|
+
include_docs: true
|
|
1536
|
+
});
|
|
1537
|
+
return resp.rows.map((row) => row.doc);
|
|
1538
|
+
}
|
|
1539
|
+
static async updateDisambiguator(courseID, disambiguator) {
|
|
1540
|
+
const doc = await _CourseLookup._db.get(courseID);
|
|
1541
|
+
doc.disambiguator = disambiguator;
|
|
1542
|
+
return await _CourseLookup._db.put(doc);
|
|
1543
|
+
}
|
|
1544
|
+
static async isCourse(courseID) {
|
|
1545
|
+
try {
|
|
1546
|
+
await _CourseLookup._db.get(courseID);
|
|
1547
|
+
return true;
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
logger.info(`Courselookup failed:`, error);
|
|
1550
|
+
return false;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
// src/core/navigators/elo.ts
|
|
1558
|
+
var elo_exports = {};
|
|
1559
|
+
__export(elo_exports, {
|
|
1560
|
+
default: () => ELONavigator
|
|
1561
|
+
});
|
|
1562
|
+
var ELONavigator;
|
|
1563
|
+
var init_elo = __esm({
|
|
1564
|
+
"src/core/navigators/elo.ts"() {
|
|
1565
|
+
"use strict";
|
|
1566
|
+
init_navigators();
|
|
1567
|
+
ELONavigator = class extends ContentNavigator {
|
|
1568
|
+
user;
|
|
1569
|
+
course;
|
|
1570
|
+
constructor(user, course) {
|
|
1571
|
+
super();
|
|
1572
|
+
this.user = user;
|
|
1573
|
+
this.course = course;
|
|
1574
|
+
}
|
|
1575
|
+
async getPendingReviews() {
|
|
1576
|
+
const reviews = await this.user.getPendingReviews(this.course.getCourseID());
|
|
1577
|
+
const elo = await this.course.getCardEloData(reviews.map((r) => r.cardId));
|
|
1578
|
+
const ratedReviews = reviews.map((r, i) => {
|
|
1579
|
+
const ratedR = {
|
|
1580
|
+
...r,
|
|
1581
|
+
...elo[i]
|
|
1582
|
+
};
|
|
1583
|
+
return ratedR;
|
|
1584
|
+
});
|
|
1585
|
+
ratedReviews.sort((a, b) => {
|
|
1586
|
+
return a.global.score - b.global.score;
|
|
1587
|
+
});
|
|
1588
|
+
return ratedReviews.map((r) => {
|
|
1589
|
+
return {
|
|
1590
|
+
...r,
|
|
1591
|
+
contentSourceType: "course",
|
|
1592
|
+
contentSourceID: this.course.getCourseID(),
|
|
1593
|
+
cardID: r.cardId,
|
|
1594
|
+
courseID: r.courseId,
|
|
1595
|
+
qualifiedID: `${r.courseId}-${r.cardId}`,
|
|
1596
|
+
reviewID: r._id,
|
|
1597
|
+
status: "review"
|
|
1598
|
+
};
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
async getNewCards(limit = 99) {
|
|
1602
|
+
const activeCards = await this.user.getActiveCards();
|
|
1603
|
+
return (await this.course.getCardsCenteredAtELO({ limit, elo: "user" }, (c) => {
|
|
1604
|
+
if (activeCards.some((ac) => c.includes(ac))) {
|
|
1605
|
+
return false;
|
|
1606
|
+
} else {
|
|
1607
|
+
return true;
|
|
1608
|
+
}
|
|
1609
|
+
})).map((c) => {
|
|
1610
|
+
return {
|
|
1611
|
+
...c,
|
|
1612
|
+
status: "new"
|
|
1613
|
+
};
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
// import("./**/*") in src/core/navigators/index.ts
|
|
1621
|
+
var globImport;
|
|
1622
|
+
var init_ = __esm({
|
|
1623
|
+
'import("./**/*") in src/core/navigators/index.ts'() {
|
|
1624
|
+
globImport = __glob({
|
|
1625
|
+
"./elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
1626
|
+
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
// src/core/navigators/index.ts
|
|
1632
|
+
var navigators_exports = {};
|
|
1633
|
+
__export(navigators_exports, {
|
|
1634
|
+
ContentNavigator: () => ContentNavigator,
|
|
1635
|
+
Navigators: () => Navigators
|
|
1636
|
+
});
|
|
1637
|
+
var Navigators, ContentNavigator;
|
|
1638
|
+
var init_navigators = __esm({
|
|
1639
|
+
"src/core/navigators/index.ts"() {
|
|
1640
|
+
"use strict";
|
|
1641
|
+
init_logger();
|
|
1642
|
+
init_();
|
|
1643
|
+
Navigators = /* @__PURE__ */ ((Navigators2) => {
|
|
1644
|
+
Navigators2["ELO"] = "elo";
|
|
1645
|
+
return Navigators2;
|
|
1646
|
+
})(Navigators || {});
|
|
1647
|
+
ContentNavigator = class {
|
|
1648
|
+
/**
|
|
1649
|
+
*
|
|
1650
|
+
* @param user
|
|
1651
|
+
* @param strategyData
|
|
1652
|
+
* @returns the runtime object used to steer a study session.
|
|
1653
|
+
*/
|
|
1654
|
+
static async create(user, course, strategyData) {
|
|
1655
|
+
const implementingClass = strategyData.implementingClass;
|
|
1656
|
+
let NavigatorImpl;
|
|
1657
|
+
const variations = ["", ".js", ".ts"];
|
|
1658
|
+
for (const ext of variations) {
|
|
1659
|
+
try {
|
|
1660
|
+
const module2 = await globImport(`./${implementingClass}${ext}`);
|
|
1661
|
+
NavigatorImpl = module2.default;
|
|
1662
|
+
break;
|
|
1663
|
+
} catch (e) {
|
|
1664
|
+
logger.debug(`Failed to load with extension ${ext}:`, e);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
if (!NavigatorImpl) {
|
|
1668
|
+
throw new Error(`Could not load navigator implementation for: ${implementingClass}`);
|
|
1669
|
+
}
|
|
1670
|
+
return new NavigatorImpl(user, course, strategyData);
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
// src/impl/couch/courseDB.ts
|
|
1677
|
+
function randIntWeightedTowardZero(n) {
|
|
1678
|
+
return Math.floor(Math.random() * Math.random() * Math.random() * n);
|
|
1679
|
+
}
|
|
1680
|
+
async function getCourseDataShapes(courseID) {
|
|
1681
|
+
const cfg = await getCredentialledCourseConfig(courseID);
|
|
1682
|
+
return cfg.dataShapes;
|
|
1683
|
+
}
|
|
1684
|
+
async function getCredentialledDataShapes(courseID) {
|
|
1685
|
+
const cfg = await getCredentialledCourseConfig(courseID);
|
|
1686
|
+
return cfg.dataShapes;
|
|
1687
|
+
}
|
|
1688
|
+
async function getCourseQuestionTypes(courseID) {
|
|
1689
|
+
const cfg = await getCredentialledCourseConfig(courseID);
|
|
1690
|
+
return cfg.questionTypes;
|
|
1691
|
+
}
|
|
1692
|
+
async function getCourseTagStubs(courseID) {
|
|
1693
|
+
logger.debug(`Getting tag stubs for course: ${courseID}`);
|
|
1694
|
+
const stubs = await filterAllDocsByPrefix2(
|
|
1695
|
+
getCourseDB2(courseID),
|
|
1696
|
+
"TAG" /* TAG */.valueOf() + "-"
|
|
1697
|
+
);
|
|
1698
|
+
stubs.rows.forEach((row) => {
|
|
1699
|
+
logger.debug(` Tag stub for doc: ${row.id}`);
|
|
1700
|
+
});
|
|
1701
|
+
return stubs;
|
|
1702
|
+
}
|
|
1703
|
+
async function deleteTag(courseID, tagName) {
|
|
1704
|
+
tagName = getTagID(tagName);
|
|
1705
|
+
const courseDB = getCourseDB2(courseID);
|
|
1706
|
+
const doc = await courseDB.get("TAG" /* TAG */.valueOf() + "-" + tagName);
|
|
1707
|
+
const resp = await courseDB.remove(doc);
|
|
1708
|
+
return resp;
|
|
1709
|
+
}
|
|
1710
|
+
async function createTag(courseID, tagName, author) {
|
|
1711
|
+
logger.debug(`Creating tag: ${tagName}...`);
|
|
1712
|
+
const tagID = getTagID(tagName);
|
|
1713
|
+
const courseDB = getCourseDB2(courseID);
|
|
1714
|
+
const resp = await courseDB.put({
|
|
1715
|
+
course: courseID,
|
|
1716
|
+
docType: "TAG" /* TAG */,
|
|
1717
|
+
name: tagName,
|
|
1718
|
+
snippet: "",
|
|
1719
|
+
taggedCards: [],
|
|
1720
|
+
wiki: "",
|
|
1721
|
+
author,
|
|
1722
|
+
_id: tagID
|
|
1723
|
+
});
|
|
1724
|
+
return resp;
|
|
1725
|
+
}
|
|
1726
|
+
async function updateTag(tag) {
|
|
1727
|
+
const prior = await getTag(tag.course, tag.name);
|
|
1728
|
+
return await getCourseDB2(tag.course).put({
|
|
1729
|
+
...tag,
|
|
1730
|
+
_rev: prior._rev
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
async function getTag(courseID, tagName) {
|
|
1734
|
+
const tagID = getTagID(tagName);
|
|
1735
|
+
const courseDB = getCourseDB2(courseID);
|
|
1736
|
+
return courseDB.get(tagID);
|
|
1737
|
+
}
|
|
1738
|
+
async function removeTagFromCard(courseID, cardID, tagID) {
|
|
1739
|
+
tagID = getTagID(tagID);
|
|
1740
|
+
const courseDB = getCourseDB2(courseID);
|
|
1741
|
+
const tag = await courseDB.get(tagID);
|
|
1742
|
+
tag.taggedCards = tag.taggedCards.filter((taggedID) => {
|
|
1743
|
+
return cardID !== taggedID;
|
|
1744
|
+
});
|
|
1745
|
+
return courseDB.put(tag);
|
|
1746
|
+
}
|
|
1747
|
+
function getAncestorTagIDs(courseID, tagID) {
|
|
1748
|
+
tagID = getTagID(tagID);
|
|
1749
|
+
const split = tagID.split(">");
|
|
1750
|
+
if (split.length === 1) {
|
|
1751
|
+
return [];
|
|
1752
|
+
} else {
|
|
1753
|
+
split.pop();
|
|
1754
|
+
const parent = split.join(">");
|
|
1755
|
+
return [parent].concat(getAncestorTagIDs(courseID, parent));
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
async function getChildTagStubs(courseID, tagID) {
|
|
1759
|
+
return await filterAllDocsByPrefix2(getCourseDB2(courseID), tagID + ">");
|
|
1760
|
+
}
|
|
1761
|
+
async function getAppliedTags(id_course, id_card) {
|
|
1762
|
+
const db = getCourseDB2(id_course);
|
|
1763
|
+
const result = await db.query("getTags", {
|
|
1764
|
+
startkey: id_card,
|
|
1765
|
+
endkey: id_card
|
|
1766
|
+
// include_docs: true
|
|
1767
|
+
});
|
|
1768
|
+
return result;
|
|
1769
|
+
}
|
|
1770
|
+
async function updateCardElo2(courseID, cardID, elo) {
|
|
1771
|
+
if (elo) {
|
|
1772
|
+
const cDB = getCourseDB2(courseID);
|
|
1773
|
+
const card = await cDB.get(cardID);
|
|
1774
|
+
logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
|
|
1775
|
+
card.elo = elo;
|
|
1776
|
+
return cDB.put(card);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
async function updateCredentialledCourseConfig(courseID, config) {
|
|
1780
|
+
logger.debug(`Updating course config:
|
|
1781
|
+
|
|
1782
|
+
${JSON.stringify(config)}
|
|
1783
|
+
`);
|
|
1784
|
+
const db = getCourseDB2(courseID);
|
|
1785
|
+
const old = await getCredentialledCourseConfig(courseID);
|
|
1786
|
+
return await db.put({
|
|
1787
|
+
...config,
|
|
1788
|
+
_rev: old._rev
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
function isSuccessRow(row) {
|
|
1792
|
+
return "doc" in row && row.doc !== null && row.doc !== void 0;
|
|
1793
|
+
}
|
|
1794
|
+
var import_common6, CoursesDB, CourseDB;
|
|
1795
|
+
var init_courseDB = __esm({
|
|
1796
|
+
"src/impl/couch/courseDB.ts"() {
|
|
1797
|
+
"use strict";
|
|
1798
|
+
import_common6 = require("@vue-skuilder/common");
|
|
1799
|
+
init_couch();
|
|
1800
|
+
init_updateQueue();
|
|
1801
|
+
init_types_legacy();
|
|
1802
|
+
init_logger();
|
|
1803
|
+
init_clientCache();
|
|
1804
|
+
init_courseAPI();
|
|
1805
|
+
init_courseLookupDB();
|
|
1806
|
+
init_navigators();
|
|
1807
|
+
CoursesDB = class {
|
|
1808
|
+
_courseIDs;
|
|
1809
|
+
constructor(courseIDs) {
|
|
1810
|
+
if (courseIDs && courseIDs.length > 0) {
|
|
1811
|
+
this._courseIDs = courseIDs;
|
|
1812
|
+
} else {
|
|
1813
|
+
this._courseIDs = void 0;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
async getCourseList() {
|
|
1817
|
+
let crsList = await CourseLookup.allCourses();
|
|
1818
|
+
logger.debug(`AllCourses: ${crsList.map((c) => c.name + ", " + c._id + "\n ")}`);
|
|
1819
|
+
if (this._courseIDs) {
|
|
1820
|
+
crsList = crsList.filter((c) => this._courseIDs.includes(c._id));
|
|
1821
|
+
}
|
|
1822
|
+
logger.debug(`AllCourses.filtered: ${crsList.map((c) => c.name + ", " + c._id + "\n ")}`);
|
|
1823
|
+
const cfgs = await Promise.all(
|
|
1824
|
+
crsList.map(async (c) => {
|
|
1825
|
+
try {
|
|
1826
|
+
const cfg = await getCredentialledCourseConfig(c._id);
|
|
1827
|
+
logger.debug(`Found cfg: ${JSON.stringify(cfg)}`);
|
|
1828
|
+
return cfg;
|
|
1829
|
+
} catch (e) {
|
|
1830
|
+
logger.warn(`Error fetching cfg for course ${c.name}, ${c._id}: ${e}`);
|
|
1831
|
+
return void 0;
|
|
1832
|
+
}
|
|
1833
|
+
})
|
|
1834
|
+
);
|
|
1835
|
+
return cfgs.filter((c) => !!c);
|
|
1836
|
+
}
|
|
1837
|
+
async getCourseConfig(courseId) {
|
|
1838
|
+
if (this._courseIDs && this._courseIDs.length && !this._courseIDs.includes(courseId)) {
|
|
1839
|
+
throw new Error(`Course ${courseId} not in course list`);
|
|
1840
|
+
}
|
|
1841
|
+
const cfg = await getCredentialledCourseConfig(courseId);
|
|
1842
|
+
if (cfg === void 0) {
|
|
1843
|
+
throw new Error(`Error fetching cfg for course ${courseId}`);
|
|
1844
|
+
} else {
|
|
1845
|
+
return cfg;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
async disambiguateCourse(courseId, disambiguator) {
|
|
1849
|
+
await CourseLookup.updateDisambiguator(courseId, disambiguator);
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
CourseDB = class {
|
|
1853
|
+
// private log(msg: string): void {
|
|
1854
|
+
// log(`CourseLog: ${this.id}\n ${msg}`);
|
|
1855
|
+
// }
|
|
1856
|
+
db;
|
|
1857
|
+
id;
|
|
1858
|
+
_getCurrentUser;
|
|
1859
|
+
updateQueue;
|
|
1860
|
+
constructor(id, userLookup) {
|
|
1861
|
+
this.id = id;
|
|
1862
|
+
this.db = getCourseDB2(this.id);
|
|
1863
|
+
this._getCurrentUser = userLookup;
|
|
1864
|
+
this.updateQueue = new UpdateQueue(this.db);
|
|
1865
|
+
}
|
|
1866
|
+
getCourseID() {
|
|
1867
|
+
return this.id;
|
|
1868
|
+
}
|
|
1869
|
+
async getCourseInfo() {
|
|
1870
|
+
const cardCount = (await this.db.find({
|
|
1871
|
+
selector: {
|
|
1872
|
+
docType: "CARD" /* CARD */
|
|
1873
|
+
},
|
|
1874
|
+
limit: 1e3
|
|
1875
|
+
})).docs.length;
|
|
1876
|
+
return {
|
|
1877
|
+
cardCount,
|
|
1878
|
+
registeredUsers: 0
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
async getInexperiencedCards(limit = 2) {
|
|
1882
|
+
return (await this.db.query("cardsByInexperience", {
|
|
1883
|
+
limit
|
|
1884
|
+
})).rows.map((r) => {
|
|
1885
|
+
const ret = {
|
|
1886
|
+
courseId: this.id,
|
|
1887
|
+
cardId: r.id,
|
|
1888
|
+
count: r.key,
|
|
1889
|
+
elo: r.value
|
|
1890
|
+
};
|
|
1891
|
+
return ret;
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
async getCardsByEloLimits(options = {
|
|
1895
|
+
low: 0,
|
|
1896
|
+
high: Number.MIN_SAFE_INTEGER,
|
|
1897
|
+
limit: 25,
|
|
1898
|
+
page: 0
|
|
1899
|
+
}) {
|
|
1900
|
+
return (await this.db.query("elo", {
|
|
1901
|
+
startkey: options.low,
|
|
1902
|
+
endkey: options.high,
|
|
1903
|
+
limit: options.limit,
|
|
1904
|
+
skip: options.limit * options.page
|
|
1905
|
+
})).rows.map((r) => {
|
|
1906
|
+
return `${this.id}-${r.id}-${r.key}`;
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
async getCardEloData(id) {
|
|
1910
|
+
const docs = await this.db.allDocs({
|
|
1911
|
+
keys: id,
|
|
1912
|
+
include_docs: true
|
|
1913
|
+
});
|
|
1914
|
+
const ret = [];
|
|
1915
|
+
docs.rows.forEach((r) => {
|
|
1916
|
+
if (isSuccessRow(r)) {
|
|
1917
|
+
if (r.doc && r.doc.elo) {
|
|
1918
|
+
ret.push((0, import_common6.toCourseElo)(r.doc.elo));
|
|
1919
|
+
} else {
|
|
1920
|
+
logger.warn("no elo data for card: " + r.id);
|
|
1921
|
+
ret.push((0, import_common6.blankCourseElo)());
|
|
1922
|
+
}
|
|
1923
|
+
} else {
|
|
1924
|
+
logger.warn("no elo data for card: " + JSON.stringify(r));
|
|
1925
|
+
ret.push((0, import_common6.blankCourseElo)());
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
return ret;
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Returns the lowest and highest `global` ELO ratings in the course
|
|
1932
|
+
*/
|
|
1933
|
+
async getELOBounds() {
|
|
1934
|
+
const [low, high] = await Promise.all([
|
|
1935
|
+
(await this.db.query("elo", {
|
|
1936
|
+
startkey: 0,
|
|
1937
|
+
limit: 1,
|
|
1938
|
+
include_docs: false
|
|
1939
|
+
})).rows[0].key,
|
|
1940
|
+
(await this.db.query("elo", {
|
|
1941
|
+
limit: 1,
|
|
1942
|
+
descending: true,
|
|
1943
|
+
startkey: 1e5
|
|
1944
|
+
})).rows[0].key
|
|
1945
|
+
]);
|
|
1946
|
+
return {
|
|
1947
|
+
low,
|
|
1948
|
+
high
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
async removeCard(id) {
|
|
1952
|
+
const doc = await this.db.get(id);
|
|
1953
|
+
if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
|
|
1954
|
+
throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
|
|
1955
|
+
}
|
|
1956
|
+
return this.db.remove(doc);
|
|
1957
|
+
}
|
|
1958
|
+
async getCardDisplayableDataIDs(id) {
|
|
1959
|
+
logger.debug(id.join(", "));
|
|
1960
|
+
const cards = await this.db.allDocs({
|
|
1961
|
+
keys: id,
|
|
1962
|
+
include_docs: true
|
|
1963
|
+
});
|
|
1964
|
+
const ret = {};
|
|
1965
|
+
cards.rows.forEach((r) => {
|
|
1966
|
+
if (isSuccessRow(r)) {
|
|
1967
|
+
ret[r.id] = r.doc.id_displayable_data;
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
await Promise.all(
|
|
1971
|
+
cards.rows.map((r) => {
|
|
1972
|
+
return async () => {
|
|
1973
|
+
if (isSuccessRow(r)) {
|
|
1974
|
+
ret[r.id] = r.doc.id_displayable_data;
|
|
1975
|
+
}
|
|
1976
|
+
};
|
|
1977
|
+
})
|
|
1978
|
+
);
|
|
1979
|
+
return ret;
|
|
1980
|
+
}
|
|
1981
|
+
async getCardsByELO(elo, cardLimit) {
|
|
1982
|
+
elo = parseInt(elo);
|
|
1983
|
+
const limit = cardLimit ? cardLimit : 25;
|
|
1984
|
+
const below = await this.db.query("elo", {
|
|
1985
|
+
limit: Math.ceil(limit / 2),
|
|
1986
|
+
startkey: elo,
|
|
1987
|
+
descending: true
|
|
1988
|
+
});
|
|
1989
|
+
const aboveLimit = limit - below.rows.length;
|
|
1990
|
+
const above = await this.db.query("elo", {
|
|
1991
|
+
limit: aboveLimit,
|
|
1992
|
+
startkey: elo + 1
|
|
1993
|
+
});
|
|
1994
|
+
let cards = below.rows;
|
|
1995
|
+
cards = cards.concat(above.rows);
|
|
1996
|
+
const ret = cards.sort((a, b) => {
|
|
1997
|
+
const s = Math.abs(a.key - elo) - Math.abs(b.key - elo);
|
|
1998
|
+
if (s === 0) {
|
|
1999
|
+
return Math.random() - 0.5;
|
|
2000
|
+
} else {
|
|
2001
|
+
return s;
|
|
2002
|
+
}
|
|
2003
|
+
}).map((c) => `${this.id}-${c.id}-${c.key}`);
|
|
2004
|
+
const str = `below:
|
|
2005
|
+
${below.rows.map((r) => ` ${r.id}-${r.key}
|
|
2006
|
+
`)}
|
|
2007
|
+
|
|
2008
|
+
above:
|
|
2009
|
+
${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
2010
|
+
`)}`;
|
|
2011
|
+
logger.debug(`Getting ${limit} cards centered around elo: ${elo}:
|
|
2012
|
+
|
|
2013
|
+
` + str);
|
|
2014
|
+
return ret;
|
|
2015
|
+
}
|
|
2016
|
+
async getCourseConfig() {
|
|
2017
|
+
const ret = await getCredentialledCourseConfig(this.id);
|
|
2018
|
+
if (ret) {
|
|
2019
|
+
return ret;
|
|
2020
|
+
} else {
|
|
2021
|
+
throw new Error(`Course config not found for course ID: ${this.id}`);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
async updateCourseConfig(cfg) {
|
|
2025
|
+
logger.debug(`Updating: ${JSON.stringify(cfg)}`);
|
|
2026
|
+
try {
|
|
2027
|
+
return await updateCredentialledCourseConfig(this.id, cfg);
|
|
2028
|
+
} catch (error) {
|
|
2029
|
+
logger.error(`Error updating course config in course DB: ${error}`);
|
|
2030
|
+
throw error;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
async updateCardElo(cardId, elo) {
|
|
2034
|
+
if (!elo) {
|
|
2035
|
+
throw new Error(`Cannot update card elo with null or undefined value for card ID: ${cardId}`);
|
|
2036
|
+
}
|
|
2037
|
+
try {
|
|
2038
|
+
const result = await this.updateQueue.update(cardId, (card) => {
|
|
2039
|
+
logger.debug(`Replacing ${JSON.stringify(card.elo)} with ${JSON.stringify(elo)}`);
|
|
2040
|
+
card.elo = elo;
|
|
2041
|
+
return card;
|
|
2042
|
+
});
|
|
2043
|
+
return { ok: true, id: cardId, rev: result._rev };
|
|
2044
|
+
} catch (error) {
|
|
2045
|
+
logger.error(`Failed to update card elo for card ID: ${cardId}`, error);
|
|
2046
|
+
throw new Error(`Failed to update card elo for card ID: ${cardId}`);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
async getAppliedTags(cardId) {
|
|
2050
|
+
const ret = await getAppliedTags(this.id, cardId);
|
|
2051
|
+
if (ret) {
|
|
2052
|
+
return ret;
|
|
2053
|
+
} else {
|
|
2054
|
+
throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
async addTagToCard(cardId, tagId, updateELO) {
|
|
2058
|
+
return await addTagToCard(this.id, cardId, tagId, (await this._getCurrentUser()).getUsername(), updateELO);
|
|
2059
|
+
}
|
|
2060
|
+
async removeTagFromCard(cardId, tagId) {
|
|
2061
|
+
return await removeTagFromCard(this.id, cardId, tagId);
|
|
2062
|
+
}
|
|
2063
|
+
async createTag(name, author) {
|
|
2064
|
+
return await createTag(this.id, name, author);
|
|
2065
|
+
}
|
|
2066
|
+
async getTag(tagId) {
|
|
2067
|
+
return await getTag(this.id, tagId);
|
|
2068
|
+
}
|
|
2069
|
+
async updateTag(tag) {
|
|
2070
|
+
if (tag.course !== this.id) {
|
|
2071
|
+
throw new Error(`Tag ${JSON.stringify(tag)} does not belong to course ${this.id}`);
|
|
2072
|
+
}
|
|
2073
|
+
return await updateTag(tag);
|
|
2074
|
+
}
|
|
2075
|
+
async getCourseTagStubs() {
|
|
2076
|
+
return getCourseTagStubs(this.id);
|
|
2077
|
+
}
|
|
2078
|
+
async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common6.blankCourseElo)()) {
|
|
2079
|
+
try {
|
|
2080
|
+
const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
|
|
2081
|
+
if (resp.ok) {
|
|
2082
|
+
if (resp.cardCreationFailed) {
|
|
2083
|
+
logger.warn(
|
|
2084
|
+
`[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
|
|
2085
|
+
);
|
|
2086
|
+
return {
|
|
2087
|
+
status: import_common6.Status.error,
|
|
2088
|
+
message: `Note was added but no cards were created: ${resp.cardCreationError}`,
|
|
2089
|
+
id: resp.id
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
return {
|
|
2093
|
+
status: import_common6.Status.ok,
|
|
2094
|
+
message: "",
|
|
2095
|
+
id: resp.id
|
|
2096
|
+
};
|
|
2097
|
+
} else {
|
|
2098
|
+
return {
|
|
2099
|
+
status: import_common6.Status.error,
|
|
2100
|
+
message: "Unexpected error adding note"
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
} catch (e) {
|
|
2104
|
+
const err = e;
|
|
2105
|
+
logger.error(
|
|
2106
|
+
`[addNote] error ${err.name}
|
|
2107
|
+
reason: ${err.reason}
|
|
2108
|
+
message: ${err.message}`
|
|
2109
|
+
);
|
|
2110
|
+
return {
|
|
2111
|
+
status: import_common6.Status.error,
|
|
2112
|
+
message: `Error adding note to course. ${e.reason || err.message}`
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
async getCourseDoc(id, options) {
|
|
2117
|
+
return await getCourseDoc(this.id, id, options);
|
|
2118
|
+
}
|
|
2119
|
+
async getCourseDocs(ids, options = {}) {
|
|
2120
|
+
return await getCourseDocs(this.id, ids, options);
|
|
2121
|
+
}
|
|
2122
|
+
////////////////////////////////////
|
|
2123
|
+
// NavigationStrategyManager implementation
|
|
2124
|
+
////////////////////////////////////
|
|
2125
|
+
getNavigationStrategy(id) {
|
|
2126
|
+
logger.debug(`[courseDB] Getting navigation strategy: ${id}`);
|
|
2127
|
+
const strategy = {
|
|
2128
|
+
id: "ELO",
|
|
2129
|
+
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
2130
|
+
name: "ELO",
|
|
2131
|
+
description: "ELO-based navigation strategy for ordering content by difficulty",
|
|
2132
|
+
implementingClass: "elo" /* ELO */,
|
|
2133
|
+
course: this.id,
|
|
2134
|
+
serializedData: ""
|
|
2135
|
+
// serde is a noop for ELO navigator.
|
|
2136
|
+
};
|
|
2137
|
+
return Promise.resolve(strategy);
|
|
2138
|
+
}
|
|
2139
|
+
getAllNavigationStrategies() {
|
|
2140
|
+
logger.debug("[courseDB] Returning hard-coded navigation strategies");
|
|
2141
|
+
const strategies = [
|
|
2142
|
+
{
|
|
2143
|
+
id: "ELO",
|
|
2144
|
+
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
2145
|
+
name: "ELO",
|
|
2146
|
+
description: "ELO-based navigation strategy for ordering content by difficulty",
|
|
2147
|
+
implementingClass: "elo" /* ELO */,
|
|
2148
|
+
course: this.id,
|
|
2149
|
+
serializedData: ""
|
|
2150
|
+
// serde is a noop for ELO navigator.
|
|
2151
|
+
}
|
|
2152
|
+
];
|
|
2153
|
+
return Promise.resolve(strategies);
|
|
2154
|
+
}
|
|
2155
|
+
addNavigationStrategy(data) {
|
|
2156
|
+
logger.debug(`[courseDB] Adding navigation strategy: ${data.id}`);
|
|
2157
|
+
logger.debug(JSON.stringify(data));
|
|
2158
|
+
return Promise.resolve();
|
|
2159
|
+
}
|
|
2160
|
+
updateNavigationStrategy(id, data) {
|
|
2161
|
+
logger.debug(`[courseDB] Updating navigation strategy: ${id}`);
|
|
2162
|
+
logger.debug(JSON.stringify(data));
|
|
2163
|
+
return Promise.resolve();
|
|
2164
|
+
}
|
|
2165
|
+
async surfaceNavigationStrategy() {
|
|
2166
|
+
logger.warn(`Returning hard-coded default ELO navigator`);
|
|
2167
|
+
const ret = {
|
|
2168
|
+
id: "ELO",
|
|
2169
|
+
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
2170
|
+
name: "ELO",
|
|
2171
|
+
description: "ELO-based navigation strategy",
|
|
2172
|
+
implementingClass: "elo" /* ELO */,
|
|
2173
|
+
course: this.id,
|
|
2174
|
+
serializedData: ""
|
|
2175
|
+
// serde is a noop for ELO navigator.
|
|
2176
|
+
};
|
|
2177
|
+
return Promise.resolve(ret);
|
|
2178
|
+
}
|
|
2179
|
+
////////////////////////////////////
|
|
2180
|
+
// END NavigationStrategyManager implementation
|
|
2181
|
+
////////////////////////////////////
|
|
2182
|
+
////////////////////////////////////
|
|
2183
|
+
// StudyContentSource implementation
|
|
2184
|
+
////////////////////////////////////
|
|
2185
|
+
async getNewCards(limit = 99) {
|
|
2186
|
+
const u = await this._getCurrentUser();
|
|
2187
|
+
try {
|
|
2188
|
+
const strategy = await this.surfaceNavigationStrategy();
|
|
2189
|
+
const navigator = await ContentNavigator.create(u, this, strategy);
|
|
2190
|
+
return navigator.getNewCards(limit);
|
|
2191
|
+
} catch (e) {
|
|
2192
|
+
logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
|
|
2193
|
+
throw e;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
async getPendingReviews() {
|
|
2197
|
+
const u = await this._getCurrentUser();
|
|
2198
|
+
try {
|
|
2199
|
+
const strategy = await this.surfaceNavigationStrategy();
|
|
2200
|
+
const navigator = await ContentNavigator.create(u, this, strategy);
|
|
2201
|
+
return navigator.getPendingReviews();
|
|
2202
|
+
} catch (e) {
|
|
2203
|
+
logger.error(`[courseDB] Error surfacing a NavigationStrategy: ${e}`);
|
|
2204
|
+
throw e;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
async getCardsCenteredAtELO(options = {
|
|
2208
|
+
limit: 99,
|
|
2209
|
+
elo: "user"
|
|
2210
|
+
}, filter) {
|
|
2211
|
+
let targetElo;
|
|
2212
|
+
if (options.elo === "user") {
|
|
2213
|
+
const u = await this._getCurrentUser();
|
|
2214
|
+
targetElo = -1;
|
|
2215
|
+
try {
|
|
2216
|
+
const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
|
|
2217
|
+
return c.courseID === this.id;
|
|
2218
|
+
});
|
|
2219
|
+
targetElo = (0, import_common6.EloToNumber)(courseDoc.elo);
|
|
2220
|
+
} catch {
|
|
2221
|
+
targetElo = 1e3;
|
|
2222
|
+
}
|
|
2223
|
+
} else if (options.elo === "random") {
|
|
2224
|
+
const bounds = await GET_CACHED(`elo-bounds-${this.id}`, () => this.getELOBounds());
|
|
2225
|
+
targetElo = Math.round(bounds.low + Math.random() * (bounds.high - bounds.low));
|
|
2226
|
+
} else {
|
|
2227
|
+
targetElo = options.elo;
|
|
2228
|
+
}
|
|
2229
|
+
let cards = [];
|
|
2230
|
+
let mult = 4;
|
|
2231
|
+
let previousCount = -1;
|
|
2232
|
+
let newCount = 0;
|
|
2233
|
+
while (cards.length < options.limit && newCount !== previousCount) {
|
|
2234
|
+
cards = await this.getCardsByELO(targetElo, mult * options.limit);
|
|
2235
|
+
previousCount = newCount;
|
|
2236
|
+
newCount = cards.length;
|
|
2237
|
+
logger.debug(`Found ${cards.length} elo neighbor cards...`);
|
|
2238
|
+
if (filter) {
|
|
2239
|
+
cards = cards.filter(filter);
|
|
2240
|
+
logger.debug(`Filtered to ${cards.length} cards...`);
|
|
2241
|
+
}
|
|
2242
|
+
mult *= 2;
|
|
2243
|
+
}
|
|
2244
|
+
const selectedCards = [];
|
|
2245
|
+
while (selectedCards.length < options.limit && cards.length > 0) {
|
|
2246
|
+
const index = randIntWeightedTowardZero(cards.length);
|
|
2247
|
+
const card = cards.splice(index, 1)[0];
|
|
2248
|
+
selectedCards.push(card);
|
|
2249
|
+
}
|
|
2250
|
+
return selectedCards.map((c) => {
|
|
2251
|
+
const split = c.split("-");
|
|
2252
|
+
return {
|
|
2253
|
+
courseID: this.id,
|
|
2254
|
+
cardID: split[1],
|
|
2255
|
+
qualifiedID: `${split[0]}-${split[1]}`,
|
|
2256
|
+
contentSourceType: "course",
|
|
2257
|
+
contentSourceID: this.id,
|
|
2258
|
+
status: "new"
|
|
2259
|
+
};
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
// src/impl/couch/classroomDB.ts
|
|
2267
|
+
function getClassroomDB(classID, version) {
|
|
2268
|
+
const dbName = `classdb-${version}-${classID}`;
|
|
2269
|
+
logger.info(`Retrieving classroom db: ${dbName}`);
|
|
2270
|
+
return new pouchdb_setup_default(
|
|
2271
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2272
|
+
pouchDBincludeCredentialsConfig
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
async function getClassroomConfig(classID) {
|
|
2276
|
+
return await getClassroomDB(classID, "student").get(CLASSROOM_CONFIG);
|
|
2277
|
+
}
|
|
2278
|
+
var import_moment4, classroomLookupDBTitle, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB, TeacherClassroomDB, ClassroomLookupDB;
|
|
2279
|
+
var init_classroomDB = __esm({
|
|
2280
|
+
"src/impl/couch/classroomDB.ts"() {
|
|
2281
|
+
"use strict";
|
|
2282
|
+
init_factory();
|
|
2283
|
+
init_logger();
|
|
2284
|
+
import_moment4 = __toESM(require("moment"));
|
|
2285
|
+
init_pouchdb_setup();
|
|
2286
|
+
init_couch();
|
|
2287
|
+
init_courseDB();
|
|
2288
|
+
classroomLookupDBTitle = "classdb-lookup";
|
|
2289
|
+
CLASSROOM_CONFIG = "ClassroomConfig";
|
|
2290
|
+
ClassroomDBBase = class {
|
|
2291
|
+
_id;
|
|
2292
|
+
_db;
|
|
2293
|
+
_cfg;
|
|
2294
|
+
_initComplete = false;
|
|
2295
|
+
_content_prefix = "content";
|
|
2296
|
+
get _content_searchkeys() {
|
|
2297
|
+
return getStartAndEndKeys2(this._content_prefix);
|
|
2298
|
+
}
|
|
2299
|
+
async getAssignedContent() {
|
|
2300
|
+
logger.info(`Getting assigned content...`);
|
|
2301
|
+
const docRows = await this._db.allDocs({
|
|
2302
|
+
startkey: this._content_prefix,
|
|
2303
|
+
endkey: this._content_prefix + `\uFFF0`,
|
|
2304
|
+
include_docs: true
|
|
2305
|
+
});
|
|
2306
|
+
const ret = docRows.rows.map((row) => {
|
|
2307
|
+
return row.doc;
|
|
2308
|
+
});
|
|
2309
|
+
return ret;
|
|
2310
|
+
}
|
|
2311
|
+
getContentId(content) {
|
|
2312
|
+
if (content.type === "tag") {
|
|
2313
|
+
return `${this._content_prefix}-${content.courseID}-${content.tagID}`;
|
|
2314
|
+
} else {
|
|
2315
|
+
return `${this._content_prefix}-${content.courseID}`;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
get ready() {
|
|
2319
|
+
return this._initComplete;
|
|
2320
|
+
}
|
|
2321
|
+
getConfig() {
|
|
2322
|
+
return this._cfg;
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
StudentClassroomDB = class _StudentClassroomDB extends ClassroomDBBase {
|
|
2326
|
+
// private readonly _prefix: string = 'content';
|
|
2327
|
+
userMessages;
|
|
2328
|
+
_user;
|
|
2329
|
+
constructor(classID, user) {
|
|
2330
|
+
super();
|
|
2331
|
+
this._id = classID;
|
|
2332
|
+
this._user = user;
|
|
2333
|
+
}
|
|
2334
|
+
async init() {
|
|
2335
|
+
const dbName = `classdb-student-${this._id}`;
|
|
2336
|
+
this._db = new pouchdb_setup_default(
|
|
2337
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2338
|
+
pouchDBincludeCredentialsConfig
|
|
2339
|
+
);
|
|
2340
|
+
try {
|
|
2341
|
+
const cfg = await this._db.get(CLASSROOM_CONFIG);
|
|
2342
|
+
this._cfg = cfg;
|
|
2343
|
+
this.userMessages = this._db.changes({
|
|
2344
|
+
since: "now",
|
|
2345
|
+
live: true,
|
|
2346
|
+
include_docs: true
|
|
2347
|
+
});
|
|
2348
|
+
this._initComplete = true;
|
|
2349
|
+
return;
|
|
2350
|
+
} catch (e) {
|
|
2351
|
+
throw new Error(`Error in StudentClassroomDB constructor: ${JSON.stringify(e)}`);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
static async factory(classID, user) {
|
|
2355
|
+
const ret = new _StudentClassroomDB(classID, user);
|
|
2356
|
+
await ret.init();
|
|
2357
|
+
return ret;
|
|
2358
|
+
}
|
|
2359
|
+
setChangeFcn(f) {
|
|
2360
|
+
void this.userMessages.on("change", f);
|
|
2361
|
+
}
|
|
2362
|
+
async getPendingReviews() {
|
|
2363
|
+
const u = this._user;
|
|
2364
|
+
return (await u.getPendingReviews()).filter((r) => r.scheduledFor === "classroom" && r.schedulingAgentId === this._id).map((r) => {
|
|
2365
|
+
return {
|
|
2366
|
+
...r,
|
|
2367
|
+
qualifiedID: `${r.courseId}-${r.cardId}`,
|
|
2368
|
+
courseID: r.courseId,
|
|
2369
|
+
cardID: r.cardId,
|
|
2370
|
+
contentSourceType: "classroom",
|
|
2371
|
+
contentSourceID: this._id,
|
|
2372
|
+
reviewID: r._id,
|
|
2373
|
+
status: "review"
|
|
2374
|
+
};
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
async getNewCards() {
|
|
2378
|
+
const activeCards = await this._user.getActiveCards();
|
|
2379
|
+
const now = import_moment4.default.utc();
|
|
2380
|
+
const assigned = await this.getAssignedContent();
|
|
2381
|
+
const due = assigned.filter((c) => now.isAfter(import_moment4.default.utc(c.activeOn, REVIEW_TIME_FORMAT2)));
|
|
2382
|
+
logger.info(`Due content: ${JSON.stringify(due)}`);
|
|
2383
|
+
let ret = [];
|
|
2384
|
+
for (let i = 0; i < due.length; i++) {
|
|
2385
|
+
const content = due[i];
|
|
2386
|
+
if (content.type === "course") {
|
|
2387
|
+
const db = new CourseDB(content.courseID, async () => this._user);
|
|
2388
|
+
ret = ret.concat(await db.getNewCards());
|
|
2389
|
+
} else if (content.type === "tag") {
|
|
2390
|
+
const tagDoc = await getTag(content.courseID, content.tagID);
|
|
2391
|
+
ret = ret.concat(
|
|
2392
|
+
tagDoc.taggedCards.map((c) => {
|
|
2393
|
+
return {
|
|
2394
|
+
courseID: content.courseID,
|
|
2395
|
+
cardID: c,
|
|
2396
|
+
qualifiedID: `${content.courseID}-${c}`,
|
|
2397
|
+
contentSourceType: "classroom",
|
|
2398
|
+
contentSourceID: this._id,
|
|
2399
|
+
status: "new"
|
|
2400
|
+
};
|
|
2401
|
+
})
|
|
2402
|
+
);
|
|
2403
|
+
} else if (content.type === "card") {
|
|
2404
|
+
ret.push(await getCourseDB2(content.courseID).get(content.cardID));
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
logger.info(`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => c.qualifiedID)}`);
|
|
2408
|
+
return ret.filter((c) => {
|
|
2409
|
+
if (activeCards.some((ac) => c.qualifiedID.includes(ac))) {
|
|
2410
|
+
return false;
|
|
2411
|
+
} else {
|
|
2412
|
+
return true;
|
|
2413
|
+
}
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
TeacherClassroomDB = class _TeacherClassroomDB extends ClassroomDBBase {
|
|
2418
|
+
_stuDb;
|
|
2419
|
+
constructor(classID) {
|
|
2420
|
+
super();
|
|
2421
|
+
this._id = classID;
|
|
2422
|
+
}
|
|
2423
|
+
async init() {
|
|
2424
|
+
const dbName = `classdb-teacher-${this._id}`;
|
|
2425
|
+
const stuDbName = `classdb-student-${this._id}`;
|
|
2426
|
+
this._db = new pouchdb_setup_default(
|
|
2427
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2428
|
+
pouchDBincludeCredentialsConfig
|
|
2429
|
+
);
|
|
2430
|
+
this._stuDb = new pouchdb_setup_default(
|
|
2431
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + stuDbName,
|
|
2432
|
+
pouchDBincludeCredentialsConfig
|
|
2433
|
+
);
|
|
2434
|
+
try {
|
|
2435
|
+
return this._db.get(CLASSROOM_CONFIG).then((cfg) => {
|
|
2436
|
+
this._cfg = cfg;
|
|
2437
|
+
this._initComplete = true;
|
|
2438
|
+
}).then(() => {
|
|
2439
|
+
return;
|
|
2440
|
+
});
|
|
2441
|
+
} catch (e) {
|
|
2442
|
+
throw new Error(`Error in TeacherClassroomDB constructor: ${JSON.stringify(e)}`);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
static async factory(classID) {
|
|
2446
|
+
const ret = new _TeacherClassroomDB(classID);
|
|
2447
|
+
await ret.init();
|
|
2448
|
+
return ret;
|
|
2449
|
+
}
|
|
2450
|
+
async removeContent(content) {
|
|
2451
|
+
const contentID = this.getContentId(content);
|
|
2452
|
+
try {
|
|
2453
|
+
const doc = await this._db.get(contentID);
|
|
2454
|
+
await this._db.remove(doc);
|
|
2455
|
+
void this._db.replicate.to(this._stuDb, {
|
|
2456
|
+
doc_ids: [contentID]
|
|
2457
|
+
});
|
|
2458
|
+
} catch (error) {
|
|
2459
|
+
logger.error("Failed to remove content:", contentID, error);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
async assignContent(content) {
|
|
2463
|
+
let put;
|
|
2464
|
+
const id = this.getContentId(content);
|
|
2465
|
+
if (content.type === "tag") {
|
|
2466
|
+
put = await this._db.put({
|
|
2467
|
+
courseID: content.courseID,
|
|
2468
|
+
tagID: content.tagID,
|
|
2469
|
+
type: "tag",
|
|
2470
|
+
_id: id,
|
|
2471
|
+
assignedBy: content.assignedBy,
|
|
2472
|
+
assignedOn: import_moment4.default.utc(),
|
|
2473
|
+
activeOn: content.activeOn || import_moment4.default.utc()
|
|
2474
|
+
});
|
|
2475
|
+
} else {
|
|
2476
|
+
put = await this._db.put({
|
|
2477
|
+
courseID: content.courseID,
|
|
2478
|
+
type: "course",
|
|
2479
|
+
_id: id,
|
|
2480
|
+
assignedBy: content.assignedBy,
|
|
2481
|
+
assignedOn: import_moment4.default.utc(),
|
|
2482
|
+
activeOn: content.activeOn || import_moment4.default.utc()
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
if (put.ok) {
|
|
2486
|
+
void this._db.replicate.to(this._stuDb, {
|
|
2487
|
+
doc_ids: [id]
|
|
2488
|
+
});
|
|
2489
|
+
return true;
|
|
2490
|
+
} else {
|
|
2491
|
+
return false;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
};
|
|
2495
|
+
ClassroomLookupDB = () => new pouchdb_setup_default(ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + classroomLookupDBTitle, {
|
|
2496
|
+
skip_setup: true
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
|
|
2501
|
+
// src/core/interfaces/contentSource.ts
|
|
2502
|
+
function isReview(item) {
|
|
2503
|
+
const ret = item.status === "review" || item.status === "failed-review" || "reviewID" in item;
|
|
2504
|
+
return ret;
|
|
2505
|
+
}
|
|
2506
|
+
async function getStudySource(source, user) {
|
|
2507
|
+
if (source.type === "classroom") {
|
|
2508
|
+
return await StudentClassroomDB.factory(source.id, user);
|
|
2509
|
+
} else {
|
|
2510
|
+
return getDataLayer().getCourseDB(source.id);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
var init_contentSource = __esm({
|
|
2514
|
+
"src/core/interfaces/contentSource.ts"() {
|
|
2515
|
+
"use strict";
|
|
2516
|
+
init_factory();
|
|
2517
|
+
init_classroomDB();
|
|
2518
|
+
}
|
|
2519
|
+
});
|
|
2520
|
+
|
|
2521
|
+
// src/impl/couch/adminDB.ts
|
|
2522
|
+
var AdminDB;
|
|
2523
|
+
var init_adminDB = __esm({
|
|
2524
|
+
"src/impl/couch/adminDB.ts"() {
|
|
2525
|
+
"use strict";
|
|
2526
|
+
init_pouchdb_setup();
|
|
2527
|
+
init_factory();
|
|
2528
|
+
init_couch();
|
|
2529
|
+
init_classroomDB();
|
|
2530
|
+
init_courseLookupDB();
|
|
2531
|
+
init_logger();
|
|
2532
|
+
AdminDB = class {
|
|
2533
|
+
usersDB;
|
|
2534
|
+
constructor() {
|
|
2535
|
+
this.usersDB = new pouchdb_setup_default(
|
|
2536
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + "_users",
|
|
2537
|
+
pouchDBincludeCredentialsConfig
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
async getUsers() {
|
|
2541
|
+
return (await this.usersDB.allDocs({
|
|
2542
|
+
include_docs: true,
|
|
2543
|
+
...getStartAndEndKeys2("org.couchdb.user:")
|
|
2544
|
+
})).rows.map((r) => r.doc);
|
|
2545
|
+
}
|
|
2546
|
+
async getCourses() {
|
|
2547
|
+
const list = await CourseLookup.allCourses();
|
|
2548
|
+
return await Promise.all(
|
|
2549
|
+
list.map((c) => {
|
|
2550
|
+
return getCredentialledCourseConfig(c._id);
|
|
2551
|
+
})
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
async removeCourse(id) {
|
|
2555
|
+
const delResp = await CourseLookup.delete(id);
|
|
2556
|
+
const cfg = await getCredentialledCourseConfig(id);
|
|
2557
|
+
cfg.deleted = true;
|
|
2558
|
+
const isDeletedResp = await updateCredentialledCourseConfig(id, cfg);
|
|
2559
|
+
return {
|
|
2560
|
+
ok: delResp.ok && isDeletedResp.ok,
|
|
2561
|
+
id: delResp.id,
|
|
2562
|
+
rev: delResp.rev
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
async getClassrooms() {
|
|
2566
|
+
const uuids = (await ClassroomLookupDB().allDocs({
|
|
2567
|
+
include_docs: true
|
|
2568
|
+
})).rows.map((r) => r.doc.uuid);
|
|
2569
|
+
logger.debug(uuids.join(", "));
|
|
2570
|
+
const promisedCRDbs = [];
|
|
2571
|
+
for (let i = 0; i < uuids.length; i++) {
|
|
2572
|
+
try {
|
|
2573
|
+
const db = await TeacherClassroomDB.factory(uuids[i]);
|
|
2574
|
+
promisedCRDbs.push(db);
|
|
2575
|
+
} catch (e) {
|
|
2576
|
+
const err = e;
|
|
2577
|
+
if (err.error && err.error === "not_found") {
|
|
2578
|
+
logger.warn(`db ${uuids[i]} not found`);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
const dbs = await Promise.all(promisedCRDbs);
|
|
2583
|
+
return dbs.map((db) => {
|
|
2584
|
+
return {
|
|
2585
|
+
...db.getConfig(),
|
|
2586
|
+
_id: db._id
|
|
2587
|
+
};
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
});
|
|
2593
|
+
|
|
2594
|
+
// src/impl/couch/auth.ts
|
|
2595
|
+
async function getCurrentSession() {
|
|
2596
|
+
return new Promise((resolve, reject) => {
|
|
2597
|
+
const authXML = new XMLHttpRequest();
|
|
2598
|
+
authXML.withCredentials = true;
|
|
2599
|
+
authXML.onerror = (e) => {
|
|
2600
|
+
reject(new Error("Session check failed:", e));
|
|
2601
|
+
};
|
|
2602
|
+
authXML.addEventListener("load", () => {
|
|
2603
|
+
try {
|
|
2604
|
+
const resp = JSON.parse(authXML.responseText);
|
|
2605
|
+
resolve(resp);
|
|
2606
|
+
} catch (e) {
|
|
2607
|
+
reject(e);
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
const url = `${ENV.COUCHDB_SERVER_PROTOCOL}://${ENV.COUCHDB_SERVER_URL}_session`;
|
|
2611
|
+
authXML.open("GET", url);
|
|
2612
|
+
authXML.send();
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
async function getLoggedInUsername() {
|
|
2616
|
+
try {
|
|
2617
|
+
const session = await getCurrentSession();
|
|
2618
|
+
if (session.userCtx.name && session.userCtx.name !== "") {
|
|
2619
|
+
return session.userCtx.name;
|
|
2620
|
+
}
|
|
2621
|
+
} catch (error) {
|
|
2622
|
+
logger.error("Failed to get session:", error);
|
|
2623
|
+
}
|
|
2624
|
+
return GuestUsername;
|
|
2625
|
+
}
|
|
2626
|
+
var init_auth = __esm({
|
|
2627
|
+
"src/impl/couch/auth.ts"() {
|
|
2628
|
+
"use strict";
|
|
2629
|
+
init_factory();
|
|
2630
|
+
init_types_legacy();
|
|
2631
|
+
init_logger();
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
|
|
2635
|
+
// src/impl/couch/CouchDBSyncStrategy.ts
|
|
2636
|
+
var import_common7, log4, CouchDBSyncStrategy;
|
|
2637
|
+
var init_CouchDBSyncStrategy = __esm({
|
|
2638
|
+
"src/impl/couch/CouchDBSyncStrategy.ts"() {
|
|
2639
|
+
"use strict";
|
|
2640
|
+
init_factory();
|
|
2641
|
+
init_types_legacy();
|
|
2642
|
+
init_logger();
|
|
2643
|
+
import_common7 = require("@vue-skuilder/common");
|
|
2644
|
+
init_common();
|
|
2645
|
+
init_pouchdb_setup();
|
|
2646
|
+
init_couch();
|
|
2647
|
+
init_auth();
|
|
2648
|
+
log4 = (s) => {
|
|
2649
|
+
logger.info(s);
|
|
2650
|
+
};
|
|
2651
|
+
CouchDBSyncStrategy = class {
|
|
2652
|
+
syncHandle;
|
|
2653
|
+
// Handle to cancel sync if needed
|
|
2654
|
+
setupRemoteDB(username) {
|
|
2655
|
+
if (username === GuestUsername || username.startsWith(GuestUsername)) {
|
|
2656
|
+
return getLocalUserDB(username);
|
|
2657
|
+
} else {
|
|
2658
|
+
return this.getUserDB(username);
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
startSync(localDB, remoteDB) {
|
|
2662
|
+
if (localDB !== remoteDB) {
|
|
2663
|
+
this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
|
|
2664
|
+
live: true,
|
|
2665
|
+
retry: true
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
stopSync() {
|
|
2670
|
+
if (this.syncHandle) {
|
|
2671
|
+
this.syncHandle.cancel();
|
|
2672
|
+
this.syncHandle = void 0;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
canCreateAccount() {
|
|
2676
|
+
return true;
|
|
2677
|
+
}
|
|
2678
|
+
canAuthenticate() {
|
|
2679
|
+
return true;
|
|
2680
|
+
}
|
|
2681
|
+
async createAccount(username, password) {
|
|
2682
|
+
try {
|
|
2683
|
+
const signupRequest = await this.getRemoteCouchRootDB().signUp(username, password);
|
|
2684
|
+
if (signupRequest.ok) {
|
|
2685
|
+
log4(`CREATEACCOUNT: Successfully created account for ${username}`);
|
|
2686
|
+
try {
|
|
2687
|
+
const logoutResult = await this.getRemoteCouchRootDB().logOut();
|
|
2688
|
+
log4(`CREATEACCOUNT: logged out: ${logoutResult.ok}`);
|
|
2689
|
+
} catch {
|
|
2690
|
+
}
|
|
2691
|
+
const loginResult = await this.getRemoteCouchRootDB().logIn(username, password);
|
|
2692
|
+
log4(`CREATEACCOUNT: logged in as new user: ${loginResult.ok}`);
|
|
2693
|
+
if (loginResult.ok) {
|
|
2694
|
+
return {
|
|
2695
|
+
status: import_common7.Status.ok,
|
|
2696
|
+
error: void 0
|
|
2697
|
+
};
|
|
2698
|
+
} else {
|
|
2699
|
+
return {
|
|
2700
|
+
status: import_common7.Status.error,
|
|
2701
|
+
error: "Failed to log in after account creation"
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
} else {
|
|
2705
|
+
logger.warn(`Signup not OK: ${JSON.stringify(signupRequest)}`);
|
|
2706
|
+
return {
|
|
2707
|
+
status: import_common7.Status.error,
|
|
2708
|
+
error: "Account creation failed"
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
} catch (e) {
|
|
2712
|
+
if (e.reason === "Document update conflict.") {
|
|
2713
|
+
return {
|
|
2714
|
+
status: import_common7.Status.error,
|
|
2715
|
+
error: "This username is taken!"
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
logger.error(`Error on signup: ${JSON.stringify(e)}`);
|
|
2719
|
+
return {
|
|
2720
|
+
status: import_common7.Status.error,
|
|
2721
|
+
error: e.message || "Unknown error during account creation"
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
async authenticate(username, password) {
|
|
2726
|
+
try {
|
|
2727
|
+
const loginResult = await this.getRemoteCouchRootDB().logIn(username, password);
|
|
2728
|
+
if (loginResult.ok) {
|
|
2729
|
+
log4(`Successfully logged in as ${username}`);
|
|
2730
|
+
return {
|
|
2731
|
+
ok: true
|
|
2732
|
+
};
|
|
2733
|
+
} else {
|
|
2734
|
+
log4(`Login failed for ${username}`);
|
|
2735
|
+
return {
|
|
2736
|
+
ok: false,
|
|
2737
|
+
error: "Invalid username or password"
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
} catch (error) {
|
|
2741
|
+
logger.error(`Authentication error for ${username}:`, error);
|
|
2742
|
+
return {
|
|
2743
|
+
ok: false,
|
|
2744
|
+
error: error.message || "Authentication failed"
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
async logout() {
|
|
2749
|
+
try {
|
|
2750
|
+
const result = await this.getRemoteCouchRootDB().logOut();
|
|
2751
|
+
return {
|
|
2752
|
+
ok: result.ok,
|
|
2753
|
+
error: result.ok ? void 0 : "Logout failed"
|
|
2754
|
+
};
|
|
2755
|
+
} catch (error) {
|
|
2756
|
+
logger.error("Logout error:", error);
|
|
2757
|
+
return {
|
|
2758
|
+
ok: false,
|
|
2759
|
+
error: error.message || "Logout failed"
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
async getCurrentUsername() {
|
|
2764
|
+
try {
|
|
2765
|
+
return await getLoggedInUsername();
|
|
2766
|
+
} catch {
|
|
2767
|
+
return GuestUsername;
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Get remote CouchDB root database for authentication operations
|
|
2772
|
+
*/
|
|
2773
|
+
getRemoteCouchRootDB() {
|
|
2774
|
+
const remoteStr = ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + "skuilder";
|
|
2775
|
+
try {
|
|
2776
|
+
return new pouchdb_setup_default(remoteStr, {
|
|
2777
|
+
skip_setup: true
|
|
2778
|
+
});
|
|
2779
|
+
} catch (error) {
|
|
2780
|
+
logger.error("Failed to initialize remote CouchDB connection:", error);
|
|
2781
|
+
throw new Error(`Failed to initialize CouchDB: ${JSON.stringify(error)}`);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
/**
|
|
2785
|
+
* Get remote user database for a specific user
|
|
2786
|
+
*/
|
|
2787
|
+
getUserDB(username) {
|
|
2788
|
+
const guestAccount = false;
|
|
2789
|
+
const hexName = hexEncode(username);
|
|
2790
|
+
const dbName = `userdb-${hexName}`;
|
|
2791
|
+
log4(`Fetching user database: ${dbName} (${username})`);
|
|
2792
|
+
const ret = new pouchdb_setup_default(
|
|
2793
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2794
|
+
pouchDBincludeCredentialsConfig
|
|
2795
|
+
);
|
|
2796
|
+
if (guestAccount) {
|
|
2797
|
+
updateGuestAccountExpirationDate(ret);
|
|
2798
|
+
}
|
|
2799
|
+
return ret;
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
});
|
|
2804
|
+
|
|
2805
|
+
// src/impl/couch/index.ts
|
|
2806
|
+
var couch_exports = {};
|
|
2807
|
+
__export(couch_exports, {
|
|
2808
|
+
AdminDB: () => AdminDB,
|
|
2809
|
+
CLASSROOM_CONFIG: () => CLASSROOM_CONFIG,
|
|
2810
|
+
ClassroomLookupDB: () => ClassroomLookupDB,
|
|
2811
|
+
CouchDBSyncStrategy: () => CouchDBSyncStrategy,
|
|
2812
|
+
CourseDB: () => CourseDB,
|
|
2813
|
+
CoursesDB: () => CoursesDB,
|
|
2814
|
+
REVIEW_PREFIX: () => REVIEW_PREFIX2,
|
|
2815
|
+
REVIEW_TIME_FORMAT: () => REVIEW_TIME_FORMAT2,
|
|
2816
|
+
StudentClassroomDB: () => StudentClassroomDB,
|
|
2817
|
+
TeacherClassroomDB: () => TeacherClassroomDB,
|
|
2818
|
+
addNote55: () => addNote55,
|
|
2819
|
+
addTagToCard: () => addTagToCard,
|
|
2820
|
+
createTag: () => createTag,
|
|
2821
|
+
deleteTag: () => deleteTag,
|
|
2822
|
+
filterAllDocsByPrefix: () => filterAllDocsByPrefix2,
|
|
2823
|
+
getAncestorTagIDs: () => getAncestorTagIDs,
|
|
2824
|
+
getAppliedTags: () => getAppliedTags,
|
|
2825
|
+
getChildTagStubs: () => getChildTagStubs,
|
|
2826
|
+
getClassroomConfig: () => getClassroomConfig,
|
|
2827
|
+
getClassroomDB: () => getClassroomDB,
|
|
2828
|
+
getCouchUserDB: () => getCouchUserDB,
|
|
2829
|
+
getCourseDB: () => getCourseDB2,
|
|
2830
|
+
getCourseDataShapes: () => getCourseDataShapes,
|
|
2831
|
+
getCourseDoc: () => getCourseDoc,
|
|
2832
|
+
getCourseDocs: () => getCourseDocs,
|
|
2833
|
+
getCourseQuestionTypes: () => getCourseQuestionTypes,
|
|
2834
|
+
getCourseTagStubs: () => getCourseTagStubs,
|
|
2835
|
+
getCredentialledCourseConfig: () => getCredentialledCourseConfig,
|
|
2836
|
+
getCredentialledDataShapes: () => getCredentialledDataShapes,
|
|
2837
|
+
getLatestVersion: () => getLatestVersion,
|
|
2838
|
+
getRandomCards: () => getRandomCards,
|
|
2839
|
+
getStartAndEndKeys: () => getStartAndEndKeys2,
|
|
2840
|
+
getStudySource: () => getStudySource,
|
|
2841
|
+
getTag: () => getTag,
|
|
2842
|
+
getTagID: () => getTagID,
|
|
2843
|
+
hexEncode: () => hexEncode2,
|
|
2844
|
+
isReview: () => isReview,
|
|
2845
|
+
localUserDB: () => localUserDB,
|
|
2846
|
+
pouchDBincludeCredentialsConfig: () => pouchDBincludeCredentialsConfig,
|
|
2847
|
+
removeTagFromCard: () => removeTagFromCard,
|
|
2848
|
+
scheduleCardReview: () => scheduleCardReview,
|
|
2849
|
+
updateCardElo: () => updateCardElo2,
|
|
2850
|
+
updateCredentialledCourseConfig: () => updateCredentialledCourseConfig,
|
|
2851
|
+
updateGuestAccountExpirationDate: () => updateGuestAccountExpirationDate2,
|
|
2852
|
+
updateTag: () => updateTag,
|
|
2853
|
+
usernameIsAvailable: () => usernameIsAvailable
|
|
2854
|
+
});
|
|
2855
|
+
module.exports = __toCommonJS(couch_exports);
|
|
2856
|
+
function hexEncode2(str) {
|
|
2857
|
+
let hex;
|
|
2858
|
+
let returnStr = "";
|
|
2859
|
+
for (let i = 0; i < str.length; i++) {
|
|
2860
|
+
hex = str.charCodeAt(i).toString(16);
|
|
2861
|
+
returnStr += ("000" + hex).slice(3);
|
|
2862
|
+
}
|
|
2863
|
+
return returnStr;
|
|
2864
|
+
}
|
|
2865
|
+
function getCouchDB(dbName) {
|
|
2866
|
+
return new pouchdb_setup_default(
|
|
2867
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2868
|
+
pouchDBincludeCredentialsConfig
|
|
2869
|
+
);
|
|
2870
|
+
}
|
|
2871
|
+
function getCourseDB2(courseID) {
|
|
2872
|
+
return new pouchdb_setup_default(
|
|
2873
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + "coursedb-" + courseID,
|
|
2874
|
+
pouchDBincludeCredentialsConfig
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
async function getLatestVersion() {
|
|
2878
|
+
try {
|
|
2879
|
+
const docs = await getCouchDB("version").allDocs({
|
|
2880
|
+
descending: true,
|
|
2881
|
+
limit: 1
|
|
2882
|
+
});
|
|
2883
|
+
if (docs && docs.rows && docs.rows[0]) {
|
|
2884
|
+
return docs.rows[0].id;
|
|
2885
|
+
} else {
|
|
2886
|
+
return "0.0.0";
|
|
2887
|
+
}
|
|
2888
|
+
} catch {
|
|
2889
|
+
return "-1";
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
async function usernameIsAvailable(username) {
|
|
2893
|
+
log(`Checking availability of ${username}`);
|
|
2894
|
+
const req = new XMLHttpRequest();
|
|
2895
|
+
const url = ENV.COUCHDB_SERVER_URL + "userdb-" + hexEncode2(username);
|
|
2896
|
+
req.open("HEAD", url, false);
|
|
2897
|
+
req.send();
|
|
2898
|
+
return req.status === 404;
|
|
2899
|
+
}
|
|
2900
|
+
function updateGuestAccountExpirationDate2(guestDB) {
|
|
2901
|
+
const currentTime = import_moment5.default.utc();
|
|
2902
|
+
const expirationDate = currentTime.add(2, "months").toISOString();
|
|
2903
|
+
void guestDB.get(expiryDocID).then((doc) => {
|
|
2904
|
+
return guestDB.put({
|
|
2905
|
+
_id: expiryDocID,
|
|
2906
|
+
_rev: doc._rev,
|
|
2907
|
+
date: expirationDate
|
|
2908
|
+
});
|
|
2909
|
+
}).catch(() => {
|
|
2910
|
+
return guestDB.put({
|
|
2911
|
+
_id: expiryDocID,
|
|
2912
|
+
date: expirationDate
|
|
2913
|
+
});
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
function getCourseDocs(courseID, docIDs, options = {}) {
|
|
2917
|
+
return getCourseDB2(courseID).allDocs({
|
|
2918
|
+
...options,
|
|
2919
|
+
keys: docIDs
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
function getCourseDoc(courseID, docID, options = {}) {
|
|
2923
|
+
return getCourseDB2(courseID).get(docID, options);
|
|
2924
|
+
}
|
|
2925
|
+
async function getRandomCards(courseIDs) {
|
|
2926
|
+
if (courseIDs.length === 0) {
|
|
2927
|
+
throw new Error(`getRandomCards:
|
|
2928
|
+
Attempted to get all cards from no courses!`);
|
|
2929
|
+
} else {
|
|
2930
|
+
const courseResults = await Promise.all(
|
|
2931
|
+
courseIDs.map((course) => {
|
|
2932
|
+
return getCourseDB2(course).find({
|
|
2933
|
+
selector: {
|
|
2934
|
+
docType: "CARD" /* CARD */
|
|
2935
|
+
},
|
|
2936
|
+
limit: 1e3
|
|
2937
|
+
});
|
|
2938
|
+
})
|
|
2939
|
+
);
|
|
2940
|
+
const ret = [];
|
|
2941
|
+
courseResults.forEach((courseCards, index) => {
|
|
2942
|
+
courseCards.docs.forEach((doc) => {
|
|
2943
|
+
ret.push(`${courseIDs[index]}-${doc._id}`);
|
|
2944
|
+
});
|
|
2945
|
+
});
|
|
2946
|
+
return ret;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
function getCouchUserDB(username) {
|
|
2950
|
+
const guestAccount = false;
|
|
2951
|
+
const hexName = hexEncode2(username);
|
|
2952
|
+
const dbName = `userdb-${hexName}`;
|
|
2953
|
+
log(`Fetching user database: ${dbName} (${username})`);
|
|
2954
|
+
const ret = new pouchdb_setup_default(
|
|
2955
|
+
ENV.COUCHDB_SERVER_PROTOCOL + "://" + ENV.COUCHDB_SERVER_URL + dbName,
|
|
2956
|
+
pouchDBincludeCredentialsConfig
|
|
2957
|
+
);
|
|
2958
|
+
if (guestAccount) {
|
|
2959
|
+
updateGuestAccountExpirationDate2(ret);
|
|
2960
|
+
}
|
|
2961
|
+
return ret;
|
|
2962
|
+
}
|
|
2963
|
+
function scheduleCardReview(review) {
|
|
2964
|
+
const now = import_moment5.default.utc();
|
|
2965
|
+
logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
|
|
2966
|
+
void getCouchUserDB(review.user).put({
|
|
2967
|
+
_id: REVIEW_PREFIX2 + review.time.format(REVIEW_TIME_FORMAT2),
|
|
2968
|
+
cardId: review.card_id,
|
|
2969
|
+
reviewTime: review.time,
|
|
2970
|
+
courseId: review.course_id,
|
|
2971
|
+
scheduledAt: now,
|
|
2972
|
+
scheduledFor: review.scheduledFor,
|
|
2973
|
+
schedulingAgentId: review.schedulingAgentId
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
function filterAllDocsByPrefix2(db, prefix, opts) {
|
|
2977
|
+
const options = {
|
|
2978
|
+
startkey: prefix,
|
|
2979
|
+
endkey: prefix + "\uFFF0",
|
|
2980
|
+
include_docs: true
|
|
2981
|
+
};
|
|
2982
|
+
if (opts) {
|
|
2983
|
+
Object.assign(options, opts);
|
|
2984
|
+
}
|
|
2985
|
+
return db.allDocs(options);
|
|
2986
|
+
}
|
|
2987
|
+
function getStartAndEndKeys2(key) {
|
|
2988
|
+
return {
|
|
2989
|
+
startkey: key,
|
|
2990
|
+
endkey: key + "\uFFF0"
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
var import_moment5, import_process, isBrowser, expiryDocID, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_PREFIX2, REVIEW_TIME_FORMAT2;
|
|
2994
|
+
var init_couch = __esm({
|
|
2995
|
+
"src/impl/couch/index.ts"() {
|
|
2996
|
+
init_factory();
|
|
2997
|
+
init_types_legacy();
|
|
2998
|
+
import_moment5 = __toESM(require("moment"));
|
|
2999
|
+
init_logger();
|
|
3000
|
+
init_pouchdb_setup();
|
|
3001
|
+
import_process = __toESM(require("process"));
|
|
3002
|
+
init_contentSource();
|
|
3003
|
+
init_adminDB();
|
|
3004
|
+
init_classroomDB();
|
|
3005
|
+
init_courseAPI();
|
|
3006
|
+
init_courseDB();
|
|
3007
|
+
init_CouchDBSyncStrategy();
|
|
3008
|
+
isBrowser = typeof window !== "undefined";
|
|
3009
|
+
if (isBrowser) {
|
|
3010
|
+
window.process = import_process.default;
|
|
3011
|
+
}
|
|
3012
|
+
expiryDocID = "GuestAccountExpirationDate";
|
|
3013
|
+
GUEST_LOCAL_DB = `userdb-${GuestUsername}`;
|
|
3014
|
+
localUserDB = new pouchdb_setup_default(GUEST_LOCAL_DB);
|
|
3015
|
+
pouchDBincludeCredentialsConfig = {
|
|
3016
|
+
fetch(url, opts) {
|
|
3017
|
+
opts.credentials = "include";
|
|
3018
|
+
return pouchdb_setup_default.fetch(url, opts);
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
REVIEW_PREFIX2 = "card_review_";
|
|
3022
|
+
REVIEW_TIME_FORMAT2 = "YYYY-MM-DD--kk:mm:ss-SSS";
|
|
3023
|
+
}
|
|
3024
|
+
});
|
|
3025
|
+
init_couch();
|
|
3026
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3027
|
+
0 && (module.exports = {
|
|
3028
|
+
AdminDB,
|
|
3029
|
+
CLASSROOM_CONFIG,
|
|
3030
|
+
ClassroomLookupDB,
|
|
3031
|
+
CouchDBSyncStrategy,
|
|
3032
|
+
CourseDB,
|
|
3033
|
+
CoursesDB,
|
|
3034
|
+
REVIEW_PREFIX,
|
|
3035
|
+
REVIEW_TIME_FORMAT,
|
|
3036
|
+
StudentClassroomDB,
|
|
3037
|
+
TeacherClassroomDB,
|
|
3038
|
+
addNote55,
|
|
3039
|
+
addTagToCard,
|
|
3040
|
+
createTag,
|
|
3041
|
+
deleteTag,
|
|
3042
|
+
filterAllDocsByPrefix,
|
|
3043
|
+
getAncestorTagIDs,
|
|
3044
|
+
getAppliedTags,
|
|
3045
|
+
getChildTagStubs,
|
|
3046
|
+
getClassroomConfig,
|
|
3047
|
+
getClassroomDB,
|
|
3048
|
+
getCouchUserDB,
|
|
3049
|
+
getCourseDB,
|
|
3050
|
+
getCourseDataShapes,
|
|
3051
|
+
getCourseDoc,
|
|
3052
|
+
getCourseDocs,
|
|
3053
|
+
getCourseQuestionTypes,
|
|
3054
|
+
getCourseTagStubs,
|
|
3055
|
+
getCredentialledCourseConfig,
|
|
3056
|
+
getCredentialledDataShapes,
|
|
3057
|
+
getLatestVersion,
|
|
3058
|
+
getRandomCards,
|
|
3059
|
+
getStartAndEndKeys,
|
|
3060
|
+
getStudySource,
|
|
3061
|
+
getTag,
|
|
3062
|
+
getTagID,
|
|
3063
|
+
hexEncode,
|
|
3064
|
+
isReview,
|
|
3065
|
+
localUserDB,
|
|
3066
|
+
pouchDBincludeCredentialsConfig,
|
|
3067
|
+
removeTagFromCard,
|
|
3068
|
+
scheduleCardReview,
|
|
3069
|
+
updateCardElo,
|
|
3070
|
+
updateCredentialledCourseConfig,
|
|
3071
|
+
updateGuestAccountExpirationDate,
|
|
3072
|
+
updateTag,
|
|
3073
|
+
usernameIsAvailable
|
|
3074
|
+
});
|
|
3075
|
+
//# sourceMappingURL=index.js.map
|