MOBILE-4304 scorm: Update database usage

main
Noel De Martin 2024-01-17 10:44:24 +01:00
parent 38d0ad1aad
commit b6f32dfddd
2 changed files with 140 additions and 126 deletions

View File

@ -13,7 +13,6 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { SQLiteDB } from '@classes/sqlitedb';
import { CoreUser } from '@features/user/services/user';
import { CoreSites } from '@services/sites';
import { CoreSync } from '@services/sync';
@ -38,6 +37,10 @@ import {
AddonModScormUserDataMap,
AddonModScormWSSco,
} from './scorm';
import { lazyMap, LazyMap } from '@/core/utils/lazy-map';
import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance';
import { CoreDatabaseTable } from '@classes/database/database-table';
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
/**
* Service to handle offline SCORM.
@ -47,8 +50,36 @@ export class AddonModScormOfflineProvider {
protected logger: CoreLogger;
protected tracksTables: LazyMap<
AsyncInstance<CoreDatabaseTable<AddonModScormTrackDBRecord, 'scormid' | 'userid' | 'attempt' | 'scoid' | 'element'>>
>;
protected attemptsTables: LazyMap<
AsyncInstance<CoreDatabaseTable<AddonModScormAttemptDBRecord, 'scormid' | 'userid' | 'attempt'>>
>;
constructor() {
this.logger = CoreLogger.getInstance('AddonModScormOfflineProvider');
this.tracksTables = lazyMap(
siteId => asyncInstance(
() => CoreSites.getSiteTable(TRACKS_TABLE_NAME, {
siteId,
primaryKeyColumns: ['scormid', 'userid', 'attempt', 'scoid', 'element'],
config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
onDestroy: () => delete this.tracksTables[siteId],
}),
),
);
this.attemptsTables = lazyMap(
siteId => asyncInstance(
() => CoreSites.getSiteTable(ATTEMPTS_TABLE_NAME, {
siteId,
primaryKeyColumns: ['scormid', 'userid', 'attempt'],
config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
onDestroy: () => delete this.tracksTables[siteId],
}),
),
);
}
/**
@ -76,40 +107,43 @@ export class AddonModScormOfflineProvider {
this.logger.debug(`Change attempt number from ${attempt} to ${newAttempt} in SCORM ${scormId}`);
// Update the attempt number.
const db = site.getDb();
const currentAttemptConditions: AddonModScormOfflineDBCommonData = {
scormid: scormId,
userid: userId,
attempt,
};
const newAttemptConditions: AddonModScormOfflineDBCommonData = {
scormid: scormId,
userid: userId,
attempt: newAttempt,
};
const newAttemptData: Partial<AddonModScormAttemptDBRecord> = {
attempt: newAttempt,
timemodified: CoreTimeUtils.timestamp(),
};
// Block the SCORM so it can't be synced.
CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scormId, 'changeAttemptNumber', site.id);
try {
await db.updateRecords(ATTEMPTS_TABLE_NAME, newAttemptData, currentAttemptConditions);
const currentAttemptConditions = {
sql: 'scormid = ? AND userid = ? AND attempt = ?',
sqlParams: [scormId, userId, attempt],
js: (record: AddonModScormOfflineDBCommonData) =>
record.scormid === scormId &&
record.userid === userId &&
record.attempt === attempt,
};
await this.attemptsTables[site.id].updateWhere(
{ attempt: newAttempt, timemodified: CoreTimeUtils.timestamp() },
currentAttemptConditions,
);
try {
// Now update the attempt number of all the tracks and mark them as not synced.
const newTrackData: Partial<AddonModScormTrackDBRecord> = {
attempt: newAttempt,
synced: 0,
};
await db.updateRecords(TRACKS_TABLE_NAME, newTrackData, currentAttemptConditions);
await this.tracksTables[site.id].updateWhere(
{ attempt: newAttempt, synced: 0 },
currentAttemptConditions,
);
} catch (error) {
// Failed to update the tracks, restore the old attempt number.
await db.updateRecords(ATTEMPTS_TABLE_NAME, { attempt }, newAttemptConditions);
await this.attemptsTables[site.id].updateWhere(
{ attempt },
{
sql: 'scormid = ? AND userid = ? AND attempt = ?',
sqlParams: [scormId, userId, newAttempt],
js: (attempt) =>
attempt.scormid === scormId &&
attempt.userid === userId &&
attempt.attempt === newAttempt,
},
);
throw error;
}
@ -148,7 +182,6 @@ export class AddonModScormOfflineProvider {
CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scorm.id, 'createNewAttempt', site.id);
// Create attempt in DB.
const db = site.getDb();
const entry: AddonModScormAttemptDBRecord = {
scormid: scorm.id,
userid: userId,
@ -166,7 +199,7 @@ export class AddonModScormOfflineProvider {
}
try {
await db.insertRecord(ATTEMPTS_TABLE_NAME, entry);
await this.attemptsTables[site.id].insert(entry);
// Store all the data in userData.
const promises: Promise<void>[] = [];
@ -204,16 +237,15 @@ export class AddonModScormOfflineProvider {
this.logger.debug(`Delete offline attempt ${attempt} in SCORM ${scormId}`);
const db = site.getDb();
const conditions: AddonModScormOfflineDBCommonData = {
const conditions = {
scormid: scormId,
userid: userId,
attempt,
};
await Promise.all([
db.deleteRecords(ATTEMPTS_TABLE_NAME, conditions),
db.deleteRecords(TRACKS_TABLE_NAME, conditions),
this.attemptsTables[site.id].delete(conditions),
this.tracksTables[site.id].delete(conditions),
]);
}
@ -280,9 +312,9 @@ export class AddonModScormOfflineProvider {
* @returns Promise resolved when the offline attempts are retrieved.
*/
async getAllAttempts(siteId?: string): Promise<AddonModScormOfflineAttempt[]> {
const db = await CoreSites.getSiteDb(siteId);
siteId ??= CoreSites.getCurrentSiteId();
const attempts = await db.getAllRecords<AddonModScormAttemptDBRecord>(ATTEMPTS_TABLE_NAME);
const attempts = await this.attemptsTables[siteId].getMany();
return attempts.map((attempt) => this.parseAttempt(attempt));
}
@ -300,7 +332,7 @@ export class AddonModScormOfflineProvider {
const site = await CoreSites.getSite(siteId);
userId = userId || site.getUserId();
const attemptRecord = await site.getDb().getRecord<AddonModScormAttemptDBRecord>(ATTEMPTS_TABLE_NAME, {
const attemptRecord = await this.attemptsTables[site.id].getOneByPrimaryKey({
scormid: scormId,
userid: userId,
attempt,
@ -340,7 +372,7 @@ export class AddonModScormOfflineProvider {
const site = await CoreSites.getSite(siteId);
userId = userId || site.getUserId();
const attempts = await site.getDb().getRecords<AddonModScormAttemptDBRecord>(ATTEMPTS_TABLE_NAME, {
const attempts = await this.attemptsTables[site.id].getMany({
scormid: scormId,
userid: userId,
});
@ -428,7 +460,7 @@ export class AddonModScormOfflineProvider {
conditions.synced = 1;
}
const tracks = await site.getDb().getRecords<AddonModScormTrackDBRecord>(TRACKS_TABLE_NAME, conditions);
const tracks = await this.tracksTables[site.id].getMany(conditions);
return this.parseTracks(tracks);
}
@ -598,7 +630,6 @@ export class AddonModScormOfflineProvider {
userId = userId || site.getUserId();
const scoUserData = scoData?.userdata || {};
const db = site.getDb();
let lessonStatusInserted = false;
if (forceCompleted) {
@ -611,7 +642,16 @@ export class AddonModScormOfflineProvider {
if (scoUserData['cmi.core.lesson_status'] == 'incomplete') {
lessonStatusInserted = true;
await this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'completed');
await this.tracksTables[site.id].insert({
userid: userId,
scormid: scormId,
scoid: scoId,
attempt,
element: 'cmi.core.lesson_status',
value: JSON.stringify('completed'),
timemodified: CoreTimeUtils.timestamp(),
synced: 0,
});
}
}
}
@ -622,81 +662,35 @@ export class AddonModScormOfflineProvider {
}
try {
await this.insertTrackToDB(db, userId, scormId, scoId, attempt, element, value);
await this.tracksTables[site.id].insert({
userid: userId,
scormid: scormId,
scoid: scoId,
attempt,
element,
value: value === undefined ? null : JSON.stringify(value),
timemodified: CoreTimeUtils.timestamp(),
synced: 0,
});
} catch (error) {
if (lessonStatusInserted) {
// Rollback previous insert.
await this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'incomplete');
await this.tracksTables[site.id].insert({
userid: userId,
scormid: scormId,
scoid: scoId,
attempt,
element: 'cmi.core.lesson_status',
value: JSON.stringify('incomplete'),
timemodified: CoreTimeUtils.timestamp(),
synced: 0,
});
}
throw error;
}
}
/**
* Insert a track in the DB.
*
* @param db Site's DB.
* @param userId User ID.
* @param scormId SCORM ID.
* @param scoId SCO ID.
* @param attempt Attempt number.
* @param element Name of the element to insert.
* @param value Value of the element to insert.
* @param synchronous True if insert should NOT return a promise. Please use it only if synchronous is a must.
* @returns Returns a promise if synchronous=false, otherwise returns a boolean.
*/
protected insertTrackToDB(
db: SQLiteDB,
userId: number,
scormId: number,
scoId: number,
attempt: number,
element: string,
value: AddonModScormDataValue | undefined,
synchronous: true,
): boolean;
protected insertTrackToDB(
db: SQLiteDB,
userId: number,
scormId: number,
scoId: number,
attempt: number,
element: string,
value?: AddonModScormDataValue,
synchronous?: false,
): Promise<number>;
protected insertTrackToDB(
db: SQLiteDB,
userId: number,
scormId: number,
scoId: number,
attempt: number,
element: string,
value?: AddonModScormDataValue,
synchronous?: boolean,
): boolean | Promise<number> {
const entry: AddonModScormTrackDBRecord = {
userid: userId,
scormid: scormId,
scoid: scoId,
attempt,
element: element,
value: value === undefined ? null : JSON.stringify(value),
timemodified: CoreTimeUtils.timestamp(),
synced: 0,
};
if (synchronous) {
// The insert operation is always asynchronous, always return true.
db.insertRecord(TRACKS_TABLE_NAME, entry);
return true;
} else {
return db.insertRecord(TRACKS_TABLE_NAME, entry);
}
}
/**
* Insert a track in the offline tracks store, returning a synchronous value.
* Please use this function only if synchronous is a must. It's recommended to use insertTrack.
@ -730,8 +724,7 @@ export class AddonModScormOfflineProvider {
}
const scoUserData = scoData?.userdata || {};
const db = CoreSites.getRequiredCurrentSite().getDb();
let lessonStatusInserted = false;
const siteId = CoreSites.getRequiredCurrentSite().id;
if (forceCompleted) {
if (element == 'cmi.core.lesson_status' && value == 'incomplete') {
@ -741,11 +734,16 @@ export class AddonModScormOfflineProvider {
}
if (element == 'cmi.core.score.raw') {
if (scoUserData['cmi.core.lesson_status'] == 'incomplete') {
lessonStatusInserted = true;
if (!this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'completed', true)) {
return false;
}
this.tracksTables[siteId].syncInsert({
userid: userId,
scormid: scormId,
scoid: scoId,
attempt,
element: 'cmi.core.lesson_status',
value: JSON.stringify('completed'),
timemodified: CoreTimeUtils.timestamp(),
synced: 0,
});
}
}
}
@ -755,15 +753,16 @@ export class AddonModScormOfflineProvider {
return true;
}
if (!this.insertTrackToDB(db, userId, scormId, scoId, attempt, element, value, true)) {
// Insert failed.
if (lessonStatusInserted) {
// Rollback previous insert.
this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'incomplete', true);
}
return false;
}
this.tracksTables[siteId].syncInsert({
userid: userId,
scormid: scormId,
scoid: scoId,
attempt,
element: element,
value: value === undefined ? null : JSON.stringify(value),
timemodified: CoreTimeUtils.timestamp(),
synced: 0,
});
return true;
}
@ -784,7 +783,7 @@ export class AddonModScormOfflineProvider {
this.logger.debug(`Mark SCO ${scoId} as synced for attempt ${attempt} in SCORM ${scormId}`);
await site.getDb().updateRecords(TRACKS_TABLE_NAME, { synced: 1 }, <Partial<AddonModScormTrackDBRecord>> {
await this.tracksTables[site.id].update({ synced: 1 }, {
scormid: scormId,
userid: userId,
attempt,
@ -971,10 +970,13 @@ export class AddonModScormOfflineProvider {
snapshot: JSON.stringify(this.removeDefaultData(userData)),
};
await site.getDb().updateRecords(ATTEMPTS_TABLE_NAME, newData, <Partial<AddonModScormAttemptDBRecord>> {
scormid: scormId,
userid: userId,
attempt,
await this.attemptsTables[site.id].updateWhere(newData, {
sql: 'scormid = ? AND userid = ? AND attempt = ?',
sqlParams: [scormId, userId, attempt],
js: (record: AddonModScormOfflineDBCommonData) =>
record.scormid === scormId &&
record.userid === userId &&
record.attempt === attempt,
});
}

View File

@ -258,6 +258,18 @@ export class CoreDatabaseTable<
await this.database.insertRecord(this.tableName, record);
}
/**
* Insert a new record synchronously.
*
* @param record Database record.
*/
syncInsert(record: DBRecord): void {
// The current database architecture does not support synchronous operations,
// so calling this method will mean that errors will be silenced. Because of that,
// this should only be called if using the asynchronous alternatives is not possible.
this.insert(record);
}
/**
* Update records matching the given conditions.
*