forked from CIT/Vmeda.Online
		
	Merge pull request #3091 from NoelDeMartin/MOBILE-3977
MOBILE-3977: Database optimization strategies
This commit is contained in:
		
						commit
						858cf07f73
					
				@ -271,6 +271,7 @@ testsConfig['rules']['padded-blocks'] = [
 | 
			
		||||
        switches: 'never',
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
testsConfig['rules']['jest/expect-expect'] = 'off';
 | 
			
		||||
testsConfig['plugins'].push('jest');
 | 
			
		||||
testsConfig['extends'].push('plugin:jest/recommended');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -75,5 +75,13 @@ for (const [name, { duration, scripting, styling, blocking, longTasks, database,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Sort tests
 | 
			
		||||
const tests = Object.keys(performanceMeasures).sort();
 | 
			
		||||
const sortedPerformanceMeasures = {};
 | 
			
		||||
 | 
			
		||||
for (const test of tests) {
 | 
			
		||||
    sortedPerformanceMeasures[test] = performanceMeasures[test];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Display data
 | 
			
		||||
console.table(performanceMeasures);
 | 
			
		||||
console.table(sortedPerformanceMeasures);
 | 
			
		||||
 | 
			
		||||
@ -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];
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										219
									
								
								src/core/classes/database/database-table-proxy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								src/core/classes/database/database-table-proxy.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,219 @@
 | 
			
		||||
// (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 { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { asyncInstance } from '@/core/utils/async-instance';
 | 
			
		||||
import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb';
 | 
			
		||||
import { CoreConfigProvider } from '@services/config';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreDatabaseReducer, CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey } from './database-table';
 | 
			
		||||
import { CoreDebugDatabaseTable } from './debug-database-table';
 | 
			
		||||
import { CoreEagerDatabaseTable } from './eager-database-table';
 | 
			
		||||
import { CoreLazyDatabaseTable } from './lazy-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 = asyncInstance<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>>();
 | 
			
		||||
    protected environmentObserver?: CoreEventObserver;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        config: Partial<CoreDatabaseConfiguration>,
 | 
			
		||||
        database: SQLiteDB,
 | 
			
		||||
        tableName: string,
 | 
			
		||||
        primaryKeyColumns?: PrimaryKeyColumn[],
 | 
			
		||||
    ) {
 | 
			
		||||
        super(database, tableName, primaryKeyColumns);
 | 
			
		||||
 | 
			
		||||
        this.config = { ...this.getConfigDefaults(), ...config };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async initialize(): Promise<void> {
 | 
			
		||||
        this.environmentObserver = CoreEvents.on(CoreConfigProvider.ENVIRONMENT_UPDATED, () => this.updateTarget());
 | 
			
		||||
 | 
			
		||||
        await this.updateTarget();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async destroy(): Promise<void> {
 | 
			
		||||
        this.environmentObserver?.off();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getMany(conditions?: Partial<DBRecord>): Promise<DBRecord[]> {
 | 
			
		||||
        return this.target.getMany(conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getOne(conditions: Partial<DBRecord>): Promise<DBRecord> {
 | 
			
		||||
        return this.target.getOne(conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getOneByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> {
 | 
			
		||||
        return this.target.getOneByPrimaryKey(primaryKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async reduce<T>(reducer: CoreDatabaseReducer<DBRecord, T>, conditions?: CoreDatabaseConditions<DBRecord>): Promise<T> {
 | 
			
		||||
        return this.target.reduce<T>(reducer, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async insert(record: DBRecord): Promise<void> {
 | 
			
		||||
        return this.target.insert(record);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async update(updates: Partial<DBRecord>, conditions?: Partial<DBRecord>): Promise<void> {
 | 
			
		||||
        return this.target.update(updates, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async updateWhere(updates: Partial<DBRecord>, conditions: CoreDatabaseConditions<DBRecord>): Promise<void> {
 | 
			
		||||
        return this.target.updateWhere(updates, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async delete(conditions?: Partial<DBRecord>): Promise<void> {
 | 
			
		||||
        return this.target.delete(conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> {
 | 
			
		||||
        return this.target.deleteByPrimaryKey(primaryKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get default configuration values.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Config defaults.
 | 
			
		||||
     */
 | 
			
		||||
    protected getConfigDefaults(): CoreDatabaseConfiguration {
 | 
			
		||||
        return {
 | 
			
		||||
            cachingStrategy: CoreDatabaseCachingStrategy.None,
 | 
			
		||||
            debug: false,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get database configuration to use at runtime.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Database configuration.
 | 
			
		||||
     */
 | 
			
		||||
    protected getRuntimeConfig(): CoreDatabaseConfiguration {
 | 
			
		||||
        return {
 | 
			
		||||
            ...this.config,
 | 
			
		||||
            ...CoreConstants.CONFIG.databaseOptimizations,
 | 
			
		||||
            ...CoreConstants.CONFIG.databaseTableOptimizations?.[this.tableName],
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update underlying target instance.
 | 
			
		||||
     */
 | 
			
		||||
    protected async updateTarget(): Promise<void> {
 | 
			
		||||
        const oldTarget = this.target.instance;
 | 
			
		||||
        const newTarget = this.createTarget();
 | 
			
		||||
 | 
			
		||||
        if (oldTarget) {
 | 
			
		||||
            await oldTarget.destroy();
 | 
			
		||||
 | 
			
		||||
            this.target.resetInstance();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await newTarget.initialize();
 | 
			
		||||
 | 
			
		||||
        this.target.setInstance(newTarget);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create proxy target.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns Target instance.
 | 
			
		||||
     */
 | 
			
		||||
    protected createTarget(): CoreDatabaseTable<DBRecord, PrimaryKeyColumn> {
 | 
			
		||||
        const config = this.getRuntimeConfig();
 | 
			
		||||
        const table = this.createTable(config.cachingStrategy);
 | 
			
		||||
 | 
			
		||||
        return config.debug ? new CoreDebugDatabaseTable(table) : table;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.Lazy:
 | 
			
		||||
                return new CoreLazyDatabaseTable(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;
 | 
			
		||||
    debug: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Database caching strategies.
 | 
			
		||||
 */
 | 
			
		||||
export enum CoreDatabaseCachingStrategy {
 | 
			
		||||
    Eager = 'eager',
 | 
			
		||||
    Lazy = 'lazy',
 | 
			
		||||
    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.
 | 
			
		||||
     */
 | 
			
		||||
    getMany(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.
 | 
			
		||||
     */
 | 
			
		||||
    getOne(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.
 | 
			
		||||
     */
 | 
			
		||||
    getOneByPrimaryKey(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;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										139
									
								
								src/core/classes/database/debug-database-table.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								src/core/classes/database/debug-database-table.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,139 @@
 | 
			
		||||
// (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 { SQLiteDBRecordValues } from '@classes/sqlitedb';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey, CoreDatabaseReducer } from './database-table';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Database table proxy used to debug runtime operations.
 | 
			
		||||
 *
 | 
			
		||||
 * This proxy should only be used for development purposes.
 | 
			
		||||
 */
 | 
			
		||||
export class CoreDebugDatabaseTable<
 | 
			
		||||
    DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues,
 | 
			
		||||
    PrimaryKeyColumn extends keyof DBRecord = 'id',
 | 
			
		||||
    PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn>
 | 
			
		||||
> extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> {
 | 
			
		||||
 | 
			
		||||
    protected target: CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey>;
 | 
			
		||||
    protected logger: CoreLogger;
 | 
			
		||||
 | 
			
		||||
    constructor(target: CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey>) {
 | 
			
		||||
        super(target.getDatabase(), target.getTableName(), target.getPrimaryKeyColumns());
 | 
			
		||||
 | 
			
		||||
        this.target = target;
 | 
			
		||||
        this.logger = CoreLogger.getInstance(`CoreDatabase[${this.tableName}]`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    initialize(): Promise<void> {
 | 
			
		||||
        this.logger.log('initialize');
 | 
			
		||||
 | 
			
		||||
        return this.target.initialize();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    destroy(): Promise<void> {
 | 
			
		||||
        this.logger.log('destroy');
 | 
			
		||||
 | 
			
		||||
        return this.target.destroy();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    getMany(conditions?: Partial<DBRecord>): Promise<DBRecord[]> {
 | 
			
		||||
        this.logger.log('getMany', conditions);
 | 
			
		||||
 | 
			
		||||
        return this.target.getMany(conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    getOne(conditions: Partial<DBRecord>): Promise<DBRecord> {
 | 
			
		||||
        this.logger.log('getOne', conditions);
 | 
			
		||||
 | 
			
		||||
        return this.target.getOne(conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    getOneByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> {
 | 
			
		||||
        this.logger.log('findByPrimaryKey', primaryKey);
 | 
			
		||||
 | 
			
		||||
        return this.target.getOneByPrimaryKey(primaryKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    reduce<T>(reducer: CoreDatabaseReducer<DBRecord, T>, conditions?: CoreDatabaseConditions<DBRecord>): Promise<T> {
 | 
			
		||||
        this.logger.log('reduce', reducer, conditions);
 | 
			
		||||
 | 
			
		||||
        return this.target.reduce<T>(reducer, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    insert(record: DBRecord): Promise<void> {
 | 
			
		||||
        this.logger.log('insert', record);
 | 
			
		||||
 | 
			
		||||
        return this.target.insert(record);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    update(updates: Partial<DBRecord>, conditions?: Partial<DBRecord>): Promise<void> {
 | 
			
		||||
        this.logger.log('update', updates, conditions);
 | 
			
		||||
 | 
			
		||||
        return this.target.update(updates, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    updateWhere(updates: Partial<DBRecord>, conditions: CoreDatabaseConditions<DBRecord>): Promise<void> {
 | 
			
		||||
        this.logger.log('updateWhere', updates, conditions);
 | 
			
		||||
 | 
			
		||||
        return this.target.updateWhere(updates, conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    delete(conditions?: Partial<DBRecord>): Promise<void> {
 | 
			
		||||
        this.logger.log('delete', conditions);
 | 
			
		||||
 | 
			
		||||
        return this.target.delete(conditions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> {
 | 
			
		||||
        this.logger.log('deleteByPrimaryKey', primaryKey);
 | 
			
		||||
 | 
			
		||||
        return this.target.deleteByPrimaryKey(primaryKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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.getMany();
 | 
			
		||||
 | 
			
		||||
        this.records = records.reduce((data, record) => {
 | 
			
		||||
            const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record));
 | 
			
		||||
 | 
			
		||||
            data[primaryKey] = record;
 | 
			
		||||
 | 
			
		||||
            return data;
 | 
			
		||||
        }, {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getMany(conditions?: Partial<DBRecord>): Promise<DBRecord[]> {
 | 
			
		||||
        const records = Object.values(this.records);
 | 
			
		||||
 | 
			
		||||
        return conditions
 | 
			
		||||
            ? records.filter(record => this.recordMatches(record, conditions))
 | 
			
		||||
            : records;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getOne(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 getOneByPrimaryKey(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)];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										141
									
								
								src/core/classes/database/lazy-database-table.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/core/classes/database/lazy-database-table.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,141 @@
 | 
			
		||||
// (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 } from './database-table';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Wrapper used to improve performance by caching records that are used often for faster read operations.
 | 
			
		||||
 *
 | 
			
		||||
 * This implementation works best for tables that have a lot of records and are read often; for tables with a few records use
 | 
			
		||||
 * CoreEagerDatabaseTable instead.
 | 
			
		||||
 */
 | 
			
		||||
export class CoreLazyDatabaseTable<
 | 
			
		||||
    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 | null> = {};
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getOne(conditions: Partial<DBRecord>): Promise<DBRecord> {
 | 
			
		||||
        let record: DBRecord | null =
 | 
			
		||||
            Object.values(this.records).find(record => record && this.recordMatches(record, conditions)) ?? null;
 | 
			
		||||
 | 
			
		||||
        if (!record) {
 | 
			
		||||
            record = await super.getOne(conditions);
 | 
			
		||||
 | 
			
		||||
            this.records[this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record))] = record;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return record;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getOneByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> {
 | 
			
		||||
        const serializePrimaryKey = this.serializePrimaryKey(primaryKey);
 | 
			
		||||
 | 
			
		||||
        if (!(serializePrimaryKey in this.records)) {
 | 
			
		||||
            try {
 | 
			
		||||
                const record = await super.getOneByPrimaryKey(primaryKey);
 | 
			
		||||
 | 
			
		||||
                this.records[serializePrimaryKey] = record;
 | 
			
		||||
 | 
			
		||||
                return record;
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                this.records[serializePrimaryKey] = null;
 | 
			
		||||
 | 
			
		||||
                throw error;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const record = this.records[serializePrimaryKey];
 | 
			
		||||
 | 
			
		||||
        if (!record) {
 | 
			
		||||
            throw new CoreError('No records found.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return record;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async insert(record: DBRecord): Promise<void> {
 | 
			
		||||
        await super.insert(record);
 | 
			
		||||
 | 
			
		||||
        this.records[this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record))] = 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 (!record || (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 (!record || !conditions.js(record)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Object.assign(record, updates);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async delete(conditions?: Partial<DBRecord>): Promise<void> {
 | 
			
		||||
        await super.delete(conditions);
 | 
			
		||||
 | 
			
		||||
        for (const [primaryKey, record] of Object.entries(this.records)) {
 | 
			
		||||
            if (!record || (conditions && !this.recordMatches(record, conditions))) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.records[primaryKey] = null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> {
 | 
			
		||||
        await super.deleteByPrimaryKey(primaryKey);
 | 
			
		||||
 | 
			
		||||
        this.records[this.serializePrimaryKey(primaryKey)] = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -134,6 +134,16 @@ export class CorePromisedValue<T = unknown> implements Promise<T> {
 | 
			
		||||
        this._reject(reason);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reset status and value.
 | 
			
		||||
     */
 | 
			
		||||
    reset(): void {
 | 
			
		||||
        delete this._resolvedValue;
 | 
			
		||||
        delete this._rejectedReason;
 | 
			
		||||
 | 
			
		||||
        this.initPromise();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize the promise and the callbacks.
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -41,6 +41,10 @@ import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreIonLoadingElement } from './ion-loading';
 | 
			
		||||
import { CoreLang } from '@services/lang';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { asyncInstance, AsyncInstance } from '../utils/async-instance';
 | 
			
		||||
import { CoreDatabaseTable } from './database/database-table';
 | 
			
		||||
import { CoreDatabaseCachingStrategy } from './database/database-table-proxy';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * QR Code type enumeration.
 | 
			
		||||
@ -103,6 +107,7 @@ export class CoreSite {
 | 
			
		||||
    // Rest of variables.
 | 
			
		||||
    protected logger: CoreLogger;
 | 
			
		||||
    protected db?: SQLiteDB;
 | 
			
		||||
    protected cacheTable: AsyncInstance<CoreDatabaseTable<CoreSiteWSCacheRecord>>;
 | 
			
		||||
    protected cleanUnicode = false;
 | 
			
		||||
    protected lastAutoLogin = 0;
 | 
			
		||||
    protected offlineDisabled = false;
 | 
			
		||||
@ -136,6 +141,11 @@ export class CoreSite {
 | 
			
		||||
    ) {
 | 
			
		||||
        this.logger = CoreLogger.getInstance('CoreSite');
 | 
			
		||||
        this.siteUrl = CoreUrlUtils.removeUrlParams(this.siteUrl); // Make sure the URL doesn't have params.
 | 
			
		||||
        this.cacheTable = asyncInstance(() => CoreSites.getSiteTable(CoreSite.WS_CACHE_TABLE, {
 | 
			
		||||
            siteId: this.getId(),
 | 
			
		||||
            database: this.getDb(),
 | 
			
		||||
            config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
 | 
			
		||||
        }));
 | 
			
		||||
        this.setInfo(infos);
 | 
			
		||||
        this.calculateOfflineDisabled();
 | 
			
		||||
 | 
			
		||||
@ -920,8 +930,7 @@ export class CoreSite {
 | 
			
		||||
        preSets: CoreSiteWSPreSets,
 | 
			
		||||
        emergency?: boolean,
 | 
			
		||||
    ): Promise<T> {
 | 
			
		||||
        const db = this.db;
 | 
			
		||||
        if (!db || !preSets.getFromCache) {
 | 
			
		||||
        if (!this.db || !preSets.getFromCache) {
 | 
			
		||||
            throw new CoreError('Get from cache is disabled.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -929,11 +938,11 @@ export class CoreSite {
 | 
			
		||||
        let entry: CoreSiteWSCacheRecord | undefined;
 | 
			
		||||
 | 
			
		||||
        if (preSets.getCacheUsingCacheKey || (emergency && preSets.getEmergencyCacheUsingCacheKey)) {
 | 
			
		||||
            const entries = await db.getRecords<CoreSiteWSCacheRecord>(CoreSite.WS_CACHE_TABLE, { key: preSets.cacheKey });
 | 
			
		||||
            const entries = await this.cacheTable.getMany({ key: preSets.cacheKey });
 | 
			
		||||
 | 
			
		||||
            if (!entries.length) {
 | 
			
		||||
                // Cache key not found, get by params sent.
 | 
			
		||||
                entry = await db.getRecord(CoreSite.WS_CACHE_TABLE, { id });
 | 
			
		||||
                entry = await this.cacheTable.getOneByPrimaryKey({ id });
 | 
			
		||||
            } else {
 | 
			
		||||
                if (entries.length > 1) {
 | 
			
		||||
                    // More than one entry found. Search the one with same ID as this call.
 | 
			
		||||
@ -945,7 +954,7 @@ export class CoreSite {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            entry = await db.getRecord(CoreSite.WS_CACHE_TABLE, { id });
 | 
			
		||||
            entry = await this.cacheTable.getOneByPrimaryKey({ id });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (entry === undefined) {
 | 
			
		||||
@ -996,12 +1005,18 @@ export class CoreSite {
 | 
			
		||||
            extraClause = ' AND componentId = ?';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const size = <number> await this.getDb().getFieldSql(
 | 
			
		||||
            'SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE + ' WHERE component = ?' + extraClause,
 | 
			
		||||
            params,
 | 
			
		||||
        return this.cacheTable.reduce(
 | 
			
		||||
            {
 | 
			
		||||
                sql: 'SUM(length(data))',
 | 
			
		||||
                js: (size, record) => size + record.data.length,
 | 
			
		||||
                jsInitialValue: 0,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                sql: 'WHERE component = ?' + extraClause,
 | 
			
		||||
                sqlParams: params,
 | 
			
		||||
                js: record => record.component === component && (params.length === 1 || record.componentId === componentId),
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return size;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1015,10 +1030,6 @@ export class CoreSite {
 | 
			
		||||
     */
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
    protected async saveToCache(method: string, data: any, response: any, preSets: CoreSiteWSPreSets): Promise<void> {
 | 
			
		||||
        if (!this.db) {
 | 
			
		||||
            throw new CoreError('Site DB not initialized.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (preSets.uniqueCacheKey) {
 | 
			
		||||
            // Cache key must be unique, delete all entries with same cache key.
 | 
			
		||||
            await CoreUtils.ignoreErrors(this.deleteFromCache(method, data, preSets, true));
 | 
			
		||||
@ -1044,7 +1055,7 @@ export class CoreSite {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.db.insertRecord(CoreSite.WS_CACHE_TABLE, entry);
 | 
			
		||||
        await this.cacheTable.insert(entry);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1058,16 +1069,12 @@ export class CoreSite {
 | 
			
		||||
     */
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
    protected async deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean): Promise<void> {
 | 
			
		||||
        if (!this.db) {
 | 
			
		||||
            throw new CoreError('Site DB not initialized.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const id = this.getCacheId(method, data);
 | 
			
		||||
 | 
			
		||||
        if (allCacheKey) {
 | 
			
		||||
            await this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, { key: preSets.cacheKey });
 | 
			
		||||
            await this.cacheTable.delete({ key: preSets.cacheKey });
 | 
			
		||||
        } else {
 | 
			
		||||
            await this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, { id });
 | 
			
		||||
            await this.cacheTable.deleteByPrimaryKey({ id });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1084,18 +1091,13 @@ export class CoreSite {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.db) {
 | 
			
		||||
            throw new CoreError('Site DB not initialized');
 | 
			
		||||
        }
 | 
			
		||||
        const params = { component };
 | 
			
		||||
 | 
			
		||||
        const params = {
 | 
			
		||||
            component,
 | 
			
		||||
        };
 | 
			
		||||
        if (componentId) {
 | 
			
		||||
            params['componentId'] = componentId;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, params);
 | 
			
		||||
        await this.cacheTable.delete(params);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
@ -1127,14 +1129,10 @@ export class CoreSite {
 | 
			
		||||
     * @return Promise resolved when the cache entries are invalidated.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidateWsCache(): Promise<void> {
 | 
			
		||||
        if (!this.db) {
 | 
			
		||||
            throw new CoreError('Site DB not initialized');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.logger.debug('Invalidate all the cache for site: ' + this.id);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.db.updateRecords(CoreSite.WS_CACHE_TABLE, { expirationTime: 0 });
 | 
			
		||||
            await this.cacheTable.update({ expirationTime: 0 });
 | 
			
		||||
        } finally {
 | 
			
		||||
            CoreEvents.trigger(CoreEvents.WS_CACHE_INVALIDATED, {}, this.getId());
 | 
			
		||||
        }
 | 
			
		||||
@ -1147,16 +1145,13 @@ export class CoreSite {
 | 
			
		||||
     * @return Promise resolved when the cache entries are invalidated.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidateWsCacheForKey(key: string): Promise<void> {
 | 
			
		||||
        if (!this.db) {
 | 
			
		||||
            throw new CoreError('Site DB not initialized');
 | 
			
		||||
        }
 | 
			
		||||
        if (!key) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.logger.debug('Invalidate cache for key: ' + key);
 | 
			
		||||
 | 
			
		||||
        await this.db.updateRecords(CoreSite.WS_CACHE_TABLE, { expirationTime: 0 }, { key });
 | 
			
		||||
        await this.cacheTable.update({ expirationTime: 0 }, { key });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1184,18 +1179,17 @@ export class CoreSite {
 | 
			
		||||
     * @return Promise resolved when the cache entries are invalidated.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidateWsCacheForKeyStartingWith(key: string): Promise<void> {
 | 
			
		||||
        if (!this.db) {
 | 
			
		||||
            throw new CoreError('Site DB not initialized');
 | 
			
		||||
        }
 | 
			
		||||
        if (!key) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.logger.debug('Invalidate cache for key starting with: ' + key);
 | 
			
		||||
 | 
			
		||||
        const sql = 'UPDATE ' + CoreSite.WS_CACHE_TABLE + ' SET expirationTime=0 WHERE key LIKE ?';
 | 
			
		||||
 | 
			
		||||
        await this.db.execute(sql, [key + '%']);
 | 
			
		||||
        await this.cacheTable.updateWhere({ expirationTime: 0 }, {
 | 
			
		||||
            sql: 'key LIKE ?',
 | 
			
		||||
            sqlParams: [key],
 | 
			
		||||
            js: record => !!record.key?.startsWith(key),
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1270,9 +1264,11 @@ export class CoreSite {
 | 
			
		||||
     * @return Promise resolved with the total size of all data in the cache table (bytes)
 | 
			
		||||
     */
 | 
			
		||||
    async getCacheUsage(): Promise<number> {
 | 
			
		||||
        const size = <number> await this.getDb().getFieldSql('SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE);
 | 
			
		||||
 | 
			
		||||
        return size;
 | 
			
		||||
        return this.cacheTable.reduce({
 | 
			
		||||
            sql: 'SUM(length(data))',
 | 
			
		||||
            js: (size, record) => size + record.data.length,
 | 
			
		||||
            jsInitialValue: 0,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -1206,4 +1206,4 @@ export type SQLiteDBQueryParams = {
 | 
			
		||||
    params: SQLiteDBRecordValue[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type SQLiteDBRecordValue = number | string;
 | 
			
		||||
export type SQLiteDBRecordValue = number | string;
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,12 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { mock } from '@/testing/utils';
 | 
			
		||||
import { CoreDatabaseTable } from '@classes/database-table';
 | 
			
		||||
import { CoreDatabaseTable } from '@classes/database/database-table';
 | 
			
		||||
import {
 | 
			
		||||
    CoreDatabaseCachingStrategy,
 | 
			
		||||
    CoreDatabaseConfiguration,
 | 
			
		||||
    CoreDatabaseTableProxy,
 | 
			
		||||
} from '@classes/database/database-table-proxy';
 | 
			
		||||
import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb';
 | 
			
		||||
 | 
			
		||||
interface User extends SQLiteDBRecordValues {
 | 
			
		||||
@ -22,103 +27,197 @@ 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, CoreDatabaseTable<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];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function testFindItems(records: User[], table: CoreDatabaseTable<User>) {
 | 
			
		||||
    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.getOneByPrimaryKey({ id: 1 })).resolves.toEqual(john);
 | 
			
		||||
    await expect(table.getOneByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
 | 
			
		||||
    await expect(table.getOne({ surname: 'Doe', name: 'John' })).resolves.toEqual(john);
 | 
			
		||||
    await expect(table.getOne({ surname: 'Doe', name: 'Amy' })).resolves.toEqual(amy);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function testInsertItems(records: User[], database: SQLiteDB, table: CoreDatabaseTable<User>) {
 | 
			
		||||
    // 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.getOneByPrimaryKey({ id: 1 })).resolves.toEqual(john);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function testDeleteItems(records: User[], database: SQLiteDB, table: CoreDatabaseTable<User>) {
 | 
			
		||||
    // 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.getOneByPrimaryKey({ id: 1 })).rejects.toThrow();
 | 
			
		||||
    await expect(table.getOneByPrimaryKey({ id: 2 })).rejects.toThrow();
 | 
			
		||||
    await expect(table.getOneByPrimaryKey({ id: 3 })).resolves.toEqual(jane);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function testDeleteItemsByPrimaryKey(records: User[], database: SQLiteDB, table: CoreDatabaseTable<User>) {
 | 
			
		||||
    // 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.getOneByPrimaryKey({ id: 1 })).rejects.toThrow();
 | 
			
		||||
    await expect(table.getOneByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('CoreDatabaseTable with eager caching', () => {
 | 
			
		||||
 | 
			
		||||
    let records: User[];
 | 
			
		||||
    let db: SQLiteDB;
 | 
			
		||||
    let database: SQLiteDB;
 | 
			
		||||
    let table: CoreDatabaseTable<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 () => {
 | 
			
		||||
        const john = { id: 1, name: 'John', surname: 'Doe' };
 | 
			
		||||
        const amy = { id: 2, name: 'Amy', surname: 'Doe' };
 | 
			
		||||
        await testFindItems(records, table);
 | 
			
		||||
 | 
			
		||||
        records.push(john);
 | 
			
		||||
        records.push(amy);
 | 
			
		||||
 | 
			
		||||
        const table = await UsersTable.create(db);
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
        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.insert(john);
 | 
			
		||||
 | 
			
		||||
        // Assert.
 | 
			
		||||
        expect(db.insertRecord).toHaveBeenCalledWith('users', john);
 | 
			
		||||
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 1 })).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);
 | 
			
		||||
 | 
			
		||||
        // Act.
 | 
			
		||||
        const table = await UsersTable.create(db);
 | 
			
		||||
 | 
			
		||||
        await table.delete({ surname: 'Doe' });
 | 
			
		||||
 | 
			
		||||
        // Assert.
 | 
			
		||||
        expect(db.deleteRecords).toHaveBeenCalledWith('users', { surname: 'Doe' });
 | 
			
		||||
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 1 })).toBeNull();
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 2 })).toBeNull();
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 3 })).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);
 | 
			
		||||
 | 
			
		||||
        // Act.
 | 
			
		||||
        const table = await UsersTable.create(db);
 | 
			
		||||
 | 
			
		||||
        await table.deleteByPrimaryKey({ id: 1 });
 | 
			
		||||
 | 
			
		||||
        // Assert.
 | 
			
		||||
        expect(db.deleteRecords).toHaveBeenCalledWith('users', { id: 1 });
 | 
			
		||||
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 1 })).toBeNull();
 | 
			
		||||
        expect(table.findByPrimaryKey({ id: 2 })).toEqual(amy);
 | 
			
		||||
    });
 | 
			
		||||
    it('inserts items', () => testInsertItems(records, database, table));
 | 
			
		||||
    it('deletes items', () => testDeleteItems(records, database, table));
 | 
			
		||||
    it('deletes items by primary key', () => testDeleteItemsByPrimaryKey(records, database, table));
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('CoreDatabaseTable with lazy caching', () => {
 | 
			
		||||
 | 
			
		||||
    let records: User[];
 | 
			
		||||
    let database: SQLiteDB;
 | 
			
		||||
    let table: CoreDatabaseTable<User>;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.Lazy }));
 | 
			
		||||
 | 
			
		||||
    it('reads no records on initialization', async () => {
 | 
			
		||||
        await table.initialize();
 | 
			
		||||
 | 
			
		||||
        expect(database.getRecords).not.toHaveBeenCalled();
 | 
			
		||||
        expect(database.getAllRecords).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('finds items', async () => {
 | 
			
		||||
        await testFindItems(records, table);
 | 
			
		||||
 | 
			
		||||
        expect(database.getRecord).toHaveBeenCalledTimes(2);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('inserts items', () => testInsertItems(records, database, table));
 | 
			
		||||
    it('deletes items', () => testDeleteItems(records, database, table));
 | 
			
		||||
    it('deletes items by primary key', () => testDeleteItemsByPrimaryKey(records, database, table));
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('CoreDatabaseTable with no caching', () => {
 | 
			
		||||
 | 
			
		||||
    let records: User[];
 | 
			
		||||
    let database: SQLiteDB;
 | 
			
		||||
    let table: CoreDatabaseTable<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 () => {
 | 
			
		||||
        await testFindItems(records, table);
 | 
			
		||||
 | 
			
		||||
        expect(database.getRecord).toHaveBeenCalledTimes(4);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('inserts items', () => testInsertItems(records, database, table));
 | 
			
		||||
    it('deletes items', () => testDeleteItems(records, database, table));
 | 
			
		||||
    it('deletes items by primary key', () => testDeleteItemsByPrimaryKey(records, database, table));
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -145,23 +145,6 @@ export class CoreConstants {
 | 
			
		||||
    static readonly CONFIG = { ...envJson.config } as unknown as EnvironmentConfig; // Data parsed from config.json files.
 | 
			
		||||
    static readonly BUILD = envJson.build as unknown as EnvironmentBuild; // Build info.
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update config with the given values.
 | 
			
		||||
     *
 | 
			
		||||
     * @param config Config updates.
 | 
			
		||||
     */
 | 
			
		||||
    static patchConfig(config: Partial<EnvironmentConfig>): void {
 | 
			
		||||
        Object.assign(this.CONFIG, config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reset config values to its original state.
 | 
			
		||||
     */
 | 
			
		||||
    static resetConfig(): void {
 | 
			
		||||
        Object.keys(this.CONFIG).forEach(key => delete this.CONFIG[key]);
 | 
			
		||||
        Object.assign(this.CONFIG, envJson.config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface EnvironmentBuild {
 | 
			
		||||
 | 
			
		||||
@ -15,11 +15,13 @@
 | 
			
		||||
import { CoreFilepool } from '@services/filepool';
 | 
			
		||||
import { CoreLang } from '@services/lang';
 | 
			
		||||
import { CoreLocalNotifications } from '@services/local-notifications';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreUpdateManager } from '@services/update-manager';
 | 
			
		||||
 | 
			
		||||
export default async function(): Promise<void> {
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        CoreFilepool.initialize(),
 | 
			
		||||
        CoreSites.initialize(),
 | 
			
		||||
        CoreLang.initialize(),
 | 
			
		||||
        CoreLocalNotifications.initialize(),
 | 
			
		||||
        CoreUpdateManager.initialize(),
 | 
			
		||||
 | 
			
		||||
@ -12,13 +12,29 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { EnvironmentConfig } from '@/types/config';
 | 
			
		||||
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 { CorePromisedValue } from '@classes/promised-value';
 | 
			
		||||
import { CoreConstants } from '../constants';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
import { CoreDatabaseTable } from '@classes/database/database-table';
 | 
			
		||||
import { asyncInstance } from '../utils/async-instance';
 | 
			
		||||
 | 
			
		||||
declare module '@singletons/events' {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Augment CoreEventsData interface with events specific to this service.
 | 
			
		||||
     *
 | 
			
		||||
     * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
 | 
			
		||||
     */
 | 
			
		||||
    export interface CoreEventsData {
 | 
			
		||||
        [CoreConfigProvider.ENVIRONMENT_UPDATED]: EnvironmentConfig;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Factory to provide access to dynamic and permanent config and settings.
 | 
			
		||||
@ -27,11 +43,10 @@ import { CorePromisedValue } from '@classes/promised-value';
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class CoreConfigProvider {
 | 
			
		||||
 | 
			
		||||
    protected dbTable: CorePromisedValue<CoreConfigTable>;
 | 
			
		||||
    static readonly ENVIRONMENT_UPDATED = 'environment_updated';
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.dbTable = new CorePromisedValue();
 | 
			
		||||
    }
 | 
			
		||||
    protected table = asyncInstance<CoreDatabaseTable<ConfigDBEntry, 'name'>>();
 | 
			
		||||
    protected defaultEnvironment?: EnvironmentConfig;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize database.
 | 
			
		||||
@ -43,10 +58,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.setInstance(table);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -56,9 +77,7 @@ export class CoreConfigProvider {
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async delete(name: string): Promise<void> {
 | 
			
		||||
        const table = await this.dbTable;
 | 
			
		||||
 | 
			
		||||
        await table.deleteByPrimaryKey({ name });
 | 
			
		||||
        await this.table.deleteByPrimaryKey({ name });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -69,18 +88,17 @@ 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 record = await this.table.getOneByPrimaryKey({ 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,21 +109,36 @@ export class CoreConfigProvider {
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async set(name: string, value: number | string): Promise<void> {
 | 
			
		||||
        const table = await this.dbTable;
 | 
			
		||||
        await this.table.insert({ name, value });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        await table.insert({ name, value });
 | 
			
		||||
    /**
 | 
			
		||||
     * Update config with the given values.
 | 
			
		||||
     *
 | 
			
		||||
     * @param config Config updates.
 | 
			
		||||
     */
 | 
			
		||||
    patchEnvironment(config: Partial<EnvironmentConfig>): void {
 | 
			
		||||
        this.defaultEnvironment = this.defaultEnvironment ?? CoreConstants.CONFIG;
 | 
			
		||||
 | 
			
		||||
        Object.assign(CoreConstants.CONFIG, config);
 | 
			
		||||
        CoreEvents.trigger(CoreConfigProvider.ENVIRONMENT_UPDATED, CoreConstants.CONFIG);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reset config values to its original state.
 | 
			
		||||
     */
 | 
			
		||||
    resetEnvironment(): void {
 | 
			
		||||
        if (!this.defaultEnvironment) {
 | 
			
		||||
            // The environment config hasn't been modified; there's not need to reset.
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Object.keys(CoreConstants.CONFIG).forEach(key => delete CoreConstants.CONFIG[key]);
 | 
			
		||||
        Object.assign(CoreConstants.CONFIG, this.defaultEnvironment);
 | 
			
		||||
        CoreEvents.trigger(CoreConfigProvider.ENVIRONMENT_UPDATED, CoreConstants.CONFIG);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CoreConfig = makeSingleton(CoreConfigProvider);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Config database table.
 | 
			
		||||
 */
 | 
			
		||||
class CoreConfigTable extends CoreDatabaseTable<ConfigDBEntry, 'name'> {
 | 
			
		||||
 | 
			
		||||
    protected table = CONFIG_TABLE_NAME;
 | 
			
		||||
    protected primaryKeys = ['name'];
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,17 @@ export class CoreDbProvider {
 | 
			
		||||
        return CoreAppProvider.isAutomated();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Print query history in console.
 | 
			
		||||
     */
 | 
			
		||||
    printHistory(): void {
 | 
			
		||||
        const substituteParams = ({ sql, params }: CoreDbQueryLog) =>
 | 
			
		||||
            Object.values(params ?? []).reduce((sql: string, param: string) => sql.replace('?', param), sql);
 | 
			
		||||
 | 
			
		||||
        // eslint-disable-next-line no-console
 | 
			
		||||
        console.log(this.queryLogs.map(substituteParams).join('\n'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Log a query.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,10 @@ import {
 | 
			
		||||
} from '@services/database/filepool';
 | 
			
		||||
import { CoreFileHelper } from './file-helper';
 | 
			
		||||
import { CoreUrl } from '@singletons/url';
 | 
			
		||||
import { CoreDatabaseTable } from '@classes/database/database-table';
 | 
			
		||||
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
 | 
			
		||||
import { lazyMap, LazyMap } from '../utils/lazy-map';
 | 
			
		||||
import { asyncInstance, AsyncInstance } from '../utils/async-instance';
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Factory for handling downloading files and retrieve downloaded files.
 | 
			
		||||
@ -72,9 +76,13 @@ export class CoreFilepoolProvider {
 | 
			
		||||
    protected static readonly ERR_FS_OR_NETWORK_UNAVAILABLE = 'CoreFilepoolError:ERR_FS_OR_NETWORK_UNAVAILABLE';
 | 
			
		||||
    protected static readonly ERR_QUEUE_ON_PAUSE = 'CoreFilepoolError:ERR_QUEUE_ON_PAUSE';
 | 
			
		||||
 | 
			
		||||
    protected static readonly FILE_UPDATE_UNKNOWN_WHERE_CLAUSE =
 | 
			
		||||
    protected static readonly FILE_IS_UNKNOWN_SQL =
 | 
			
		||||
        'isexternalfile = 1 OR ((revision IS NULL OR revision = 0) AND (timemodified IS NULL OR timemodified = 0))';
 | 
			
		||||
 | 
			
		||||
    protected static readonly FILE_IS_UNKNOWN_JS =
 | 
			
		||||
        ({ isexternalfile, revision, timemodified }: CoreFilepoolFileEntry): boolean =>
 | 
			
		||||
            isexternalfile === 1 || ((revision === null || revision === 0) && (timemodified === null || timemodified === 0));
 | 
			
		||||
 | 
			
		||||
    protected logger: CoreLogger;
 | 
			
		||||
    protected queueState = CoreFilepoolProvider.QUEUE_PAUSED;
 | 
			
		||||
    protected urlAttributes: RegExp[] = [
 | 
			
		||||
@ -94,10 +102,20 @@ export class CoreFilepoolProvider {
 | 
			
		||||
    // Variables for DB.
 | 
			
		||||
    protected appDB: Promise<SQLiteDB>;
 | 
			
		||||
    protected resolveAppDB!: (appDB: SQLiteDB) => void;
 | 
			
		||||
    protected filesTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreFilepoolFileEntry, 'fileId'>>>;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
 | 
			
		||||
        this.logger = CoreLogger.getInstance('CoreFilepoolProvider');
 | 
			
		||||
        this.filesTables = lazyMap(
 | 
			
		||||
            siteId => asyncInstance(
 | 
			
		||||
                () => CoreSites.getSiteTable<CoreFilepoolFileEntry, 'fileId'>(FILES_TABLE_NAME, {
 | 
			
		||||
                    siteId,
 | 
			
		||||
                    config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy },
 | 
			
		||||
                    primaryKeyColumns: ['fileId'],
 | 
			
		||||
                }),
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -114,6 +132,16 @@ export class CoreFilepoolProvider {
 | 
			
		||||
                NgZone.run(() => this.checkQueueProcessing());
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        CoreEvents.on(CoreEvents.SITE_DELETED, async ({ siteId }) => {
 | 
			
		||||
            if (!siteId || !(siteId in this.filesTables)) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await this.filesTables[siteId].destroy();
 | 
			
		||||
 | 
			
		||||
            delete this.filesTables[siteId];
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -215,9 +243,7 @@ export class CoreFilepoolProvider {
 | 
			
		||||
            ...data,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const db = await CoreSites.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        await db.insertRecord(FILES_TABLE_NAME, record);
 | 
			
		||||
        await this.filesTables[siteId].insert(record);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -560,11 +586,11 @@ export class CoreFilepoolProvider {
 | 
			
		||||
        const db = await CoreSites.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        // Read the data first to be able to notify the deletions.
 | 
			
		||||
        const filesEntries = await db.getAllRecords<CoreFilepoolFileEntry>(FILES_TABLE_NAME);
 | 
			
		||||
        const filesEntries = await this.filesTables[siteId].getMany();
 | 
			
		||||
        const filesLinks = await db.getAllRecords<CoreFilepoolLinksRecord>(LINKS_TABLE_NAME);
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            db.deleteRecords(FILES_TABLE_NAME),
 | 
			
		||||
            this.filesTables[siteId].delete(),
 | 
			
		||||
            db.deleteRecords(LINKS_TABLE_NAME),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
@ -1125,7 +1151,7 @@ export class CoreFilepoolProvider {
 | 
			
		||||
            // Minor problem: file will remain in the filesystem once downloaded again.
 | 
			
		||||
            this.logger.debug('Staled file with no extension ' + entry.fileId);
 | 
			
		||||
 | 
			
		||||
            await db.updateRecords(FILES_TABLE_NAME, { stale: 1 }, { fileId: entry.fileId });
 | 
			
		||||
            await this.filesTables[siteId].update({ stale: 1 }, { fileId: entry.fileId });
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@ -1135,7 +1161,7 @@ export class CoreFilepoolProvider {
 | 
			
		||||
        entry.fileId = CoreMimetypeUtils.removeExtension(fileId);
 | 
			
		||||
        entry.extension = extension;
 | 
			
		||||
 | 
			
		||||
        await db.updateRecords(FILES_TABLE_NAME, entry, { fileId });
 | 
			
		||||
        await this.filesTables[siteId].update(entry, { fileId });
 | 
			
		||||
        if (entry.fileId == fileId) {
 | 
			
		||||
            // File ID hasn't changed, we're done.
 | 
			
		||||
            this.logger.debug('Removed extesion ' + extension + ' from file ' + entry.fileId);
 | 
			
		||||
@ -1401,10 +1427,7 @@ export class CoreFilepoolProvider {
 | 
			
		||||
 | 
			
		||||
        await Promise.all(items.map(async (item) => {
 | 
			
		||||
            try {
 | 
			
		||||
                const fileEntry = await db.getRecord<CoreFilepoolFileEntry>(
 | 
			
		||||
                    FILES_TABLE_NAME,
 | 
			
		||||
                    { fileId: item.fileId },
 | 
			
		||||
                );
 | 
			
		||||
                const fileEntry = await this.filesTables[siteId].getOneByPrimaryKey({ fileId: item.fileId });
 | 
			
		||||
 | 
			
		||||
                if (!fileEntry) {
 | 
			
		||||
                    return;
 | 
			
		||||
@ -2137,14 +2160,7 @@ export class CoreFilepoolProvider {
 | 
			
		||||
     * @return Resolved with file object from DB on success, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    protected async hasFileInPool(siteId: string, fileId: string): Promise<CoreFilepoolFileEntry> {
 | 
			
		||||
        const db = await CoreSites.getSiteDb(siteId);
 | 
			
		||||
        const entry = await db.getRecord<CoreFilepoolFileEntry>(FILES_TABLE_NAME, { fileId });
 | 
			
		||||
 | 
			
		||||
        if (entry === undefined) {
 | 
			
		||||
            throw new CoreError('File not found in filepool.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return entry;
 | 
			
		||||
        return this.filesTables[siteId].getOneByPrimaryKey({ fileId });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -2176,11 +2192,15 @@ export class CoreFilepoolProvider {
 | 
			
		||||
     * @return Resolved on success.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise<void> {
 | 
			
		||||
        const db = await CoreSites.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        const where = onlyUnknown ? CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE : undefined;
 | 
			
		||||
 | 
			
		||||
        await db.updateRecordsWhere(FILES_TABLE_NAME, { stale: 1 }, where);
 | 
			
		||||
        onlyUnknown
 | 
			
		||||
            ? await this.filesTables[siteId].updateWhere(
 | 
			
		||||
                { stale: 1 },
 | 
			
		||||
                {
 | 
			
		||||
                    sql: CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL,
 | 
			
		||||
                    js: CoreFilepoolProvider.FILE_IS_UNKNOWN_JS,
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            : await this.filesTables[siteId].update({ stale: 1 });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -2199,9 +2219,7 @@ export class CoreFilepoolProvider {
 | 
			
		||||
        const file = await this.fixPluginfileURL(siteId, fileUrl);
 | 
			
		||||
        const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file));
 | 
			
		||||
 | 
			
		||||
        const db = await CoreSites.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        await db.updateRecords(FILES_TABLE_NAME, { stale: 1 }, { fileId });
 | 
			
		||||
        await this.filesTables[siteId].update({ stale: 1 }, { fileId });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -2221,7 +2239,6 @@ export class CoreFilepoolProvider {
 | 
			
		||||
        onlyUnknown: boolean = true,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        const db = await CoreSites.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        const items = await this.getComponentFiles(db, component, componentId);
 | 
			
		||||
 | 
			
		||||
        if (!items.length) {
 | 
			
		||||
@ -2229,6 +2246,8 @@ export class CoreFilepoolProvider {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        siteId = siteId ?? CoreSites.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        const fileIds = items.map((item) => item.fileId);
 | 
			
		||||
 | 
			
		||||
        const whereAndParams = db.getInOrEqual(fileIds);
 | 
			
		||||
@ -2236,10 +2255,19 @@ export class CoreFilepoolProvider {
 | 
			
		||||
        whereAndParams.sql = 'fileId ' + whereAndParams.sql;
 | 
			
		||||
 | 
			
		||||
        if (onlyUnknown) {
 | 
			
		||||
            whereAndParams.sql += ' AND (' + CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE + ')';
 | 
			
		||||
            whereAndParams.sql += ' AND (' + CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL + ')';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await db.updateRecordsWhere(FILES_TABLE_NAME, { stale: 1 }, whereAndParams.sql, whereAndParams.params);
 | 
			
		||||
        await this.filesTables[siteId].updateWhere(
 | 
			
		||||
            { stale: 1 },
 | 
			
		||||
            {
 | 
			
		||||
                sql: whereAndParams.sql,
 | 
			
		||||
                sqlParams: whereAndParams.params,
 | 
			
		||||
                js: record => fileIds.includes(record.fileId) && (
 | 
			
		||||
                    !onlyUnknown || CoreFilepoolProvider.FILE_IS_UNKNOWN_JS(record)
 | 
			
		||||
                ),
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -2657,6 +2685,7 @@ export class CoreFilepoolProvider {
 | 
			
		||||
     */
 | 
			
		||||
    protected async removeFileById(siteId: string, fileId: string): Promise<void> {
 | 
			
		||||
        const db = await CoreSites.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        // Get the path to the file first since it relies on the file object stored in the pool.
 | 
			
		||||
        // Don't use getFilePath to prevent performing 2 DB requests.
 | 
			
		||||
        let path = this.getFilepoolFolderPath(siteId) + '/' + fileId;
 | 
			
		||||
@ -2682,7 +2711,7 @@ export class CoreFilepoolProvider {
 | 
			
		||||
        const promises: Promise<unknown>[] = [];
 | 
			
		||||
 | 
			
		||||
        // Remove entry from filepool store.
 | 
			
		||||
        promises.push(db.deleteRecords(FILES_TABLE_NAME, conditions));
 | 
			
		||||
        promises.push(this.filesTables[siteId].delete(conditions));
 | 
			
		||||
 | 
			
		||||
        // Remove links.
 | 
			
		||||
        promises.push(db.deleteRecords(LINKS_TABLE_NAME, conditions));
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@ import {
 | 
			
		||||
    CoreSitePublicConfigResponse,
 | 
			
		||||
    CoreSiteInfoResponse,
 | 
			
		||||
} from '@classes/site';
 | 
			
		||||
import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb';
 | 
			
		||||
import { SQLiteDB, SQLiteDBRecordValues, SQLiteDBTableSchema } from '@classes/sqlitedb';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreSiteError } from '@classes/errors/siteerror';
 | 
			
		||||
import { makeSingleton, Translate, Http } from '@singletons';
 | 
			
		||||
@ -57,6 +57,9 @@ import { CoreErrorWithTitle } from '@classes/errors/errorwithtitle';
 | 
			
		||||
import { CoreAjaxError } from '@classes/errors/ajaxerror';
 | 
			
		||||
import { CoreAjaxWSError } from '@classes/errors/ajaxwserror';
 | 
			
		||||
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
 | 
			
		||||
import { CorePromisedValue } from '@classes/promised-value';
 | 
			
		||||
import { CoreDatabaseTable } from '@classes/database/database-table';
 | 
			
		||||
import { CoreDatabaseConfiguration, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy';
 | 
			
		||||
 | 
			
		||||
export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
 | 
			
		||||
 | 
			
		||||
@ -85,6 +88,7 @@ export class CoreSitesProvider {
 | 
			
		||||
    // Variables for DB.
 | 
			
		||||
    protected appDB: Promise<SQLiteDB>;
 | 
			
		||||
    protected resolveAppDB!: (appDB: SQLiteDB) => void;
 | 
			
		||||
    protected siteTables: Record<string, Record<string, CorePromisedValue<CoreDatabaseTable>>> = {};
 | 
			
		||||
 | 
			
		||||
    constructor(@Optional() @Inject(CORE_SITE_SCHEMAS) siteSchemas: CoreSiteSchema[][] = []) {
 | 
			
		||||
        this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
 | 
			
		||||
@ -99,6 +103,25 @@ export class CoreSitesProvider {
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize.
 | 
			
		||||
     */
 | 
			
		||||
    initialize(): void {
 | 
			
		||||
        CoreEvents.on(CoreEvents.SITE_DELETED, async ({ siteId }) => {
 | 
			
		||||
            if (!siteId || !(siteId in this.siteTables)) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await Promise.all(
 | 
			
		||||
                Object
 | 
			
		||||
                    .values(this.siteTables[siteId])
 | 
			
		||||
                    .map(promisedTable => promisedTable.then(table => table.destroy())),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            delete this.siteTables[siteId];
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize database.
 | 
			
		||||
     */
 | 
			
		||||
@ -112,6 +135,49 @@ export class CoreSitesProvider {
 | 
			
		||||
        this.resolveAppDB(CoreApp.getDB());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get site table.
 | 
			
		||||
     *
 | 
			
		||||
     * @param tableName Site table name.
 | 
			
		||||
     * @param options Options to configure table initialization.
 | 
			
		||||
     * @returns Site table.
 | 
			
		||||
     */
 | 
			
		||||
    async getSiteTable<
 | 
			
		||||
        DBRecord extends SQLiteDBRecordValues,
 | 
			
		||||
        PrimaryKeyColumn extends keyof DBRecord
 | 
			
		||||
    >(
 | 
			
		||||
        tableName: string,
 | 
			
		||||
        options: Partial<{
 | 
			
		||||
            siteId: string;
 | 
			
		||||
            config: Partial<CoreDatabaseConfiguration>;
 | 
			
		||||
            database: SQLiteDB;
 | 
			
		||||
            primaryKeyColumns: PrimaryKeyColumn[];
 | 
			
		||||
        }> = {},
 | 
			
		||||
    ): Promise<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>> {
 | 
			
		||||
        const siteId = options.siteId ?? this.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        if (!(siteId in this.siteTables)) {
 | 
			
		||||
            this.siteTables[siteId] = {};
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!(tableName in this.siteTables[siteId])) {
 | 
			
		||||
            const promisedTable = this.siteTables[siteId][tableName] = new CorePromisedValue();
 | 
			
		||||
            const database = options.database ?? await this.getSiteDb(siteId);
 | 
			
		||||
            const table = new CoreDatabaseTableProxy<DBRecord, PrimaryKeyColumn>(
 | 
			
		||||
                options.config ?? {},
 | 
			
		||||
                database,
 | 
			
		||||
                tableName,
 | 
			
		||||
                options.primaryKeyColumns,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            await table.initialize();
 | 
			
		||||
 | 
			
		||||
            promisedTable.resolve(table as unknown as CoreDatabaseTable);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.siteTables[siteId][tableName] as unknown as Promise<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>>;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the demo data for a certain "name" if it is a demo site.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										122
									
								
								src/core/utils/async-instance.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/core/utils/async-instance.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,122 @@
 | 
			
		||||
// (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';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Create a wrapper to hold an asynchronous instance.
 | 
			
		||||
 *
 | 
			
		||||
 * @param lazyConstructor Constructor to use the first time the instance is needed.
 | 
			
		||||
 * @returns Asynchronous instance wrapper.
 | 
			
		||||
 */
 | 
			
		||||
function createAsyncInstanceWrapper<T>(lazyConstructor?: () => T | Promise<T>): AsyncInstanceWrapper<T> {
 | 
			
		||||
    let promisedInstance: CorePromisedValue<T> | null = null;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        get instance() {
 | 
			
		||||
            return promisedInstance?.value ?? undefined;
 | 
			
		||||
        },
 | 
			
		||||
        async getInstance() {
 | 
			
		||||
            if (!promisedInstance) {
 | 
			
		||||
                promisedInstance = new CorePromisedValue();
 | 
			
		||||
 | 
			
		||||
                if (lazyConstructor) {
 | 
			
		||||
                    const instance = await lazyConstructor();
 | 
			
		||||
 | 
			
		||||
                    promisedInstance.resolve(instance);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return promisedInstance;
 | 
			
		||||
        },
 | 
			
		||||
        async getProperty(property) {
 | 
			
		||||
            const instance = await this.getInstance();
 | 
			
		||||
 | 
			
		||||
            return instance[property];
 | 
			
		||||
        },
 | 
			
		||||
        setInstance(instance) {
 | 
			
		||||
            if (!promisedInstance) {
 | 
			
		||||
                promisedInstance = new CorePromisedValue();
 | 
			
		||||
            } else if (promisedInstance.isSettled()) {
 | 
			
		||||
                promisedInstance.reset();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            promisedInstance.resolve(instance);
 | 
			
		||||
        },
 | 
			
		||||
        resetInstance() {
 | 
			
		||||
            if (!promisedInstance) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            promisedInstance.reset();
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Asynchronous instance wrapper.
 | 
			
		||||
 */
 | 
			
		||||
export interface AsyncInstanceWrapper<T> {
 | 
			
		||||
    instance?: T;
 | 
			
		||||
    getInstance(): Promise<T>;
 | 
			
		||||
    getProperty<P extends keyof T>(property: P): Promise<T[P]>;
 | 
			
		||||
    setInstance(instance: T): void;
 | 
			
		||||
    resetInstance(): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Asynchronous version of a method.
 | 
			
		||||
 */
 | 
			
		||||
export type AsyncMethod<T> =
 | 
			
		||||
    T extends (...args: infer Params) => infer Return
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
        ? T extends (...args: Params) => Promise<any>
 | 
			
		||||
            ? T
 | 
			
		||||
            : (...args: Params) => Promise<Return>
 | 
			
		||||
        : never;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Asynchronous instance.
 | 
			
		||||
 *
 | 
			
		||||
 * All methods are converted to their asynchronous version, and properties are available asynchronously using
 | 
			
		||||
 * the getProperty method.
 | 
			
		||||
 */
 | 
			
		||||
export type AsyncInstance<T> = AsyncInstanceWrapper<T> & {
 | 
			
		||||
    [k in keyof T]: AsyncMethod<T[k]>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Create an asynchronous instance proxy, where all methods will be callable directly but will become asynchronous. If the
 | 
			
		||||
 * underlying instance hasn't been set, methods will be resolved once it is.
 | 
			
		||||
 *
 | 
			
		||||
 * @param lazyConstructor Constructor to use the first time the instance is needed.
 | 
			
		||||
 * @returns Asynchronous instance.
 | 
			
		||||
 */
 | 
			
		||||
export function asyncInstance<T>(lazyConstructor?: () => T | Promise<T>): AsyncInstance<T> {
 | 
			
		||||
    const wrapper = createAsyncInstanceWrapper<T>(lazyConstructor);
 | 
			
		||||
 | 
			
		||||
    return new Proxy(wrapper, {
 | 
			
		||||
        get: (target, property, receiver) => {
 | 
			
		||||
            if (property in target) {
 | 
			
		||||
                return Reflect.get(target, property, receiver);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return async (...args: unknown[]) => {
 | 
			
		||||
                const instance = await wrapper.getInstance();
 | 
			
		||||
 | 
			
		||||
                return instance[property](...args);
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
    }) as AsyncInstance<T>;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								src/core/utils/lazy-map.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/core/utils/lazy-map.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
			
		||||
// (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.
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Lazy map.
 | 
			
		||||
 *
 | 
			
		||||
 * Lazy maps are empty by default, but entries are generated lazily when accessed.
 | 
			
		||||
 */
 | 
			
		||||
export type LazyMap<T> = Record<string, T>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Create a map that will initialize entries lazily with the given constructor.
 | 
			
		||||
 *
 | 
			
		||||
 * @param lazyConstructor Constructor to use the first time an entry is accessed.
 | 
			
		||||
 * @returns Lazy map.
 | 
			
		||||
 */
 | 
			
		||||
export function lazyMap<T>(lazyConstructor: (key: string) => T): LazyMap<T> {
 | 
			
		||||
    const instances = {};
 | 
			
		||||
 | 
			
		||||
    return new Proxy(instances, {
 | 
			
		||||
        get(target, property, receiver) {
 | 
			
		||||
            if (!(property in instances)) {
 | 
			
		||||
                target[property] = lazyConstructor(property.toString());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Reflect.get(target, property, receiver);
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/types/config.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								src/types/config.d.ts
									
									
									
									
										vendored
									
									
								
							@ -17,6 +17,7 @@ import { CoreMainMenuLocalizedCustomItem } from '@features/mainmenu/services/mai
 | 
			
		||||
import { CoreSitesDemoSiteData } from '@services/sites';
 | 
			
		||||
import { OpenFileAction } from '@services/utils/utils';
 | 
			
		||||
import { CoreLoginSiteSelectorListMethod } from '@features/login/services/login-helper';
 | 
			
		||||
import { CoreDatabaseConfiguration } from '@classes/database/database-table-proxy';
 | 
			
		||||
 | 
			
		||||
/* eslint-disable @typescript-eslint/naming-convention */
 | 
			
		||||
 | 
			
		||||
@ -31,6 +32,8 @@ export interface EnvironmentConfig {
 | 
			
		||||
    cache_update_frequency_rarely: number;
 | 
			
		||||
    default_lang: string;
 | 
			
		||||
    languages: Record<string, string>;
 | 
			
		||||
    databaseOptimizations?: Partial<CoreDatabaseConfiguration>;
 | 
			
		||||
    databaseTableOptimizations?: Record<string, Partial<CoreDatabaseConfiguration>>;
 | 
			
		||||
    wsservice: string;
 | 
			
		||||
    demo_sites: Record<string, CoreSitesDemoSiteData>;
 | 
			
		||||
    zoomlevels: Record<CoreZoomLevel, number>;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user