// (C) Copyright 2015 Moodle Pty Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { SQLiteDB, SQLiteDBRecordValues } from './sqlitedb'; /** * Database table wrapper used to improve performance by caching all data in memory * for faster read operations. */ export abstract class CoreDatabaseTable< DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues, PrimaryKeyColumns extends keyof DBRecord = 'id', PrimaryKey extends GetPrimaryKey = GetPrimaryKey > { /** * Create an instance. * * @param db Database connection. * @returns Instance. */ static async create(this: CoreDatabaseTableConstructor, db: SQLiteDB): Promise { const instance = new this(db); await instance.initialize(); return instance; } protected db: SQLiteDB; protected data: Record; protected primaryKeys: string[] = ['id']; constructor(db: SQLiteDB) { this.db = db; this.data = {}; } /** * Find a record matching the given conditions. * * @param conditions Matching conditions. * @returns Database record. */ find(conditions: Partial): DBRecord | null { return Object.values(this.data).find(record => this.recordMatches(record, conditions)) ?? null; } /** * Find a record by its primary key. * * @param primaryKey Primary key. * @returns Database record. */ findByPrimaryKey(primaryKey: PrimaryKey): DBRecord | null { return this.data[this.serializePrimaryKey(primaryKey)] ?? null; } /** * Insert a new record. * * @param record Database record. */ async insert(record: DBRecord): Promise { await this.db.insertRecord(this.table, record); const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record)); this.data[primaryKey] = record; } /** * Delete records matching the given conditions. * * @param conditions Matching conditions. If this argument is missing, all records will be deleted. */ async delete(conditions?: Partial): Promise { if (!conditions) { await this.db.deleteRecords(this.table); this.data = {}; return; } await this.db.deleteRecords(this.table, conditions); Object.entries(this.data).forEach(([id, record]) => { if (!this.recordMatches(record, conditions)) { return; } delete this.data[id]; }); } /** * Delete a single record identified by its primary key. * * @param primaryKey Record primary key. */ async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise { await this.db.deleteRecords(this.table, primaryKey); delete this.data[this.serializePrimaryKey(primaryKey)]; } /** * Database table name. */ protected abstract get table(): string; /** * Initialize object by getting the current state of the database table. */ protected async initialize(): Promise { const records = await this.db.getRecords(this.table); this.data = records.reduce((data, record) => { const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record)); data[primaryKey] = record; return data; }, {}); } /** * Get an object with the columns representing the primary key of a database record. * * @param record Database record. * @returns Primary key column-value pairs. */ protected getPrimaryKeyFromRecord(record: DBRecord): PrimaryKey { return this.primaryKeys.reduce((primaryKey, column) => { primaryKey[column] = record[column]; return primaryKey; }, {} as Record) as PrimaryKey; } /** * Serialize a primary key with a string representation. * * @param primaryKey Database record primary key. * @returns Serialized primary key. */ protected serializePrimaryKey(primaryKey: PrimaryKey): string { return Object.values(primaryKey).map(value => String(value)).join('-'); } /** * Check whether a given record matches the given conditions. * * @param record Database record. * @param conditions Conditions. * @returns Whether the record matches the conditions. */ protected recordMatches(record: DBRecord, conditions: Partial): boolean { return !Object.entries(conditions).some(([column, value]) => record[column] !== value); } } /** * Generic type to match against any concrete database table type. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any type AnyCoreDatabaseTable = CoreDatabaseTable>; /** * Database table constructor. */ type CoreDatabaseTableConstructor = { new (db: SQLiteDB): T; }; /** * Infer primary key type from database record and columns types. */ type GetPrimaryKey = { [column in PrimaryKeyColumns]: DBRecord[column]; };