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. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { SQLiteDB } from '@classes/sqlitedb';
import { CoreUser } from '@features/user/services/user'; import { CoreUser } from '@features/user/services/user';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreSync } from '@services/sync'; import { CoreSync } from '@services/sync';
@ -38,6 +37,10 @@ import {
AddonModScormUserDataMap, AddonModScormUserDataMap,
AddonModScormWSSco, AddonModScormWSSco,
} from './scorm'; } 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. * Service to handle offline SCORM.
@ -47,8 +50,36 @@ export class AddonModScormOfflineProvider {
protected logger: CoreLogger; protected logger: CoreLogger;
protected tracksTables: LazyMap<
AsyncInstance<CoreDatabaseTable<AddonModScormTrackDBRecord, 'scormid' | 'userid' | 'attempt' | 'scoid' | 'element'>>
>;
protected attemptsTables: LazyMap<
AsyncInstance<CoreDatabaseTable<AddonModScormAttemptDBRecord, 'scormid' | 'userid' | 'attempt'>>
>;
constructor() { constructor() {
this.logger = CoreLogger.getInstance('AddonModScormOfflineProvider'); 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}`); 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. // Block the SCORM so it can't be synced.
CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scormId, 'changeAttemptNumber', site.id); CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scormId, 'changeAttemptNumber', site.id);
try { 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 { try {
// Now update the attempt number of all the tracks and mark them as not synced. // Now update the attempt number of all the tracks and mark them as not synced.
const newTrackData: Partial<AddonModScormTrackDBRecord> = { await this.tracksTables[site.id].updateWhere(
attempt: newAttempt, { attempt: newAttempt, synced: 0 },
synced: 0, currentAttemptConditions,
}; );
await db.updateRecords(TRACKS_TABLE_NAME, newTrackData, currentAttemptConditions);
} catch (error) { } catch (error) {
// Failed to update the tracks, restore the old attempt number. // 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; throw error;
} }
@ -148,7 +182,6 @@ export class AddonModScormOfflineProvider {
CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scorm.id, 'createNewAttempt', site.id); CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scorm.id, 'createNewAttempt', site.id);
// Create attempt in DB. // Create attempt in DB.
const db = site.getDb();
const entry: AddonModScormAttemptDBRecord = { const entry: AddonModScormAttemptDBRecord = {
scormid: scorm.id, scormid: scorm.id,
userid: userId, userid: userId,
@ -166,7 +199,7 @@ export class AddonModScormOfflineProvider {
} }
try { try {
await db.insertRecord(ATTEMPTS_TABLE_NAME, entry); await this.attemptsTables[site.id].insert(entry);
// Store all the data in userData. // Store all the data in userData.
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
@ -204,16 +237,15 @@ export class AddonModScormOfflineProvider {
this.logger.debug(`Delete offline attempt ${attempt} in SCORM ${scormId}`); this.logger.debug(`Delete offline attempt ${attempt} in SCORM ${scormId}`);
const db = site.getDb(); const conditions = {
const conditions: AddonModScormOfflineDBCommonData = {
scormid: scormId, scormid: scormId,
userid: userId, userid: userId,
attempt, attempt,
}; };
await Promise.all([ await Promise.all([
db.deleteRecords(ATTEMPTS_TABLE_NAME, conditions), this.attemptsTables[site.id].delete(conditions),
db.deleteRecords(TRACKS_TABLE_NAME, conditions), this.tracksTables[site.id].delete(conditions),
]); ]);
} }
@ -280,9 +312,9 @@ export class AddonModScormOfflineProvider {
* @returns Promise resolved when the offline attempts are retrieved. * @returns Promise resolved when the offline attempts are retrieved.
*/ */
async getAllAttempts(siteId?: string): Promise<AddonModScormOfflineAttempt[]> { 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)); return attempts.map((attempt) => this.parseAttempt(attempt));
} }
@ -300,7 +332,7 @@ export class AddonModScormOfflineProvider {
const site = await CoreSites.getSite(siteId); const site = await CoreSites.getSite(siteId);
userId = userId || site.getUserId(); userId = userId || site.getUserId();
const attemptRecord = await site.getDb().getRecord<AddonModScormAttemptDBRecord>(ATTEMPTS_TABLE_NAME, { const attemptRecord = await this.attemptsTables[site.id].getOneByPrimaryKey({
scormid: scormId, scormid: scormId,
userid: userId, userid: userId,
attempt, attempt,
@ -340,7 +372,7 @@ export class AddonModScormOfflineProvider {
const site = await CoreSites.getSite(siteId); const site = await CoreSites.getSite(siteId);
userId = userId || site.getUserId(); userId = userId || site.getUserId();
const attempts = await site.getDb().getRecords<AddonModScormAttemptDBRecord>(ATTEMPTS_TABLE_NAME, { const attempts = await this.attemptsTables[site.id].getMany({
scormid: scormId, scormid: scormId,
userid: userId, userid: userId,
}); });
@ -428,7 +460,7 @@ export class AddonModScormOfflineProvider {
conditions.synced = 1; 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); return this.parseTracks(tracks);
} }
@ -598,7 +630,6 @@ export class AddonModScormOfflineProvider {
userId = userId || site.getUserId(); userId = userId || site.getUserId();
const scoUserData = scoData?.userdata || {}; const scoUserData = scoData?.userdata || {};
const db = site.getDb();
let lessonStatusInserted = false; let lessonStatusInserted = false;
if (forceCompleted) { if (forceCompleted) {
@ -611,7 +642,16 @@ export class AddonModScormOfflineProvider {
if (scoUserData['cmi.core.lesson_status'] == 'incomplete') { if (scoUserData['cmi.core.lesson_status'] == 'incomplete') {
lessonStatusInserted = true; 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 { 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) { } catch (error) {
if (lessonStatusInserted) { if (lessonStatusInserted) {
// Rollback previous insert. // 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; 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. * 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. * 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 scoUserData = scoData?.userdata || {};
const db = CoreSites.getRequiredCurrentSite().getDb(); const siteId = CoreSites.getRequiredCurrentSite().id;
let lessonStatusInserted = false;
if (forceCompleted) { if (forceCompleted) {
if (element == 'cmi.core.lesson_status' && value == 'incomplete') { if (element == 'cmi.core.lesson_status' && value == 'incomplete') {
@ -741,11 +734,16 @@ export class AddonModScormOfflineProvider {
} }
if (element == 'cmi.core.score.raw') { if (element == 'cmi.core.score.raw') {
if (scoUserData['cmi.core.lesson_status'] == 'incomplete') { if (scoUserData['cmi.core.lesson_status'] == 'incomplete') {
lessonStatusInserted = true; this.tracksTables[siteId].syncInsert({
userid: userId,
if (!this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'completed', true)) { scormid: scormId,
return false; 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; return true;
} }
if (!this.insertTrackToDB(db, userId, scormId, scoId, attempt, element, value, true)) { this.tracksTables[siteId].syncInsert({
// Insert failed. userid: userId,
if (lessonStatusInserted) { scormid: scormId,
// Rollback previous insert. scoid: scoId,
this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'incomplete', true); attempt,
} element: element,
value: value === undefined ? null : JSON.stringify(value),
return false; timemodified: CoreTimeUtils.timestamp(),
} synced: 0,
});
return true; return true;
} }
@ -784,7 +783,7 @@ export class AddonModScormOfflineProvider {
this.logger.debug(`Mark SCO ${scoId} as synced for attempt ${attempt} in SCORM ${scormId}`); 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, scormid: scormId,
userid: userId, userid: userId,
attempt, attempt,
@ -971,10 +970,13 @@ export class AddonModScormOfflineProvider {
snapshot: JSON.stringify(this.removeDefaultData(userData)), snapshot: JSON.stringify(this.removeDefaultData(userData)),
}; };
await site.getDb().updateRecords(ATTEMPTS_TABLE_NAME, newData, <Partial<AddonModScormAttemptDBRecord>> { await this.attemptsTables[site.id].updateWhere(newData, {
scormid: scormId, sql: 'scormid = ? AND userid = ? AND attempt = ?',
userid: userId, sqlParams: [scormId, userId, attempt],
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); 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. * Update records matching the given conditions.
* *