forked from EVOgeek/Vmeda.Online
		
	MOBILE-3977 core: Refactor database table wrapper
Decouple caching behaviour into subclasses using a configurable proxy
This commit is contained in:
		
							parent
							
								
									cb5508fc72
								
							
						
					
					
						commit
						808a242cbc
					
				@ -1,194 +0,0 @@
 | 
			
		||||
// (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<DBRecord, PrimaryKeyColumns> = GetPrimaryKey<DBRecord, PrimaryKeyColumns>
 | 
			
		||||
> {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create an instance.
 | 
			
		||||
     *
 | 
			
		||||
     * @param db Database connection.
 | 
			
		||||
     * @returns Instance.
 | 
			
		||||
     */
 | 
			
		||||
    static async create<This extends AnyCoreDatabaseTable>(this: CoreDatabaseTableConstructor<This>, db: SQLiteDB): Promise<This> {
 | 
			
		||||
        const instance = new this(db);
 | 
			
		||||
 | 
			
		||||
        await instance.initialize();
 | 
			
		||||
 | 
			
		||||
        return instance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected db: SQLiteDB;
 | 
			
		||||
    protected data: Record<string, DBRecord>;
 | 
			
		||||
    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>): 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<void> {
 | 
			
		||||
        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<DBRecord>): Promise<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        const records = await this.db.getRecords<DBRecord>(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<PrimaryKeyColumns, unknown>) 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<DBRecord>): 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<SQLiteDBRecordValues, string, Record<string, any>>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Database table constructor.
 | 
			
		||||
 */
 | 
			
		||||
type CoreDatabaseTableConstructor<T extends AnyCoreDatabaseTable> = {
 | 
			
		||||
    new (db: SQLiteDB): T;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Infer primary key type from database record and columns types.
 | 
			
		||||
 */
 | 
			
		||||
type GetPrimaryKey<DBRecord extends SQLiteDBRecordValues, PrimaryKeyColumns extends keyof DBRecord> = {
 | 
			
		||||
    [column in PrimaryKeyColumns]: DBRecord[column];
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										187
									
								
								src/core/classes/database/database-table-proxy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								src/core/classes/database/database-table-proxy.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,187 @@
 | 
			
		||||
// (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 { CorePromisedValue } from '@classes/promised-value';
 | 
			
		||||
import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb';
 | 
			
		||||
import { CoreDatabaseReducer, CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey } from './database-table';
 | 
			
		||||
import { CoreEagerDatabaseTable } from './eager-database-table';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Database table proxy used to route database interactions through different implementations.
 | 
			
		||||
 *
 | 
			
		||||
 * This class allows using a database wrapper with different optimization strategies that can be changed at runtime.
 | 
			
		||||
 */
 | 
			
		||||
export class CoreDatabaseTableProxy<
 | 
			
		||||
    DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues,
 | 
			
		||||
    PrimaryKeyColumn extends keyof DBRecord = 'id',
 | 
			
		||||
    PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn>
 | 
			
		||||
> extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> {
 | 
			
		||||
 | 
			
		||||
    protected config: CoreDatabaseConfiguration;
 | 
			
		||||
    protected target: CorePromisedValue<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>> = new CorePromisedValue();
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        config: Partial<CoreDatabaseConfiguration>,
 | 
			
		||||
        database: SQLiteDB,
 | 
			
		||||
        tableName: string,
 | 
			
		||||
        primaryKeyColumns?: PrimaryKeyColumn[],
 | 
			
		||||
    ) {
 | 
			
		||||
        super(database, tableName, primaryKeyColumns);
 | 
			
		||||
 | 
			
		||||
        this.config = { ...this.getConfigDefaults(), ...config };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async initialize(): Promise<void> {
 | 
			
		||||
        const target = this.createTarget();
 | 
			
		||||
 | 
			
		||||
        await target.initialize();
 | 
			
		||||
 | 
			
		||||
        this.target.resolve(target);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async all(conditions?: Partial<DBRecord>): Promise<DBRecord[]> {
 | 
			
		||||
        const target = await this.target;
 | 
			
		||||
 | 
			
		||||
        return target.all(conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async find(conditions: Partial<DBRecord>): Promise<DBRecord> {
 | 
			
		||||
        const target = await this.target;
 | 
			
		||||
 | 
			
		||||
        return target.find(conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async findByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> {
 | 
			
		||||
        const target = await this.target;
 | 
			
		||||
 | 
			
		||||
        return target.findByPrimaryKey(primaryKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async reduce<T>(reducer: CoreDatabaseReducer<DBRecord, T>, conditions?: CoreDatabaseConditions<DBRecord>): Promise<T> {
 | 
			
		||||
        const target = await this.target;
 | 
			
		||||
 | 
			
		||||
        return target.reduce<T>(reducer, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async insert(record: DBRecord): Promise<void> {
 | 
			
		||||
        const target = await this.target;
 | 
			
		||||
 | 
			
		||||
        return target.insert(record);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async update(updates: Partial<DBRecord>, conditions?: Partial<DBRecord>): Promise<void> {
 | 
			
		||||
        const target = await this.target;
 | 
			
		||||
 | 
			
		||||
        return target.update(updates, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async updateWhere(updates: Partial<DBRecord>, conditions: CoreDatabaseConditions<DBRecord>): Promise<void> {
 | 
			
		||||
        const target = await this.target;
 | 
			
		||||
 | 
			
		||||
        return target.updateWhere(updates, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async delete(conditions?: Partial<DBRecord>): Promise<void> {
 | 
			
		||||
        const target = await this.target;
 | 
			
		||||
 | 
			
		||||
        return target.delete(conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> {
 | 
			
		||||
        const target = await this.target;
 | 
			
		||||
 | 
			
		||||
        return target.deleteByPrimaryKey(primaryKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get default configuration values.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Config defaults.
 | 
			
		||||
     */
 | 
			
		||||
    protected getConfigDefaults(): CoreDatabaseConfiguration {
 | 
			
		||||
        return {
 | 
			
		||||
            cachingStrategy: CoreDatabaseCachingStrategy.None,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create proxy target.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Target instance.
 | 
			
		||||
     */
 | 
			
		||||
    protected createTarget(): CoreDatabaseTable<DBRecord, PrimaryKeyColumn> {
 | 
			
		||||
        return this.createTable(this.config.cachingStrategy);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a database table using the given caching strategy.
 | 
			
		||||
     *
 | 
			
		||||
     * @param cachingStrategy Caching strategy.
 | 
			
		||||
     * @returns Database table.
 | 
			
		||||
     */
 | 
			
		||||
    protected createTable(cachingStrategy: CoreDatabaseCachingStrategy): CoreDatabaseTable<DBRecord, PrimaryKeyColumn> {
 | 
			
		||||
        switch (cachingStrategy) {
 | 
			
		||||
            case CoreDatabaseCachingStrategy.Eager:
 | 
			
		||||
                return new CoreEagerDatabaseTable(this.database, this.tableName, this.primaryKeyColumns);
 | 
			
		||||
            case CoreDatabaseCachingStrategy.None:
 | 
			
		||||
                return new CoreDatabaseTable(this.database, this.tableName, this.primaryKeyColumns);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Database proxy configuration.
 | 
			
		||||
 */
 | 
			
		||||
export interface CoreDatabaseConfiguration {
 | 
			
		||||
    cachingStrategy: CoreDatabaseCachingStrategy;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Database caching strategies.
 | 
			
		||||
 */
 | 
			
		||||
export enum CoreDatabaseCachingStrategy {
 | 
			
		||||
    Eager = 'eager',
 | 
			
		||||
    None = 'none',
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										240
									
								
								src/core/classes/database/database-table.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								src/core/classes/database/database-table.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,240 @@
 | 
			
		||||
// (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, SQLiteDBRecordValue, SQLiteDBRecordValues } from '@classes/sqlitedb';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Wrapper used to interact with a database table.
 | 
			
		||||
 */
 | 
			
		||||
export class CoreDatabaseTable<
 | 
			
		||||
    DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues,
 | 
			
		||||
    PrimaryKeyColumn extends keyof DBRecord = 'id',
 | 
			
		||||
    PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn>
 | 
			
		||||
> {
 | 
			
		||||
 | 
			
		||||
    protected database: SQLiteDB;
 | 
			
		||||
    protected tableName: string;
 | 
			
		||||
    protected primaryKeyColumns: PrimaryKeyColumn[];
 | 
			
		||||
 | 
			
		||||
    constructor(database: SQLiteDB, tableName: string, primaryKeyColumns?: PrimaryKeyColumn[]) {
 | 
			
		||||
        this.database = database;
 | 
			
		||||
        this.tableName = tableName;
 | 
			
		||||
        this.primaryKeyColumns = primaryKeyColumns ?? ['id'] as PrimaryKeyColumn[];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get database connection.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Database connection.
 | 
			
		||||
     */
 | 
			
		||||
    getDatabase(): SQLiteDB {
 | 
			
		||||
        return this.database;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get table name.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Table name.
 | 
			
		||||
     */
 | 
			
		||||
    getTableName(): string {
 | 
			
		||||
        return this.tableName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get primary key columns.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Primary key columns.
 | 
			
		||||
     */
 | 
			
		||||
    getPrimaryKeyColumns(): PrimaryKeyColumn[] {
 | 
			
		||||
        return this.primaryKeyColumns.slice(0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize.
 | 
			
		||||
     */
 | 
			
		||||
    async initialize(): Promise<void> {
 | 
			
		||||
        // Nothing to initialize by default, override this method if necessary.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Destroy.
 | 
			
		||||
     */
 | 
			
		||||
    async destroy(): Promise<void> {
 | 
			
		||||
        // Nothing to destroy by default, override this method if necessary.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get records matching the given conditions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param conditions Matching conditions. If this argument is missing, all records in the table will be returned.
 | 
			
		||||
     * @returns Database records.
 | 
			
		||||
     */
 | 
			
		||||
    all(conditions?: Partial<DBRecord>): Promise<DBRecord[]> {
 | 
			
		||||
        return conditions
 | 
			
		||||
            ? this.database.getRecords(this.tableName, conditions)
 | 
			
		||||
            : this.database.getAllRecords(this.tableName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find one record matching the given conditions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param conditions Matching conditions.
 | 
			
		||||
     * @returns Database record.
 | 
			
		||||
     */
 | 
			
		||||
    find(conditions: Partial<DBRecord>): Promise<DBRecord> {
 | 
			
		||||
        return this.database.getRecord<DBRecord>(this.tableName, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find one record by its primary key.
 | 
			
		||||
     *
 | 
			
		||||
     * @param primaryKey Primary key.
 | 
			
		||||
     * @returns Database record.
 | 
			
		||||
     */
 | 
			
		||||
    findByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> {
 | 
			
		||||
        return this.database.getRecord<DBRecord>(this.tableName, primaryKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reduce some records into a single value.
 | 
			
		||||
     *
 | 
			
		||||
     * @param reducer Reducer functions in SQL and JavaScript.
 | 
			
		||||
     * @param conditions Matching conditions in SQL and JavaScript. If this argument is missing, all records in the table
 | 
			
		||||
     *                   will be used.
 | 
			
		||||
     * @returns Reduced value.
 | 
			
		||||
     */
 | 
			
		||||
    reduce<T>(reducer: CoreDatabaseReducer<DBRecord, T>, conditions?: CoreDatabaseConditions<DBRecord>): Promise<T> {
 | 
			
		||||
        return this.database.getFieldSql(
 | 
			
		||||
            `SELECT ${reducer.sql} FROM ${this.tableName} ${conditions?.sql ?? ''}`,
 | 
			
		||||
            conditions?.sqlParams,
 | 
			
		||||
        ) as unknown as Promise<T>;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Insert a new record.
 | 
			
		||||
     *
 | 
			
		||||
     * @param record Database record.
 | 
			
		||||
     */
 | 
			
		||||
    async insert(record: DBRecord): Promise<void> {
 | 
			
		||||
        await this.database.insertRecord(this.tableName, record);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update records matching the given conditions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param updates Record updates.
 | 
			
		||||
     * @param conditions Matching conditions. If this argument is missing, all records will be updated.
 | 
			
		||||
     */
 | 
			
		||||
    async update(updates: Partial<DBRecord>, conditions?: Partial<DBRecord>): Promise<void> {
 | 
			
		||||
        await this.database.updateRecords(this.tableName, updates, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update records matching the given conditions.
 | 
			
		||||
     *
 | 
			
		||||
     * This method should be used when it's necessary to apply complex conditions; the simple `update`
 | 
			
		||||
     * method should be favored otherwise for better performance.
 | 
			
		||||
     *
 | 
			
		||||
     * @param updates Record updates.
 | 
			
		||||
     * @param conditions Matching conditions in SQL and JavaScript.
 | 
			
		||||
     */
 | 
			
		||||
    async updateWhere(updates: Partial<DBRecord>, conditions: CoreDatabaseConditions<DBRecord>): Promise<void> {
 | 
			
		||||
        await this.database.updateRecordsWhere(this.tableName, updates, conditions.sql, conditions.sqlParams);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete records matching the given conditions.
 | 
			
		||||
     *
 | 
			
		||||
     * @param conditions Matching conditions. If this argument is missing, all records will be deleted.
 | 
			
		||||
     */
 | 
			
		||||
    async delete(conditions?: Partial<DBRecord>): Promise<void> {
 | 
			
		||||
        conditions
 | 
			
		||||
            ? await this.database.deleteRecords(this.tableName, conditions)
 | 
			
		||||
            : await this.database.deleteRecords(this.tableName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete a single record identified by its primary key.
 | 
			
		||||
     *
 | 
			
		||||
     * @param primaryKey Record primary key.
 | 
			
		||||
     */
 | 
			
		||||
    async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> {
 | 
			
		||||
        await this.database.deleteRecords(this.tableName, primaryKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the primary key from a database record.
 | 
			
		||||
     *
 | 
			
		||||
     * @param record Database record.
 | 
			
		||||
     * @returns Primary key.
 | 
			
		||||
     */
 | 
			
		||||
    protected getPrimaryKeyFromRecord(record: DBRecord): PrimaryKey {
 | 
			
		||||
        return this.primaryKeyColumns.reduce((primaryKey, column) => {
 | 
			
		||||
            primaryKey[column] = record[column];
 | 
			
		||||
 | 
			
		||||
            return primaryKey;
 | 
			
		||||
        }, {} as Record<PrimaryKeyColumn, unknown>) as PrimaryKey;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Serialize a primary key with a string representation.
 | 
			
		||||
     *
 | 
			
		||||
     * @param primaryKey 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 Matching conditions.
 | 
			
		||||
     * @returns Whether the record matches the conditions.
 | 
			
		||||
     */
 | 
			
		||||
    protected recordMatches(record: DBRecord, conditions: Partial<DBRecord>): boolean {
 | 
			
		||||
        return !Object.entries(conditions).some(([column, value]) => record[column] !== value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Infer primary key type from database record and primary key column types.
 | 
			
		||||
 */
 | 
			
		||||
export type GetDBRecordPrimaryKey<DBRecord extends SQLiteDBRecordValues, PrimaryKeyColumn extends keyof DBRecord> = {
 | 
			
		||||
    [column in PrimaryKeyColumn]: DBRecord[column];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Reducer used to accumulate a value from multiple records both in SQL and JavaScript.
 | 
			
		||||
 *
 | 
			
		||||
 * Both operations should be equivalent.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreDatabaseReducer<DBRecord, T> = {
 | 
			
		||||
    sql: string;
 | 
			
		||||
    js: (previousValue: T, record: DBRecord) => T;
 | 
			
		||||
    jsInitialValue: T;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Conditions to match database records both in SQL and JavaScript.
 | 
			
		||||
 *
 | 
			
		||||
 * Both conditions should be equivalent.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreDatabaseConditions<DBRecord> = {
 | 
			
		||||
    sql: string;
 | 
			
		||||
    sqlParams?: SQLiteDBRecordValue[];
 | 
			
		||||
    js: (record: DBRecord) => boolean;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										168
									
								
								src/core/classes/database/eager-database-table.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								src/core/classes/database/eager-database-table.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,168 @@
 | 
			
		||||
// (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 { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { SQLiteDBRecordValues } from '@classes/sqlitedb';
 | 
			
		||||
import { CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey, CoreDatabaseReducer } from './database-table';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Wrapper used to improve performance by caching all the records for faster read operations.
 | 
			
		||||
 *
 | 
			
		||||
 * This implementation works best for tables that don't have a lot of records and are read often; for tables with too many
 | 
			
		||||
 * records use CoreLazyDatabaseTable instead.
 | 
			
		||||
 */
 | 
			
		||||
export class CoreEagerDatabaseTable<
 | 
			
		||||
    DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues,
 | 
			
		||||
    PrimaryKeyColumn extends keyof DBRecord = 'id',
 | 
			
		||||
    PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn>
 | 
			
		||||
> extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> {
 | 
			
		||||
 | 
			
		||||
    protected records: Record<string, DBRecord> = {};
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async initialize(): Promise<void> {
 | 
			
		||||
        const records = await super.all();
 | 
			
		||||
 | 
			
		||||
        this.records = records.reduce((data, record) => {
 | 
			
		||||
            const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record));
 | 
			
		||||
 | 
			
		||||
            data[primaryKey] = record;
 | 
			
		||||
 | 
			
		||||
            return data;
 | 
			
		||||
        }, {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async all(conditions?: Partial<DBRecord>): Promise<DBRecord[]> {
 | 
			
		||||
        const records = Object.values(this.records);
 | 
			
		||||
 | 
			
		||||
        return conditions
 | 
			
		||||
            ? records.filter(record => this.recordMatches(record, conditions))
 | 
			
		||||
            : records;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async find(conditions: Partial<DBRecord>): Promise<DBRecord> {
 | 
			
		||||
        const record = Object.values(this.records).find(record => this.recordMatches(record, conditions)) ?? null;
 | 
			
		||||
 | 
			
		||||
        if (record === null) {
 | 
			
		||||
            throw new CoreError('No records found.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return record;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async findByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> {
 | 
			
		||||
        const record = this.records[this.serializePrimaryKey(primaryKey)] ?? null;
 | 
			
		||||
 | 
			
		||||
        if (record === null) {
 | 
			
		||||
            throw new CoreError('No records found.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return record;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async reduce<T>(reducer: CoreDatabaseReducer<DBRecord, T>, conditions?: CoreDatabaseConditions<DBRecord>): Promise<T> {
 | 
			
		||||
        return Object
 | 
			
		||||
            .values(this.records)
 | 
			
		||||
            .reduce(
 | 
			
		||||
                (result, record) => (!conditions || conditions.js(record)) ? reducer.js(result, record) : result,
 | 
			
		||||
                reducer.jsInitialValue,
 | 
			
		||||
            );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async insert(record: DBRecord): Promise<void> {
 | 
			
		||||
        await super.insert(record);
 | 
			
		||||
 | 
			
		||||
        const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record));
 | 
			
		||||
 | 
			
		||||
        this.records[primaryKey] = record;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async update(updates: Partial<DBRecord>, conditions?: Partial<DBRecord>): Promise<void> {
 | 
			
		||||
        await super.update(updates, conditions);
 | 
			
		||||
 | 
			
		||||
        for (const record of Object.values(this.records)) {
 | 
			
		||||
            if (conditions && !this.recordMatches(record, conditions)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Object.assign(record, updates);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async updateWhere(updates: Partial<DBRecord>, conditions: CoreDatabaseConditions<DBRecord>): Promise<void> {
 | 
			
		||||
        await super.updateWhere(updates, conditions);
 | 
			
		||||
 | 
			
		||||
        for (const record of Object.values(this.records)) {
 | 
			
		||||
            if (!conditions.js(record)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Object.assign(record, updates);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async delete(conditions?: Partial<DBRecord>): Promise<void> {
 | 
			
		||||
        await super.delete(conditions);
 | 
			
		||||
 | 
			
		||||
        if (!conditions) {
 | 
			
		||||
            this.records = {};
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Object.entries(this.records).forEach(([id, record]) => {
 | 
			
		||||
            if (!this.recordMatches(record, conditions)) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            delete this.records[id];
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> {
 | 
			
		||||
        await super.deleteByPrimaryKey(primaryKey);
 | 
			
		||||
 | 
			
		||||
        delete this.records[this.serializePrimaryKey(primaryKey)];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1206,4 +1206,4 @@ export type SQLiteDBQueryParams = {
 | 
			
		||||
    params: SQLiteDBRecordValue[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type SQLiteDBRecordValue = number | string;
 | 
			
		||||
export type SQLiteDBRecordValue = number | string;
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,11 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { mock } from '@/testing/utils';
 | 
			
		||||
import { CoreDatabaseTable } from '@classes/database-table';
 | 
			
		||||
import {
 | 
			
		||||
    CoreDatabaseCachingStrategy,
 | 
			
		||||
    CoreDatabaseConfiguration,
 | 
			
		||||
    CoreDatabaseTableProxy,
 | 
			
		||||
} from '@classes/database/database-table-proxy';
 | 
			
		||||
import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb';
 | 
			
		||||
 | 
			
		||||
interface User extends SQLiteDBRecordValues {
 | 
			
		||||
@ -22,30 +26,60 @@ interface User extends SQLiteDBRecordValues {
 | 
			
		||||
    surname: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class UsersTable extends CoreDatabaseTable<User> {
 | 
			
		||||
 | 
			
		||||
    protected table = 'users';
 | 
			
		||||
 | 
			
		||||
function userMatches(user: User, conditions: Partial<User>) {
 | 
			
		||||
    return !Object.entries(conditions).some(([column, value]) => user[column] !== value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('CoreDatabaseTable', () => {
 | 
			
		||||
function prepareStubs(config: Partial<CoreDatabaseConfiguration> = {}): [User[], SQLiteDB, CoreDatabaseTableProxy<User>] {
 | 
			
		||||
    const records: User[] = [];
 | 
			
		||||
    const database = mock<SQLiteDB>({
 | 
			
		||||
        getRecord: async <T>(_, conditions) => {
 | 
			
		||||
            const record = records.find(record => userMatches(record, conditions));
 | 
			
		||||
 | 
			
		||||
            if (!record) {
 | 
			
		||||
                throw new Error();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return record as unknown as T;
 | 
			
		||||
        },
 | 
			
		||||
        getRecords: async <T>(_, conditions) => records.filter(record => userMatches(record, conditions)) as unknown as T[],
 | 
			
		||||
        getAllRecords: async <T>() => records as unknown as T[],
 | 
			
		||||
        deleteRecords: async (_, conditions) => {
 | 
			
		||||
            const usersToDelete: User[] = [];
 | 
			
		||||
 | 
			
		||||
            for (const user of records) {
 | 
			
		||||
                if (conditions && !userMatches(user, conditions)) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                usersToDelete.push(user);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (const user of usersToDelete) {
 | 
			
		||||
                records.splice(records.indexOf(user), 1);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return usersToDelete.length;
 | 
			
		||||
        },
 | 
			
		||||
        insertRecord: async (_, user: User) => records.push(user) && 1,
 | 
			
		||||
    });
 | 
			
		||||
    const table = new CoreDatabaseTableProxy<User>(config, database, 'users');
 | 
			
		||||
 | 
			
		||||
    return [records, database, table];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('CoreDatabaseTable with eager caching', () => {
 | 
			
		||||
 | 
			
		||||
    let records: User[];
 | 
			
		||||
    let db: SQLiteDB;
 | 
			
		||||
    let database: SQLiteDB;
 | 
			
		||||
    let table: CoreDatabaseTableProxy<User>;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
        records = [];
 | 
			
		||||
        db = mock<SQLiteDB>({
 | 
			
		||||
            getRecords: async <T>() => records as unknown as T[],
 | 
			
		||||
            deleteRecords: async () => 0,
 | 
			
		||||
            insertRecord: async () => 0,
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.Eager }));
 | 
			
		||||
 | 
			
		||||
    it('reads all records on create', async () => {
 | 
			
		||||
        await UsersTable.create(db);
 | 
			
		||||
    it('reads all records on initialization', async () => {
 | 
			
		||||
        await table.initialize();
 | 
			
		||||
 | 
			
		||||
        expect(db.getRecords).toHaveBeenCalledWith('users');
 | 
			
		||||
        expect(database.getAllRecords).toHaveBeenCalledWith('users');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('finds items', async () => {
 | 
			
		||||
@ -55,27 +89,29 @@ describe('CoreDatabaseTable', () => {
 | 
			
		||||
        records.push(john);
 | 
			
		||||
        records.push(amy);
 | 
			
		||||
 | 
			
		||||
        const table = await UsersTable.create(db);
 | 
			
		||||
        await table.initialize();
 | 
			
		||||
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 1 })).toEqual(john);
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 2 })).toEqual(amy);
 | 
			
		||||
        expect(table.find({ surname: 'Doe', name: 'John' })).toEqual(john);
 | 
			
		||||
        expect(table.find({ surname: 'Doe', name: 'Amy' })).toEqual(amy);
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
 | 
			
		||||
        await expect(table.find({ surname: 'Doe', name: 'John' })).resolves.toEqual(john);
 | 
			
		||||
        await expect(table.find({ surname: 'Doe', name: 'Amy' })).resolves.toEqual(amy);
 | 
			
		||||
 | 
			
		||||
        expect(database.getRecord).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('inserts items', async () => {
 | 
			
		||||
        // Arrange.
 | 
			
		||||
        const john = { id: 1, name: 'John', surname: 'Doe' };
 | 
			
		||||
 | 
			
		||||
        // Act.
 | 
			
		||||
        const table = await UsersTable.create(db);
 | 
			
		||||
        await table.initialize();
 | 
			
		||||
 | 
			
		||||
        // Act.
 | 
			
		||||
        await table.insert(john);
 | 
			
		||||
 | 
			
		||||
        // Assert.
 | 
			
		||||
        expect(db.insertRecord).toHaveBeenCalledWith('users', john);
 | 
			
		||||
        expect(database.insertRecord).toHaveBeenCalledWith('users', john);
 | 
			
		||||
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 1 })).toEqual(john);
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('deletes items', async () => {
 | 
			
		||||
@ -88,17 +124,17 @@ describe('CoreDatabaseTable', () => {
 | 
			
		||||
        records.push(amy);
 | 
			
		||||
        records.push(jane);
 | 
			
		||||
 | 
			
		||||
        // Act.
 | 
			
		||||
        const table = await UsersTable.create(db);
 | 
			
		||||
        await table.initialize();
 | 
			
		||||
 | 
			
		||||
        // Act.
 | 
			
		||||
        await table.delete({ surname: 'Doe' });
 | 
			
		||||
 | 
			
		||||
        // Assert.
 | 
			
		||||
        expect(db.deleteRecords).toHaveBeenCalledWith('users', { surname: 'Doe' });
 | 
			
		||||
        expect(database.deleteRecords).toHaveBeenCalledWith('users', { surname: 'Doe' });
 | 
			
		||||
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 1 })).toBeNull();
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 2 })).toBeNull();
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 3 })).toEqual(jane);
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 2 })).rejects.toThrow();
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 3 })).resolves.toEqual(jane);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('deletes items by primary key', async () => {
 | 
			
		||||
@ -109,16 +145,108 @@ describe('CoreDatabaseTable', () => {
 | 
			
		||||
        records.push(john);
 | 
			
		||||
        records.push(amy);
 | 
			
		||||
 | 
			
		||||
        // Act.
 | 
			
		||||
        const table = await UsersTable.create(db);
 | 
			
		||||
        await table.initialize();
 | 
			
		||||
 | 
			
		||||
        // Act.
 | 
			
		||||
        await table.deleteByPrimaryKey({ id: 1 });
 | 
			
		||||
 | 
			
		||||
        // Assert.
 | 
			
		||||
        expect(db.deleteRecords).toHaveBeenCalledWith('users', { id: 1 });
 | 
			
		||||
        expect(database.deleteRecords).toHaveBeenCalledWith('users', { id: 1 });
 | 
			
		||||
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 1 })).toBeNull();
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 2 })).toEqual(amy);
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('CoreDatabaseTable with no caching', () => {
 | 
			
		||||
 | 
			
		||||
    let records: User[];
 | 
			
		||||
    let database: SQLiteDB;
 | 
			
		||||
    let table: CoreDatabaseTableProxy<User>;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.None }));
 | 
			
		||||
 | 
			
		||||
    it('reads no records on initialization', async () => {
 | 
			
		||||
        await table.initialize();
 | 
			
		||||
 | 
			
		||||
        expect(database.getRecords).not.toHaveBeenCalled();
 | 
			
		||||
        expect(database.getAllRecords).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('finds items', async () => {
 | 
			
		||||
        const john = { id: 1, name: 'John', surname: 'Doe' };
 | 
			
		||||
        const amy = { id: 2, name: 'Amy', surname: 'Doe' };
 | 
			
		||||
 | 
			
		||||
        records.push(john);
 | 
			
		||||
        records.push(amy);
 | 
			
		||||
 | 
			
		||||
        await table.initialize();
 | 
			
		||||
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
 | 
			
		||||
        await expect(table.find({ surname: 'Doe', name: 'John' })).resolves.toEqual(john);
 | 
			
		||||
        await expect(table.find({ surname: 'Doe', name: 'Amy' })).resolves.toEqual(amy);
 | 
			
		||||
 | 
			
		||||
        expect(database.getRecord).toHaveBeenCalledTimes(4);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('inserts items', async () => {
 | 
			
		||||
        // Arrange.
 | 
			
		||||
        const john = { id: 1, name: 'John', surname: 'Doe' };
 | 
			
		||||
 | 
			
		||||
        await table.initialize();
 | 
			
		||||
 | 
			
		||||
        // Act.
 | 
			
		||||
        await table.insert(john);
 | 
			
		||||
 | 
			
		||||
        // Assert.
 | 
			
		||||
        expect(database.insertRecord).toHaveBeenCalledWith('users', john);
 | 
			
		||||
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('deletes items', async () => {
 | 
			
		||||
        // Arrange.
 | 
			
		||||
        const john = { id: 1, name: 'John', surname: 'Doe' };
 | 
			
		||||
        const amy = { id: 2, name: 'Amy', surname: 'Doe' };
 | 
			
		||||
        const jane = { id: 3, name: 'Jane', surname: 'Smith' };
 | 
			
		||||
 | 
			
		||||
        records.push(john);
 | 
			
		||||
        records.push(amy);
 | 
			
		||||
        records.push(jane);
 | 
			
		||||
 | 
			
		||||
        await table.initialize();
 | 
			
		||||
 | 
			
		||||
        // Act.
 | 
			
		||||
        await table.delete({ surname: 'Doe' });
 | 
			
		||||
 | 
			
		||||
        // Assert.
 | 
			
		||||
        expect(database.deleteRecords).toHaveBeenCalledWith('users', { surname: 'Doe' });
 | 
			
		||||
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 2 })).rejects.toThrow();
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 3 })).resolves.toEqual(jane);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('deletes items by primary key', async () => {
 | 
			
		||||
        // Arrange.
 | 
			
		||||
        const john = { id: 1, name: 'John', surname: 'Doe' };
 | 
			
		||||
        const amy = { id: 2, name: 'Amy', surname: 'Doe' };
 | 
			
		||||
 | 
			
		||||
        records.push(john);
 | 
			
		||||
        records.push(amy);
 | 
			
		||||
 | 
			
		||||
        await table.initialize();
 | 
			
		||||
 | 
			
		||||
        // Act.
 | 
			
		||||
        await table.deleteByPrimaryKey({ id: 1 });
 | 
			
		||||
 | 
			
		||||
        // Assert.
 | 
			
		||||
        expect(database.deleteRecords).toHaveBeenCalledWith('users', { id: 1 });
 | 
			
		||||
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -13,11 +13,11 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { APP_SCHEMA, ConfigDBEntry, CONFIG_TABLE_NAME } from '@services/database/config';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { CONFIG_TABLE_NAME, APP_SCHEMA, ConfigDBEntry } from '@services/database/config';
 | 
			
		||||
import { CoreDatabaseTable } from '@classes/database-table';
 | 
			
		||||
import { CoreDatabaseTable } from '@classes/database/database-table';
 | 
			
		||||
import { CorePromisedValue } from '@classes/promised-value';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -27,11 +27,7 @@ import { CorePromisedValue } from '@classes/promised-value';
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class CoreConfigProvider {
 | 
			
		||||
 | 
			
		||||
    protected dbTable: CorePromisedValue<CoreConfigTable>;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.dbTable = new CorePromisedValue();
 | 
			
		||||
    }
 | 
			
		||||
    protected table: CorePromisedValue<CoreDatabaseTable<ConfigDBEntry, 'name'>> = new CorePromisedValue();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize database.
 | 
			
		||||
@ -43,10 +39,16 @@ export class CoreConfigProvider {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const db = CoreApp.getDB();
 | 
			
		||||
        const table = await CoreConfigTable.create(db);
 | 
			
		||||
        const table = new CoreDatabaseTableProxy<ConfigDBEntry, 'name'>(
 | 
			
		||||
            { cachingStrategy: CoreDatabaseCachingStrategy.Eager },
 | 
			
		||||
            CoreApp.getDB(),
 | 
			
		||||
            CONFIG_TABLE_NAME,
 | 
			
		||||
            ['name'],
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.dbTable.resolve(table);
 | 
			
		||||
        await table.initialize();
 | 
			
		||||
 | 
			
		||||
        this.table.resolve(table);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -56,7 +58,7 @@ export class CoreConfigProvider {
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async delete(name: string): Promise<void> {
 | 
			
		||||
        const table = await this.dbTable;
 | 
			
		||||
        const table = await this.table;
 | 
			
		||||
 | 
			
		||||
        await table.deleteByPrimaryKey({ name });
 | 
			
		||||
    }
 | 
			
		||||
@ -69,18 +71,18 @@ export class CoreConfigProvider {
 | 
			
		||||
     * @return Resolves upon success along with the config data. Reject on failure.
 | 
			
		||||
     */
 | 
			
		||||
    async get<T>(name: string, defaultValue?: T): Promise<T> {
 | 
			
		||||
        const table = await this.dbTable;
 | 
			
		||||
        const record = table.findByPrimaryKey({ name });
 | 
			
		||||
        try {
 | 
			
		||||
            const table = await this.table;
 | 
			
		||||
            const record = await table.findByPrimaryKey({ name });
 | 
			
		||||
 | 
			
		||||
        if (record !== null) {
 | 
			
		||||
            return record.value;
 | 
			
		||||
        }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (defaultValue !== undefined) {
 | 
			
		||||
                return defaultValue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        if (defaultValue !== undefined) {
 | 
			
		||||
            return defaultValue;
 | 
			
		||||
            throw error;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new Error(`Couldn't get config with name '${name}'`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -91,7 +93,7 @@ export class CoreConfigProvider {
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async set(name: string, value: number | string): Promise<void> {
 | 
			
		||||
        const table = await this.dbTable;
 | 
			
		||||
        const table = await this.table;
 | 
			
		||||
 | 
			
		||||
        await table.insert({ name, value });
 | 
			
		||||
    }
 | 
			
		||||
@ -99,13 +101,3 @@ export class CoreConfigProvider {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CoreConfig = makeSingleton(CoreConfigProvider);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Config database table.
 | 
			
		||||
 */
 | 
			
		||||
class CoreConfigTable extends CoreDatabaseTable<ConfigDBEntry, 'name'> {
 | 
			
		||||
 | 
			
		||||
    protected table = CONFIG_TABLE_NAME;
 | 
			
		||||
    protected primaryKeys = ['name'];
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user