@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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import pouch from './pouchdb-setup';
|
|
2
|
+
import { ENV } from '@/factory';
|
|
3
|
+
import { logger } from '../../util/logger';
|
|
4
|
+
|
|
5
|
+
const courseLookupDBTitle = 'coursedb-lookup';
|
|
6
|
+
|
|
7
|
+
interface CourseLookupDoc {
|
|
8
|
+
_id: string;
|
|
9
|
+
_rev: string;
|
|
10
|
+
name: string;
|
|
11
|
+
disambiguator?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
logger.debug(`COURSELOOKUP FILE RUNNING`);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A Lookup table of existant courses. Each docID in this DB correspondes to a
|
|
18
|
+
* course database whose name is `coursedb-{docID}`
|
|
19
|
+
*/
|
|
20
|
+
export default class CourseLookup {
|
|
21
|
+
// [ ] this db should be read only for public, admin-only for write
|
|
22
|
+
// Cache for the PouchDB instance
|
|
23
|
+
private static _dbInstance: PouchDB.Database | null = null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Static getter for the PouchDB database instance.
|
|
27
|
+
* Connects using ENV variables and caches the instance.
|
|
28
|
+
* Throws an error if required ENV variables are not set.
|
|
29
|
+
*/
|
|
30
|
+
private static get _db(): PouchDB.Database {
|
|
31
|
+
// Return cached instance if available
|
|
32
|
+
if (this._dbInstance) {
|
|
33
|
+
return this._dbInstance;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Check required environment variables ---
|
|
37
|
+
if (ENV.COUCHDB_SERVER_URL === 'NOT_SET' || !ENV.COUCHDB_SERVER_URL) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
'CourseLookup.db: COUCHDB_SERVER_URL is not set. Ensure initializeDataLayer has been called with valid configuration.'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (ENV.COUCHDB_SERVER_PROTOCOL === 'NOT_SET' || !ENV.COUCHDB_SERVER_PROTOCOL) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'CourseLookup.db: COUCHDB_SERVER_PROTOCOL is not set. Ensure initializeDataLayer has been called with valid configuration.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Construct connection options ---
|
|
49
|
+
const dbUrl = `${ENV.COUCHDB_SERVER_PROTOCOL}://${ENV.COUCHDB_SERVER_URL}/${courseLookupDBTitle}`;
|
|
50
|
+
const options: PouchDB.Configuration.RemoteDatabaseConfiguration = {
|
|
51
|
+
skip_setup: true, // Keep the original option
|
|
52
|
+
// fetch: (url, opts) => { // Optional: Add for debugging network requests
|
|
53
|
+
// console.log('PouchDB fetch:', url, opts);
|
|
54
|
+
// return pouch.fetch(url, opts);
|
|
55
|
+
// }
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Add authentication if both username and password are provided
|
|
59
|
+
if (ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD) {
|
|
60
|
+
options.auth = {
|
|
61
|
+
username: ENV.COUCHDB_USERNAME,
|
|
62
|
+
password: ENV.COUCHDB_PASSWORD,
|
|
63
|
+
};
|
|
64
|
+
logger.info(`CourseLookup: Connecting to ${dbUrl} with authentication.`);
|
|
65
|
+
} else {
|
|
66
|
+
logger.info(`CourseLookup: Connecting to ${dbUrl} without authentication.`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Create and cache the PouchDB instance ---
|
|
70
|
+
try {
|
|
71
|
+
this._dbInstance = new pouch(dbUrl, options);
|
|
72
|
+
logger.info(`CourseLookup: Database instance created for ${courseLookupDBTitle}.`);
|
|
73
|
+
return this._dbInstance;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
logger.error(`CourseLookup: Failed to create PouchDB instance for ${dbUrl}`, error);
|
|
76
|
+
// Reset cache attempt on failure
|
|
77
|
+
this._dbInstance = null;
|
|
78
|
+
// Re-throw the error to indicate connection failure
|
|
79
|
+
throw new Error(
|
|
80
|
+
`CourseLookup: Failed to initialize database connection: ${
|
|
81
|
+
error instanceof Error ? error.message : String(error)
|
|
82
|
+
}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Adds a new course to the lookup database, and returns the courseID
|
|
89
|
+
* @param courseName
|
|
90
|
+
* @returns
|
|
91
|
+
*/
|
|
92
|
+
static async add(courseName: string): Promise<string> {
|
|
93
|
+
const resp = await CourseLookup._db.post({
|
|
94
|
+
name: courseName,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return resp.id;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Removes a course from the index
|
|
102
|
+
* @param courseID
|
|
103
|
+
*/
|
|
104
|
+
static async delete(courseID: string): Promise<PouchDB.Core.Response> {
|
|
105
|
+
const doc = await CourseLookup._db.get(courseID);
|
|
106
|
+
return await CourseLookup._db.remove(doc);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static async allCourses(): Promise<CourseLookupDoc[]> {
|
|
110
|
+
const resp = await CourseLookup._db.allDocs<CourseLookupDoc>({
|
|
111
|
+
include_docs: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return resp.rows.map((row) => row.doc!);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static async updateDisambiguator(
|
|
118
|
+
courseID: string,
|
|
119
|
+
disambiguator?: string
|
|
120
|
+
): Promise<PouchDB.Core.Response> {
|
|
121
|
+
const doc = await CourseLookup._db.get<CourseLookupDoc>(courseID);
|
|
122
|
+
doc.disambiguator = disambiguator;
|
|
123
|
+
return await CourseLookup._db.put(doc);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static async isCourse(courseID: string): Promise<boolean> {
|
|
127
|
+
try {
|
|
128
|
+
await CourseLookup._db.get(courseID);
|
|
129
|
+
return true;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
logger.info(`Courselookup failed:`, error);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { ENV } from '@/factory';
|
|
2
|
+
import { DocType, GuestUsername, log, SkuilderCourseData } from '../../core/types/types-legacy';
|
|
3
|
+
// import { getCurrentUser } from '../../stores/useAuthStore';
|
|
4
|
+
import moment, { Moment } from 'moment';
|
|
5
|
+
import { logger } from '@/util/logger';
|
|
6
|
+
|
|
7
|
+
import pouch from './pouchdb-setup';
|
|
8
|
+
|
|
9
|
+
import { ScheduledCard } from '@/core/types/user';
|
|
10
|
+
import process from 'process';
|
|
11
|
+
import { getUserDB } from './userDB';
|
|
12
|
+
|
|
13
|
+
const isBrowser = typeof window !== 'undefined';
|
|
14
|
+
|
|
15
|
+
if (isBrowser) {
|
|
16
|
+
(window as any).process = process; // required as a fix for pouchdb - see #18
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const expiryDocID: string = 'GuestAccountExpirationDate';
|
|
20
|
+
|
|
21
|
+
const GUEST_LOCAL_DB = `userdb-${GuestUsername}`;
|
|
22
|
+
export const localUserDB: PouchDB.Database = new pouch(GUEST_LOCAL_DB);
|
|
23
|
+
|
|
24
|
+
export function hexEncode(str: string): string {
|
|
25
|
+
let hex: string;
|
|
26
|
+
let returnStr: string = '';
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < str.length; i++) {
|
|
29
|
+
hex = str.charCodeAt(i).toString(16);
|
|
30
|
+
returnStr += ('000' + hex).slice(3);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return returnStr;
|
|
34
|
+
}
|
|
35
|
+
export const pouchDBincludeCredentialsConfig: PouchDB.Configuration.RemoteDatabaseConfiguration = {
|
|
36
|
+
fetch(url: string | Request, opts: RequestInit): Promise<Response> {
|
|
37
|
+
opts.credentials = 'include';
|
|
38
|
+
|
|
39
|
+
return (pouch as any).fetch(url, opts);
|
|
40
|
+
},
|
|
41
|
+
} as PouchDB.Configuration.RemoteDatabaseConfiguration;
|
|
42
|
+
|
|
43
|
+
function getCouchDB(dbName: string): PouchDB.Database {
|
|
44
|
+
return new pouch(
|
|
45
|
+
ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + dbName,
|
|
46
|
+
pouchDBincludeCredentialsConfig
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getCourseDB(courseID: string): PouchDB.Database {
|
|
51
|
+
// todo: keep a cache of opened courseDBs? need to benchmark this somehow
|
|
52
|
+
return new pouch(
|
|
53
|
+
ENV.COUCHDB_SERVER_PROTOCOL + '://' + ENV.COUCHDB_SERVER_URL + 'coursedb-' + courseID,
|
|
54
|
+
pouchDBincludeCredentialsConfig
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function getLatestVersion() {
|
|
59
|
+
try {
|
|
60
|
+
const docs = await getCouchDB('version').allDocs({
|
|
61
|
+
descending: true,
|
|
62
|
+
limit: 1,
|
|
63
|
+
});
|
|
64
|
+
if (docs && docs.rows && docs.rows[0]) {
|
|
65
|
+
return docs.rows[0].id;
|
|
66
|
+
} else {
|
|
67
|
+
return '0.0.0';
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
return '-1';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Checks the remote couchdb to see if a given username is available
|
|
76
|
+
* @param username The username to be checked
|
|
77
|
+
*/
|
|
78
|
+
export async function usernameIsAvailable(username: string): Promise<boolean> {
|
|
79
|
+
log(`Checking availability of ${username}`);
|
|
80
|
+
const req = new XMLHttpRequest();
|
|
81
|
+
const url = ENV.COUCHDB_SERVER_URL + 'userdb-' + hexEncode(username);
|
|
82
|
+
req.open('HEAD', url, false);
|
|
83
|
+
req.send();
|
|
84
|
+
return req.status === 404;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function updateGuestAccountExpirationDate(guestDB: PouchDB.Database<object>) {
|
|
88
|
+
const currentTime = moment.utc();
|
|
89
|
+
const expirationDate: string = currentTime.add(2, 'months').toISOString();
|
|
90
|
+
|
|
91
|
+
void guestDB
|
|
92
|
+
.get(expiryDocID)
|
|
93
|
+
.then((doc) => {
|
|
94
|
+
return guestDB.put({
|
|
95
|
+
_id: expiryDocID,
|
|
96
|
+
_rev: doc._rev,
|
|
97
|
+
date: expirationDate,
|
|
98
|
+
});
|
|
99
|
+
})
|
|
100
|
+
.catch(() => {
|
|
101
|
+
return guestDB.put({
|
|
102
|
+
_id: expiryDocID,
|
|
103
|
+
date: expirationDate,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getCourseDocs<T extends SkuilderCourseData>(
|
|
109
|
+
courseID: string,
|
|
110
|
+
docIDs: string[],
|
|
111
|
+
options: PouchDB.Core.AllDocsOptions = {}
|
|
112
|
+
) {
|
|
113
|
+
return getCourseDB(courseID).allDocs<T>({
|
|
114
|
+
...options,
|
|
115
|
+
keys: docIDs,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getCourseDoc<T extends SkuilderCourseData>(
|
|
120
|
+
courseID: string,
|
|
121
|
+
docID: PouchDB.Core.DocumentId,
|
|
122
|
+
options: PouchDB.Core.GetOptions = {}
|
|
123
|
+
): Promise<T> {
|
|
124
|
+
return getCourseDB(courseID).get<T>(docID, options);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Returns *all* cards from the parameter courses, in
|
|
129
|
+
* 'qualified' card format ("courseid-cardid")
|
|
130
|
+
*
|
|
131
|
+
* @param courseIDs A list of all course_ids to get cards from
|
|
132
|
+
*/
|
|
133
|
+
export async function getRandomCards(courseIDs: string[]) {
|
|
134
|
+
if (courseIDs.length === 0) {
|
|
135
|
+
throw new Error(`getRandomCards:\n\tAttempted to get all cards from no courses!`);
|
|
136
|
+
} else {
|
|
137
|
+
const courseResults = await Promise.all(
|
|
138
|
+
courseIDs.map((course) => {
|
|
139
|
+
return getCourseDB(course).find({
|
|
140
|
+
selector: {
|
|
141
|
+
docType: DocType.CARD,
|
|
142
|
+
},
|
|
143
|
+
limit: 1000,
|
|
144
|
+
});
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const ret: string[] = [];
|
|
149
|
+
courseResults.forEach((courseCards, index) => {
|
|
150
|
+
courseCards.docs.forEach((doc) => {
|
|
151
|
+
ret.push(`${courseIDs[index]}-${doc._id}`);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return ret;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const REVIEW_PREFIX: string = 'card_review_';
|
|
160
|
+
export const REVIEW_TIME_FORMAT: string = 'YYYY-MM-DD--kk:mm:ss-SSS';
|
|
161
|
+
|
|
162
|
+
export function scheduleCardReview(review: {
|
|
163
|
+
user: string;
|
|
164
|
+
course_id: string;
|
|
165
|
+
card_id: PouchDB.Core.DocumentId;
|
|
166
|
+
time: Moment;
|
|
167
|
+
scheduledFor: ScheduledCard['scheduledFor'];
|
|
168
|
+
schedulingAgentId: ScheduledCard['schedulingAgentId'];
|
|
169
|
+
}) {
|
|
170
|
+
const now = moment.utc();
|
|
171
|
+
logger.info(`Scheduling for review in: ${review.time.diff(now, 'h') / 24} days`);
|
|
172
|
+
void getUserDB(review.user).put<ScheduledCard>({
|
|
173
|
+
_id: REVIEW_PREFIX + review.time.format(REVIEW_TIME_FORMAT),
|
|
174
|
+
cardId: review.card_id,
|
|
175
|
+
reviewTime: review.time,
|
|
176
|
+
courseId: review.course_id,
|
|
177
|
+
scheduledAt: now,
|
|
178
|
+
scheduledFor: review.scheduledFor,
|
|
179
|
+
schedulingAgentId: review.schedulingAgentId,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function removeScheduledCardReview(user: string, reviewDocID: string) {
|
|
184
|
+
const db = getUserDB(user);
|
|
185
|
+
const reviewDoc = await db.get(reviewDocID);
|
|
186
|
+
db.remove(reviewDoc)
|
|
187
|
+
.then((res) => {
|
|
188
|
+
if (res.ok) {
|
|
189
|
+
log(`Removed Review Doc: ${reviewDocID}`);
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
.catch((err) => {
|
|
193
|
+
log(`Failed to remove Review Doc: ${reviewDocID},\n${JSON.stringify(err)}`);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function filterAllDocsByPrefix<T>(
|
|
198
|
+
db: PouchDB.Database,
|
|
199
|
+
prefix: string,
|
|
200
|
+
opts?: PouchDB.Core.AllDocsOptions
|
|
201
|
+
) {
|
|
202
|
+
// see couchdb docs 6.2.2:
|
|
203
|
+
// Guide to Views -> Views Collation -> String Ranges
|
|
204
|
+
const options: PouchDB.Core.AllDocsWithinRangeOptions = {
|
|
205
|
+
startkey: prefix,
|
|
206
|
+
endkey: prefix + '\ufff0',
|
|
207
|
+
include_docs: true,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (opts) {
|
|
211
|
+
Object.assign(options, opts);
|
|
212
|
+
}
|
|
213
|
+
return db.allDocs<T>(options);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function getStartAndEndKeys(key: string): {
|
|
217
|
+
startkey: string;
|
|
218
|
+
endkey: string;
|
|
219
|
+
} {
|
|
220
|
+
return {
|
|
221
|
+
startkey: key,
|
|
222
|
+
endkey: key + '\ufff0',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
//////////////////////
|
|
227
|
+
// Package exports
|
|
228
|
+
//////////////////////
|
|
229
|
+
|
|
230
|
+
export * from '../../core/interfaces/contentSource';
|
|
231
|
+
export * from './adminDB';
|
|
232
|
+
export * from './classroomDB';
|
|
233
|
+
export * from './courseAPI';
|
|
234
|
+
export * from './courseDB';
|
|
235
|
+
export * from './userDB';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import PouchDB from 'pouchdb';
|
|
2
|
+
import PouchDBFind from 'pouchdb-find';
|
|
3
|
+
import PouchDBAuth from '@nilock2/pouchdb-authentication';
|
|
4
|
+
|
|
5
|
+
// Register plugins
|
|
6
|
+
PouchDB.plugin(PouchDBFind);
|
|
7
|
+
PouchDB.plugin(PouchDBAuth);
|
|
8
|
+
|
|
9
|
+
// Configure PouchDB globally
|
|
10
|
+
PouchDB.defaults({
|
|
11
|
+
ajax: {
|
|
12
|
+
timeout: 60000,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export default PouchDB;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Loggable } from '../../util/Loggable';
|
|
2
|
+
import { logger } from '../../util/logger';
|
|
3
|
+
|
|
4
|
+
export type Update<T> = Partial<T> | ((x: T) => T);
|
|
5
|
+
|
|
6
|
+
export default class UpdateQueue extends Loggable {
|
|
7
|
+
_className: string = 'UpdateQueue';
|
|
8
|
+
private pendingUpdates: {
|
|
9
|
+
[index: string]: Update<unknown>[];
|
|
10
|
+
} = {};
|
|
11
|
+
private inprogressUpdates: {
|
|
12
|
+
[index: string]: boolean;
|
|
13
|
+
} = {};
|
|
14
|
+
|
|
15
|
+
private db: PouchDB.Database;
|
|
16
|
+
|
|
17
|
+
public update<T extends PouchDB.Core.Document<object>>(
|
|
18
|
+
id: PouchDB.Core.DocumentId,
|
|
19
|
+
update: Update<T>
|
|
20
|
+
) {
|
|
21
|
+
logger.debug(`Update requested on doc: ${id}`);
|
|
22
|
+
if (this.pendingUpdates[id]) {
|
|
23
|
+
this.pendingUpdates[id].push(update);
|
|
24
|
+
} else {
|
|
25
|
+
this.pendingUpdates[id] = [update];
|
|
26
|
+
}
|
|
27
|
+
return this.applyUpdates<T>(id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
constructor(db: PouchDB.Database) {
|
|
31
|
+
super();
|
|
32
|
+
// PouchDB.debug.enable('*');
|
|
33
|
+
this.db = db;
|
|
34
|
+
logger.debug(`UpdateQ initialized...`);
|
|
35
|
+
void this.db.info().then((i) => {
|
|
36
|
+
logger.debug(`db info: ${JSON.stringify(i)}`);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async applyUpdates<T extends PouchDB.Core.Document<object>>(id: string): Promise<T> {
|
|
41
|
+
logger.debug(`Applying updates on doc: ${id}`);
|
|
42
|
+
if (this.inprogressUpdates[id]) {
|
|
43
|
+
// console.log(`Updates in progress...`);
|
|
44
|
+
await this.db.info(); // stall for a round trip
|
|
45
|
+
// console.log(`Retrying...`);
|
|
46
|
+
return this.applyUpdates<T>(id);
|
|
47
|
+
} else {
|
|
48
|
+
if (this.pendingUpdates[id] && this.pendingUpdates[id].length > 0) {
|
|
49
|
+
this.inprogressUpdates[id] = true;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
let doc = await this.db.get<T>(id);
|
|
53
|
+
logger.debug(`Retrieved doc: ${id}`);
|
|
54
|
+
while (this.pendingUpdates[id].length !== 0) {
|
|
55
|
+
const update = this.pendingUpdates[id].splice(0, 1)[0];
|
|
56
|
+
if (typeof update === 'function') {
|
|
57
|
+
doc = { ...doc, ...update(doc) };
|
|
58
|
+
} else {
|
|
59
|
+
doc = {
|
|
60
|
+
...doc,
|
|
61
|
+
...update,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// for (const k in doc) {
|
|
66
|
+
// console.log(`${k}: ${typeof k}`);
|
|
67
|
+
// }
|
|
68
|
+
// console.log(`Applied updates to doc: ${JSON.stringify(doc)}`);
|
|
69
|
+
await this.db.put<T>(doc);
|
|
70
|
+
logger.debug(`Put doc: ${id}`);
|
|
71
|
+
|
|
72
|
+
if (this.pendingUpdates[id].length === 0) {
|
|
73
|
+
this.inprogressUpdates[id] = false;
|
|
74
|
+
delete this.inprogressUpdates[id];
|
|
75
|
+
} else {
|
|
76
|
+
return this.applyUpdates<T>(id);
|
|
77
|
+
}
|
|
78
|
+
return doc;
|
|
79
|
+
} catch (e) {
|
|
80
|
+
delete this.inprogressUpdates[id];
|
|
81
|
+
logger.error(`Error on attemped update: ${JSON.stringify(e)}`);
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
throw new Error(`Empty Updates Queue Triggered`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ScheduledCard, UserCourseSetting, UserCourseSettings, UsrCrsDataInterface } from '@/core';
|
|
2
|
+
import moment, { Moment } from 'moment';
|
|
3
|
+
import { getStartAndEndKeys, REVIEW_PREFIX, REVIEW_TIME_FORMAT } from '.';
|
|
4
|
+
import { CourseDB } from './courseDB';
|
|
5
|
+
import { User } from './userDB';
|
|
6
|
+
import { logger } from '../../util/logger';
|
|
7
|
+
|
|
8
|
+
export class UsrCrsData implements UsrCrsDataInterface {
|
|
9
|
+
private user: User;
|
|
10
|
+
private course: CourseDB;
|
|
11
|
+
private _courseId: string;
|
|
12
|
+
|
|
13
|
+
constructor(user: User, courseId: string) {
|
|
14
|
+
this.user = user;
|
|
15
|
+
this.course = new CourseDB(courseId, async () => this.user);
|
|
16
|
+
this._courseId = courseId;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public async getReviewsForcast(daysCount: number) {
|
|
20
|
+
const time = moment.utc().add(daysCount, 'days');
|
|
21
|
+
return this.getReviewstoDate(time);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public async getPendingReviews() {
|
|
25
|
+
const now = moment.utc();
|
|
26
|
+
return this.getReviewstoDate(now);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public async getScheduledReviewCount(): Promise<number> {
|
|
30
|
+
return (await this.getPendingReviews()).length;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public async getCourseSettings(): Promise<UserCourseSettings> {
|
|
34
|
+
const regDoc = await this.user.getCourseRegistrationsDoc();
|
|
35
|
+
const crsDoc = regDoc.courses.find((c) => c.courseID === this._courseId);
|
|
36
|
+
|
|
37
|
+
if (crsDoc && crsDoc.settings) {
|
|
38
|
+
return crsDoc.settings;
|
|
39
|
+
} else {
|
|
40
|
+
logger.warn(`no settings found during lookup on course ${this._courseId}`);
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
public updateCourseSettings(updates: UserCourseSetting[]): void {
|
|
45
|
+
void this.user.updateCourseSettings(this._courseId, updates);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async getReviewstoDate(targetDate: Moment) {
|
|
49
|
+
const keys = getStartAndEndKeys(REVIEW_PREFIX);
|
|
50
|
+
|
|
51
|
+
const reviews = await this.user.remote().allDocs<ScheduledCard>({
|
|
52
|
+
startkey: keys.startkey,
|
|
53
|
+
endkey: keys.endkey,
|
|
54
|
+
include_docs: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
logger.debug(
|
|
58
|
+
`Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
|
|
59
|
+
);
|
|
60
|
+
return reviews.rows
|
|
61
|
+
.filter((r) => {
|
|
62
|
+
if (r.id.startsWith(REVIEW_PREFIX)) {
|
|
63
|
+
const date = moment.utc(r.id.substr(REVIEW_PREFIX.length), REVIEW_TIME_FORMAT);
|
|
64
|
+
if (targetDate.isAfter(date)) {
|
|
65
|
+
if (this._courseId === undefined || r.doc!.courseId === this._courseId) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
.map((r) => r.doc!);
|
|
72
|
+
}
|
|
73
|
+
}
|