@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.
- package/README.md +26 -0
- package/dist/core/index.d.mts +3 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +7906 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/index.mjs +7886 -0
- package/dist/core/index.mjs.map +1 -0
- package/dist/index-QMtzQI65.d.mts +734 -0
- package/dist/index-QMtzQI65.d.ts +734 -0
- package/dist/index.d.mts +133 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.js +8726 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +8699 -0
- package/dist/index.mjs.map +1 -0
- package/eslint.config.mjs +20 -0
- package/package.json +47 -0
- package/src/core/bulkImport/cardProcessor.ts +165 -0
- package/src/core/bulkImport/index.ts +2 -0
- package/src/core/bulkImport/types.ts +27 -0
- package/src/core/index.ts +9 -0
- package/src/core/interfaces/adminDB.ts +27 -0
- package/src/core/interfaces/classroomDB.ts +75 -0
- package/src/core/interfaces/contentSource.ts +64 -0
- package/src/core/interfaces/courseDB.ts +139 -0
- package/src/core/interfaces/dataLayerProvider.ts +46 -0
- package/src/core/interfaces/index.ts +7 -0
- package/src/core/interfaces/navigationStrategyManager.ts +46 -0
- package/src/core/interfaces/userDB.ts +183 -0
- package/src/core/navigators/elo.ts +76 -0
- package/src/core/navigators/index.ts +57 -0
- package/src/core/readme.md +9 -0
- package/src/core/types/contentNavigationStrategy.ts +21 -0
- package/src/core/types/db.ts +7 -0
- package/src/core/types/types-legacy.ts +155 -0
- package/src/core/types/user.ts +70 -0
- package/src/core/util/index.ts +42 -0
- package/src/factory.ts +86 -0
- package/src/impl/pouch/PouchDataLayerProvider.ts +102 -0
- package/src/impl/pouch/adminDB.ts +91 -0
- package/src/impl/pouch/auth.ts +48 -0
- package/src/impl/pouch/classroomDB.ts +306 -0
- package/src/impl/pouch/clientCache.ts +19 -0
- package/src/impl/pouch/courseAPI.ts +245 -0
- package/src/impl/pouch/courseDB.ts +772 -0
- package/src/impl/pouch/courseLookupDB.ts +135 -0
- package/src/impl/pouch/index.ts +235 -0
- package/src/impl/pouch/pouchdb-setup.ts +16 -0
- package/src/impl/pouch/types.ts +7 -0
- package/src/impl/pouch/updateQueue.ts +89 -0
- package/src/impl/pouch/user-course-relDB.ts +73 -0
- package/src/impl/pouch/userDB.ts +1097 -0
- package/src/index.ts +8 -0
- package/src/study/SessionController.ts +401 -0
- package/src/study/SpacedRepetition.ts +128 -0
- package/src/study/getCardDataShape.ts +34 -0
- package/src/study/index.ts +2 -0
- package/src/util/Loggable.ts +11 -0
- package/src/util/index.ts +1 -0
- package/src/util/logger.ts +55 -0
- package/tsconfig.json +12 -0
- 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
|
+
}
|