@vue-skuilder/db 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +26 -0
  2. package/dist/core/index.d.mts +3 -0
  3. package/dist/core/index.d.ts +3 -0
  4. package/dist/core/index.js +7906 -0
  5. package/dist/core/index.js.map +1 -0
  6. package/dist/core/index.mjs +7886 -0
  7. package/dist/core/index.mjs.map +1 -0
  8. package/dist/index-QMtzQI65.d.mts +734 -0
  9. package/dist/index-QMtzQI65.d.ts +734 -0
  10. package/dist/index.d.mts +133 -0
  11. package/dist/index.d.ts +133 -0
  12. package/dist/index.js +8726 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/index.mjs +8699 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/eslint.config.mjs +20 -0
  17. package/package.json +47 -0
  18. package/src/core/bulkImport/cardProcessor.ts +165 -0
  19. package/src/core/bulkImport/index.ts +2 -0
  20. package/src/core/bulkImport/types.ts +27 -0
  21. package/src/core/index.ts +9 -0
  22. package/src/core/interfaces/adminDB.ts +27 -0
  23. package/src/core/interfaces/classroomDB.ts +75 -0
  24. package/src/core/interfaces/contentSource.ts +64 -0
  25. package/src/core/interfaces/courseDB.ts +139 -0
  26. package/src/core/interfaces/dataLayerProvider.ts +46 -0
  27. package/src/core/interfaces/index.ts +7 -0
  28. package/src/core/interfaces/navigationStrategyManager.ts +46 -0
  29. package/src/core/interfaces/userDB.ts +183 -0
  30. package/src/core/navigators/elo.ts +76 -0
  31. package/src/core/navigators/index.ts +57 -0
  32. package/src/core/readme.md +9 -0
  33. package/src/core/types/contentNavigationStrategy.ts +21 -0
  34. package/src/core/types/db.ts +7 -0
  35. package/src/core/types/types-legacy.ts +155 -0
  36. package/src/core/types/user.ts +70 -0
  37. package/src/core/util/index.ts +42 -0
  38. package/src/factory.ts +86 -0
  39. package/src/impl/pouch/PouchDataLayerProvider.ts +102 -0
  40. package/src/impl/pouch/adminDB.ts +91 -0
  41. package/src/impl/pouch/auth.ts +48 -0
  42. package/src/impl/pouch/classroomDB.ts +306 -0
  43. package/src/impl/pouch/clientCache.ts +19 -0
  44. package/src/impl/pouch/courseAPI.ts +245 -0
  45. package/src/impl/pouch/courseDB.ts +772 -0
  46. package/src/impl/pouch/courseLookupDB.ts +135 -0
  47. package/src/impl/pouch/index.ts +235 -0
  48. package/src/impl/pouch/pouchdb-setup.ts +16 -0
  49. package/src/impl/pouch/types.ts +7 -0
  50. package/src/impl/pouch/updateQueue.ts +89 -0
  51. package/src/impl/pouch/user-course-relDB.ts +73 -0
  52. package/src/impl/pouch/userDB.ts +1097 -0
  53. package/src/index.ts +8 -0
  54. package/src/study/SessionController.ts +401 -0
  55. package/src/study/SpacedRepetition.ts +128 -0
  56. package/src/study/getCardDataShape.ts +34 -0
  57. package/src/study/index.ts +2 -0
  58. package/src/util/Loggable.ts +11 -0
  59. package/src/util/index.ts +1 -0
  60. package/src/util/logger.ts +55 -0
  61. package/tsconfig.json +12 -0
  62. package/tsup.config.ts +17 -0
package/src/factory.ts ADDED
@@ -0,0 +1,86 @@
1
+ // db/src/factory.ts
2
+
3
+ import { DataLayerProvider } from './core/interfaces';
4
+ import { logger } from './util/logger';
5
+
6
+ interface DBEnv {
7
+ COUCHDB_SERVER_URL: string; // URL of CouchDB server
8
+ COUCHDB_SERVER_PROTOCOL: string; // Protocol of CouchDB server (http or https)
9
+ COUCHDB_USERNAME?: string;
10
+ COUCHDB_PASSWORD?: string;
11
+ }
12
+
13
+ export const ENV: DBEnv = {
14
+ COUCHDB_SERVER_PROTOCOL: 'NOT_SET',
15
+ COUCHDB_SERVER_URL: 'NOT_SET',
16
+ };
17
+
18
+ // Configuration type for data layer initialization
19
+ export interface DataLayerConfig {
20
+ type: 'pouch' | 'static';
21
+ options: {
22
+ staticContentPath?: string; // Path to static content JSON files
23
+ localStoragePrefix?: string; // Prefix for IndexedDB storage names
24
+ COUCHDB_SERVER_URL?: string;
25
+ COUCHDB_SERVER_PROTOCOL?: string;
26
+ COUCHDB_USERNAME?: string;
27
+ COUCHDB_PASSWORD?: string;
28
+
29
+ COURSE_IDS?: string[];
30
+ };
31
+ }
32
+
33
+ // Singleton instance
34
+ let dataLayerInstance: DataLayerProvider | null = null;
35
+
36
+ /**
37
+ * Initialize the data layer with the specified configuration
38
+ */
39
+ export async function initializeDataLayer(config: DataLayerConfig): Promise<DataLayerProvider> {
40
+ if (dataLayerInstance) {
41
+ logger.warn('Data layer already initialized. Returning existing instance.');
42
+ return dataLayerInstance;
43
+ }
44
+
45
+ if (config.type === 'pouch') {
46
+ if (!config.options.COUCHDB_SERVER_URL || !config.options.COUCHDB_SERVER_PROTOCOL) {
47
+ throw new Error('Missing CouchDB server URL or protocol');
48
+ }
49
+ ENV.COUCHDB_SERVER_PROTOCOL = config.options.COUCHDB_SERVER_PROTOCOL;
50
+ ENV.COUCHDB_SERVER_URL = config.options.COUCHDB_SERVER_URL;
51
+ ENV.COUCHDB_USERNAME = config.options.COUCHDB_USERNAME;
52
+ ENV.COUCHDB_PASSWORD = config.options.COUCHDB_PASSWORD;
53
+
54
+ // Dynamic import to avoid loading both implementations when only one is needed
55
+ const { PouchDataLayerProvider } = await import('./impl/pouch/PouchDataLayerProvider');
56
+ dataLayerInstance = new PouchDataLayerProvider(config.options.COURSE_IDS);
57
+ } else {
58
+ throw new Error('static data layer not implemented');
59
+ // const { StaticDataLayerProvider } = await import('./impl/static/StaticDataLayerProvider');
60
+ // dataLayerInstance = new StaticDataLayerProvider(config.options);
61
+ }
62
+
63
+ await dataLayerInstance.initialize();
64
+ return dataLayerInstance;
65
+ }
66
+
67
+ /**
68
+ * Get the initialized data layer instance
69
+ * @throws Error if not initialized
70
+ */
71
+ export function getDataLayer(): DataLayerProvider {
72
+ if (!dataLayerInstance) {
73
+ throw new Error('Data layer not initialized. Call initializeDataLayer first.');
74
+ }
75
+ return dataLayerInstance;
76
+ }
77
+
78
+ /**
79
+ * Reset the data layer (primarily for testing)
80
+ */
81
+ export async function _resetDataLayer(): Promise<void> {
82
+ if (dataLayerInstance) {
83
+ await dataLayerInstance.teardown();
84
+ }
85
+ dataLayerInstance = null;
86
+ }
@@ -0,0 +1,102 @@
1
+ // db/src/impl/pouch/PouchDataLayerProvider.ts
2
+
3
+ import {
4
+ AdminDBInterface,
5
+ ClassroomDBInterface,
6
+ CoursesDBInterface,
7
+ CourseDBInterface,
8
+ DataLayerProvider,
9
+ UserDBInterface,
10
+ } from '../../core/interfaces';
11
+ import { logger } from '../../util/logger';
12
+
13
+ import { getLoggedInUsername } from './auth';
14
+
15
+ import { AdminDB } from './adminDB';
16
+ import { StudentClassroomDB, TeacherClassroomDB } from './classroomDB';
17
+ import { CourseDB, CoursesDB } from './courseDB';
18
+
19
+ import { User } from './userDB';
20
+
21
+ export class PouchDataLayerProvider implements DataLayerProvider {
22
+ private initialized: boolean = false;
23
+ private userDB!: UserDBInterface;
24
+ private currentUsername: string = '';
25
+
26
+ // the scoped list of courseIDs for a UI focused on a specific course
27
+ // or group of courses
28
+ private _courseIDs: string[] = [];
29
+
30
+ constructor(coursIDs?: string[]) {
31
+ if (coursIDs) {
32
+ this._courseIDs = coursIDs;
33
+ }
34
+ }
35
+
36
+ async initialize(): Promise<void> {
37
+ if (this.initialized) return;
38
+
39
+ // Check if we are in a Node.js environment
40
+ const isNodeEnvironment =
41
+ typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
42
+
43
+ if (isNodeEnvironment) {
44
+ logger.info(
45
+ 'PouchDataLayerProvider: Running in Node.js environment, skipping user session check and user DB initialization.'
46
+ );
47
+ } else {
48
+ // Assume browser-like environment, proceed with user session logic
49
+ try {
50
+ // Get the current username from session
51
+ this.currentUsername = await getLoggedInUsername();
52
+ logger.debug(`Current username: ${this.currentUsername}`);
53
+
54
+ // Create the user db instance if a username was found
55
+ if (this.currentUsername) {
56
+ this.userDB = await User.instance(this.currentUsername);
57
+ } else {
58
+ logger.warn('PouchDataLayerProvider: No logged-in username found in session.');
59
+ }
60
+ } catch (error) {
61
+ logger.error(
62
+ 'PouchDataLayerProvider: Error during user session check or user DB initialization:',
63
+ error
64
+ );
65
+ }
66
+ }
67
+
68
+ this.initialized = true;
69
+ }
70
+
71
+ async teardown(): Promise<void> {
72
+ // Close connections, etc.
73
+ this.initialized = false;
74
+ }
75
+
76
+ getUserDB(): UserDBInterface {
77
+ return this.userDB;
78
+ }
79
+
80
+ getCourseDB(courseId: string): CourseDBInterface {
81
+ return new CourseDB(courseId, async () => this.getUserDB());
82
+ }
83
+
84
+ getCoursesDB(): CoursesDBInterface {
85
+ return new CoursesDB(this._courseIDs);
86
+ }
87
+
88
+ async getClassroomDB(
89
+ classId: string,
90
+ type: 'student' | 'teacher'
91
+ ): Promise<ClassroomDBInterface> {
92
+ if (type === 'student') {
93
+ return await StudentClassroomDB.factory(classId, this.getUserDB());
94
+ } else {
95
+ return await TeacherClassroomDB.factory(classId);
96
+ }
97
+ }
98
+
99
+ getAdminDB(): AdminDBInterface {
100
+ return new AdminDB();
101
+ }
102
+ }
@@ -0,0 +1,91 @@
1
+ import pouch from './pouchdb-setup';
2
+ import { ENV } from '@/factory';
3
+ import {
4
+ pouchDBincludeCredentialsConfig,
5
+ getStartAndEndKeys,
6
+ getCredentialledCourseConfig,
7
+ updateCredentialledCourseConfig,
8
+ } from '.';
9
+ import { TeacherClassroomDB, ClassroomLookupDB } from './classroomDB';
10
+ import { PouchError } from './types';
11
+
12
+ import { AdminDBInterface } from '@/core';
13
+ import CourseLookup from './courseLookupDB';
14
+ import { logger } from '@/util/logger';
15
+
16
+ export class AdminDB implements AdminDBInterface {
17
+ private usersDB!: PouchDB.Database;
18
+
19
+ constructor() {
20
+ // [ ] execute a check here against credentials, and throw an error
21
+ // if the user is not an admin
22
+ this.usersDB = new pouch(
23
+ ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + '_users',
24
+ pouchDBincludeCredentialsConfig
25
+ );
26
+ }
27
+
28
+ public async getUsers() {
29
+ return (
30
+ await this.usersDB.allDocs({
31
+ include_docs: true,
32
+ ...getStartAndEndKeys('org.couchdb.user:'),
33
+ })
34
+ ).rows.map((r) => r.doc!);
35
+ }
36
+
37
+ public async getCourses() {
38
+ const list = await CourseLookup.allCourses();
39
+ return await Promise.all(
40
+ list.map((c) => {
41
+ return getCredentialledCourseConfig(c._id);
42
+ })
43
+ );
44
+ }
45
+ public async removeCourse(id: string) {
46
+ // remove the indexer
47
+ const delResp = await CourseLookup.delete(id);
48
+
49
+ // set the 'CourseConfig' to 'deleted'
50
+ const cfg = await getCredentialledCourseConfig(id);
51
+ cfg.deleted = true;
52
+ const isDeletedResp = await updateCredentialledCourseConfig(id, cfg);
53
+
54
+ return {
55
+ ok: delResp.ok && isDeletedResp.ok,
56
+ id: delResp.id,
57
+ rev: delResp.rev,
58
+ };
59
+ }
60
+
61
+ public async getClassrooms() {
62
+ // const joincodes =
63
+ const uuids = (
64
+ await ClassroomLookupDB().allDocs<{ uuid: string }>({
65
+ include_docs: true,
66
+ })
67
+ ).rows.map((r) => r.doc!.uuid);
68
+ logger.debug(uuids.join(', '));
69
+
70
+ const promisedCRDbs: TeacherClassroomDB[] = [];
71
+ for (let i = 0; i < uuids.length; i++) {
72
+ try {
73
+ const db = await TeacherClassroomDB.factory(uuids[i]);
74
+ promisedCRDbs.push(db);
75
+ } catch (e) {
76
+ const err = e as PouchError;
77
+ if (err.error && err.error === 'not_found') {
78
+ logger.warn(`db ${uuids[i]} not found`);
79
+ }
80
+ }
81
+ }
82
+
83
+ const dbs = await Promise.all(promisedCRDbs);
84
+ return dbs.map((db) => {
85
+ return {
86
+ ...db.getConfig(),
87
+ _id: db._id,
88
+ };
89
+ });
90
+ }
91
+ }
@@ -0,0 +1,48 @@
1
+ import { ENV } from '@/factory';
2
+ import { GuestUsername } from '../../core/types/types-legacy';
3
+ import { logger } from '@/util/logger';
4
+
5
+ interface SessionResponse {
6
+ info: unknown;
7
+ ok: boolean;
8
+ userCtx: {
9
+ name: string;
10
+ roles: string[];
11
+ };
12
+ }
13
+
14
+ export async function getCurrentSession(): Promise<SessionResponse> {
15
+ return new Promise((resolve, reject) => {
16
+ const authXML = new XMLHttpRequest();
17
+ authXML.withCredentials = true;
18
+
19
+ authXML.onerror = (e): void => {
20
+ reject(new Error('Session check failed:', e));
21
+ };
22
+
23
+ authXML.addEventListener('load', () => {
24
+ try {
25
+ const resp: SessionResponse = JSON.parse(authXML.responseText);
26
+ resolve(resp);
27
+ } catch (e) {
28
+ reject(e);
29
+ }
30
+ });
31
+
32
+ const url = `${ENV.COUCHDB_SERVER_PROTOCOL}://${ENV.COUCHDB_SERVER_URL}_session`;
33
+ authXML.open('GET', url);
34
+ authXML.send();
35
+ });
36
+ }
37
+
38
+ export async function getLoggedInUsername(): Promise<string> {
39
+ try {
40
+ const session = await getCurrentSession();
41
+ if (session.userCtx.name && session.userCtx.name !== '') {
42
+ return session.userCtx.name;
43
+ }
44
+ } catch (error) {
45
+ logger.error('Failed to get session:', error);
46
+ }
47
+ return GuestUsername;
48
+ }
@@ -0,0 +1,306 @@
1
+ import {
2
+ StudyContentSource,
3
+ StudySessionNewItem,
4
+ StudySessionReviewItem,
5
+ } from '@/core/interfaces/contentSource';
6
+ import { ClassroomConfig } from '@vue-skuilder/common';
7
+ import { ENV } from '@/factory';
8
+ import { logger } from '@/util/logger';
9
+ import moment from 'moment';
10
+ import pouch from './pouchdb-setup';
11
+ import {
12
+ getCourseDB,
13
+ getStartAndEndKeys,
14
+ pouchDBincludeCredentialsConfig,
15
+ REVIEW_TIME_FORMAT,
16
+ } from '.';
17
+ import { CourseDB, getTag } from './courseDB';
18
+
19
+ import { UserDBInterface } from '@/core';
20
+ import {
21
+ AssignedContent,
22
+ AssignedCourse,
23
+ AssignedTag,
24
+ StudentClassroomDBInterface,
25
+ TeacherClassroomDBInterface,
26
+ } from '@/core/interfaces/classroomDB';
27
+ import { ScheduledCard } from '@/core/types/user';
28
+
29
+ const classroomLookupDBTitle = 'classdb-lookup';
30
+ export const CLASSROOM_CONFIG = 'ClassroomConfig';
31
+
32
+ export type ClassroomMessage = object;
33
+
34
+ abstract class ClassroomDBBase {
35
+ public _id!: string;
36
+ protected _db!: PouchDB.Database;
37
+ protected _cfg!: ClassroomConfig;
38
+ protected _initComplete: boolean = false;
39
+
40
+ protected readonly _content_prefix: string = 'content';
41
+ protected get _content_searchkeys() {
42
+ return getStartAndEndKeys(this._content_prefix);
43
+ }
44
+
45
+ protected abstract init(): Promise<void>;
46
+
47
+ public async getAssignedContent(): Promise<AssignedContent[]> {
48
+ logger.info(`Getting assigned content...`);
49
+ // see couchdb docs 6.2.2:
50
+ // Guide to Views -> Views Collation -> String Ranges
51
+ const docRows = await this._db.allDocs<AssignedContent>({
52
+ startkey: this._content_prefix,
53
+ endkey: this._content_prefix + `\ufff0`,
54
+ include_docs: true,
55
+ });
56
+
57
+ const ret = docRows.rows.map((row) => {
58
+ return row.doc!;
59
+ });
60
+ // logger.info(`Assigned content: ${JSON.stringify(ret)}`);
61
+
62
+ return ret;
63
+ }
64
+
65
+ protected getContentId(content: AssignedContent): string {
66
+ if (content.type === 'tag') {
67
+ return `${this._content_prefix}-${content.courseID}-${content.tagID}`;
68
+ } else {
69
+ return `${this._content_prefix}-${content.courseID}`;
70
+ }
71
+ }
72
+
73
+ public get ready(): boolean {
74
+ return this._initComplete;
75
+ }
76
+ public getConfig(): ClassroomConfig {
77
+ return this._cfg;
78
+ }
79
+ }
80
+
81
+ export class StudentClassroomDB
82
+ extends ClassroomDBBase
83
+ implements StudyContentSource, StudentClassroomDBInterface
84
+ {
85
+ // private readonly _prefix: string = 'content';
86
+ private userMessages!: PouchDB.Core.Changes<object>;
87
+ private _user: UserDBInterface;
88
+
89
+ private constructor(classID: string, user: UserDBInterface) {
90
+ super();
91
+ this._id = classID;
92
+ this._user = user;
93
+ // init() is called explicitly in factory method, not in constructor
94
+ }
95
+
96
+ async init(): Promise<void> {
97
+ const dbName = `classdb-student-${this._id}`;
98
+ this._db = new pouch(
99
+ ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + dbName,
100
+ pouchDBincludeCredentialsConfig
101
+ );
102
+ try {
103
+ const cfg = await this._db.get<ClassroomConfig>(CLASSROOM_CONFIG);
104
+ this._cfg = cfg;
105
+ this.userMessages = this._db.changes({
106
+ since: 'now',
107
+ live: true,
108
+ include_docs: true,
109
+ });
110
+ this._initComplete = true;
111
+ return;
112
+ } catch (e) {
113
+ throw new Error(`Error in StudentClassroomDB constructor: ${JSON.stringify(e)}`);
114
+ }
115
+ }
116
+
117
+ public static async factory(classID: string, user: UserDBInterface): Promise<StudentClassroomDB> {
118
+ const ret = new StudentClassroomDB(classID, user);
119
+ await ret.init();
120
+ return ret;
121
+ }
122
+
123
+ public setChangeFcn(f: (value: unknown) => object): void {
124
+ // todo: make this into a view request, w/ the user's name attached
125
+ // todo: requires creating the view doc on classroom create in /express
126
+ void this.userMessages.on('change', f);
127
+ }
128
+
129
+ public async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
130
+ const u = this._user;
131
+ return (await u.getPendingReviews())
132
+ .filter((r) => r.scheduledFor === 'classroom' && r.schedulingAgentId === this._id)
133
+ .map((r) => {
134
+ return {
135
+ ...r,
136
+ qualifiedID: `${r.courseId}-${r.cardId}`,
137
+ courseID: r.courseId,
138
+ cardID: r.cardId,
139
+ contentSourceType: 'classroom',
140
+ contentSourceID: this._id,
141
+ reviewID: r._id,
142
+ status: 'review',
143
+ };
144
+ });
145
+ }
146
+
147
+ public async getNewCards(): Promise<StudySessionNewItem[]> {
148
+ const activeCards = await this._user.getActiveCards();
149
+ const now = moment.utc();
150
+ const assigned = await this.getAssignedContent();
151
+ const due = assigned.filter((c) => now.isAfter(moment.utc(c.activeOn, REVIEW_TIME_FORMAT)));
152
+
153
+ logger.info(`Due content: ${JSON.stringify(due)}`);
154
+
155
+ let ret: StudySessionNewItem[] = [];
156
+
157
+ for (let i = 0; i < due.length; i++) {
158
+ const content = due[i];
159
+
160
+ if (content.type === 'course') {
161
+ const db = new CourseDB(content.courseID, async () => this._user);
162
+ ret = ret.concat(await db.getNewCards());
163
+ } else if (content.type === 'tag') {
164
+ const tagDoc = await getTag(content.courseID, content.tagID);
165
+
166
+ ret = ret.concat(
167
+ tagDoc.taggedCards.map((c) => {
168
+ return {
169
+ courseID: content.courseID,
170
+ cardID: c,
171
+ qualifiedID: `${content.courseID}-${c}`,
172
+ contentSourceType: 'classroom',
173
+ contentSourceID: this._id,
174
+ status: 'new',
175
+ };
176
+ })
177
+ );
178
+ } else if (content.type === 'card') {
179
+ // returning card docs - not IDs
180
+ ret.push(await getCourseDB(content.courseID).get(content.cardID));
181
+ }
182
+ }
183
+
184
+ logger.info(`New Cards from classroom ${this._cfg.name}: ${ret.map((c) => c.qualifiedID)}`);
185
+
186
+ return ret.filter((c) => {
187
+ if (activeCards.some((ac) => c.qualifiedID.includes(ac))) {
188
+ return false;
189
+ } else {
190
+ return true;
191
+ }
192
+ });
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Interface for managing a classroom.
198
+ */
199
+ export class TeacherClassroomDB extends ClassroomDBBase implements TeacherClassroomDBInterface {
200
+ private _stuDb!: PouchDB.Database;
201
+
202
+ private constructor(classID: string) {
203
+ super();
204
+ this._id = classID;
205
+ }
206
+
207
+ async init(): Promise<void> {
208
+ const dbName = `classdb-teacher-${this._id}`;
209
+ const stuDbName = `classdb-student-${this._id}`;
210
+ this._db = new pouch(
211
+ ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + dbName,
212
+ pouchDBincludeCredentialsConfig
213
+ );
214
+ this._stuDb = new pouch(
215
+ ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + stuDbName,
216
+ pouchDBincludeCredentialsConfig
217
+ );
218
+ try {
219
+ return this._db
220
+ .get<ClassroomConfig>(CLASSROOM_CONFIG)
221
+ .then((cfg) => {
222
+ this._cfg = cfg;
223
+ this._initComplete = true;
224
+ })
225
+ .then(() => {
226
+ return;
227
+ });
228
+ } catch (e) {
229
+ throw new Error(`Error in TeacherClassroomDB constructor: ${JSON.stringify(e)}`);
230
+ }
231
+ }
232
+
233
+ public static async factory(classID: string): Promise<TeacherClassroomDB> {
234
+ const ret = new TeacherClassroomDB(classID);
235
+ await ret.init();
236
+ return ret;
237
+ }
238
+
239
+ public async removeContent(content: AssignedContent): Promise<void> {
240
+ const contentID = this.getContentId(content);
241
+
242
+ try {
243
+ const doc = await this._db.get(contentID);
244
+ await this._db.remove(doc);
245
+ void this._db.replicate.to(this._stuDb, {
246
+ doc_ids: [contentID],
247
+ });
248
+ } catch (error) {
249
+ logger.error('Failed to remove content:', contentID, error);
250
+ }
251
+ }
252
+
253
+ public async assignContent(content: AssignedContent): Promise<boolean> {
254
+ let put: PouchDB.Core.Response;
255
+ const id: string = this.getContentId(content);
256
+
257
+ if (content.type === 'tag') {
258
+ put = await this._db.put<AssignedTag>({
259
+ courseID: content.courseID,
260
+ tagID: content.tagID,
261
+ type: 'tag',
262
+ _id: id,
263
+ assignedBy: content.assignedBy,
264
+ assignedOn: moment.utc(),
265
+ activeOn: content.activeOn || moment.utc(),
266
+ });
267
+ } else {
268
+ put = await this._db.put<AssignedCourse>({
269
+ courseID: content.courseID,
270
+ type: 'course',
271
+ _id: id,
272
+ assignedBy: content.assignedBy,
273
+ assignedOn: moment.utc(),
274
+ activeOn: content.activeOn || moment.utc(),
275
+ });
276
+ }
277
+
278
+ if (put.ok) {
279
+ void this._db.replicate.to(this._stuDb, {
280
+ doc_ids: [id],
281
+ });
282
+ return true;
283
+ } else {
284
+ return false;
285
+ }
286
+ }
287
+ }
288
+
289
+ export const ClassroomLookupDB: () => PouchDB.Database = () =>
290
+ new pouch(ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + classroomLookupDBTitle, {
291
+ skip_setup: true,
292
+ });
293
+
294
+ export function getClassroomDB(classID: string, version: 'student' | 'teacher'): PouchDB.Database {
295
+ const dbName = `classdb-${version}-${classID}`;
296
+ logger.info(`Retrieving classroom db: ${dbName}`);
297
+
298
+ return new pouch(
299
+ ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + dbName,
300
+ pouchDBincludeCredentialsConfig
301
+ );
302
+ }
303
+
304
+ export async function getClassroomConfig(classID: string): Promise<ClassroomConfig> {
305
+ return await getClassroomDB(classID, 'student').get<ClassroomConfig>(CLASSROOM_CONFIG);
306
+ }
@@ -0,0 +1,19 @@
1
+ // todo: something good here instead
2
+
3
+ const CLIENT_CACHE: {
4
+ [k: string]: unknown;
5
+ } = {};
6
+
7
+ export async function GET_CACHED<K>(k: string, f?: (x: string) => Promise<K>): Promise<K> {
8
+ if (CLIENT_CACHE[k]) {
9
+ // console.log('returning a cached item');
10
+ return CLIENT_CACHE[k] as K;
11
+ }
12
+
13
+ CLIENT_CACHE[k] = f ? await f(k) : await GET_ITEM(k);
14
+ return GET_CACHED(k);
15
+ }
16
+
17
+ async function GET_ITEM(k: string): Promise<unknown> {
18
+ throw new Error(`No implementation found for GET_CACHED(${k})`);
19
+ }