@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
@@ -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,7 @@
1
+ export interface PouchError {
2
+ status?: number;
3
+ name?: string;
4
+ reason?: string;
5
+ message?: string;
6
+ error?: string;
7
+ }
@@ -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
+ }