Merge pull request #3091 from NoelDeMartin/MOBILE-3977
MOBILE-3977: Database optimization strategiesmain
commit
858cf07f73
|
@ -271,6 +271,7 @@ testsConfig['rules']['padded-blocks'] = [
|
||||||
switches: 'never',
|
switches: 'never',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
testsConfig['rules']['jest/expect-expect'] = 'off';
|
||||||
testsConfig['plugins'].push('jest');
|
testsConfig['plugins'].push('jest');
|
||||||
testsConfig['extends'].push('plugin:jest/recommended');
|
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
|
// 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];
|
|
||||||
};
|
|
|
@ -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',
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
this._reject(reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset status and value.
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
delete this._resolvedValue;
|
||||||
|
delete this._rejectedReason;
|
||||||
|
|
||||||
|
this.initPromise();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the promise and the callbacks.
|
* Initialize the promise and the callbacks.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -41,6 +41,10 @@ import { CoreLogger } from '@singletons/logger';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreIonLoadingElement } from './ion-loading';
|
import { CoreIonLoadingElement } from './ion-loading';
|
||||||
import { CoreLang } from '@services/lang';
|
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.
|
* QR Code type enumeration.
|
||||||
|
@ -103,6 +107,7 @@ export class CoreSite {
|
||||||
// Rest of variables.
|
// Rest of variables.
|
||||||
protected logger: CoreLogger;
|
protected logger: CoreLogger;
|
||||||
protected db?: SQLiteDB;
|
protected db?: SQLiteDB;
|
||||||
|
protected cacheTable: AsyncInstance<CoreDatabaseTable<CoreSiteWSCacheRecord>>;
|
||||||
protected cleanUnicode = false;
|
protected cleanUnicode = false;
|
||||||
protected lastAutoLogin = 0;
|
protected lastAutoLogin = 0;
|
||||||
protected offlineDisabled = false;
|
protected offlineDisabled = false;
|
||||||
|
@ -136,6 +141,11 @@ export class CoreSite {
|
||||||
) {
|
) {
|
||||||
this.logger = CoreLogger.getInstance('CoreSite');
|
this.logger = CoreLogger.getInstance('CoreSite');
|
||||||
this.siteUrl = CoreUrlUtils.removeUrlParams(this.siteUrl); // Make sure the URL doesn't have params.
|
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.setInfo(infos);
|
||||||
this.calculateOfflineDisabled();
|
this.calculateOfflineDisabled();
|
||||||
|
|
||||||
|
@ -920,8 +930,7 @@ export class CoreSite {
|
||||||
preSets: CoreSiteWSPreSets,
|
preSets: CoreSiteWSPreSets,
|
||||||
emergency?: boolean,
|
emergency?: boolean,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const db = this.db;
|
if (!this.db || !preSets.getFromCache) {
|
||||||
if (!db || !preSets.getFromCache) {
|
|
||||||
throw new CoreError('Get from cache is disabled.');
|
throw new CoreError('Get from cache is disabled.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -929,11 +938,11 @@ export class CoreSite {
|
||||||
let entry: CoreSiteWSCacheRecord | undefined;
|
let entry: CoreSiteWSCacheRecord | undefined;
|
||||||
|
|
||||||
if (preSets.getCacheUsingCacheKey || (emergency && preSets.getEmergencyCacheUsingCacheKey)) {
|
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) {
|
if (!entries.length) {
|
||||||
// Cache key not found, get by params sent.
|
// Cache key not found, get by params sent.
|
||||||
entry = await db.getRecord(CoreSite.WS_CACHE_TABLE, { id });
|
entry = await this.cacheTable.getOneByPrimaryKey({ id });
|
||||||
} else {
|
} else {
|
||||||
if (entries.length > 1) {
|
if (entries.length > 1) {
|
||||||
// More than one entry found. Search the one with same ID as this call.
|
// More than one entry found. Search the one with same ID as this call.
|
||||||
|
@ -945,7 +954,7 @@ export class CoreSite {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
entry = await db.getRecord(CoreSite.WS_CACHE_TABLE, { id });
|
entry = await this.cacheTable.getOneByPrimaryKey({ id });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry === undefined) {
|
if (entry === undefined) {
|
||||||
|
@ -996,12 +1005,18 @@ export class CoreSite {
|
||||||
extraClause = ' AND componentId = ?';
|
extraClause = ' AND componentId = ?';
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = <number> await this.getDb().getFieldSql(
|
return this.cacheTable.reduce(
|
||||||
'SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE + ' WHERE component = ?' + extraClause,
|
{
|
||||||
params,
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
protected async saveToCache(method: string, data: any, response: any, preSets: CoreSiteWSPreSets): Promise<void> {
|
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) {
|
if (preSets.uniqueCacheKey) {
|
||||||
// Cache key must be unique, delete all entries with same cache key.
|
// Cache key must be unique, delete all entries with same cache key.
|
||||||
await CoreUtils.ignoreErrors(this.deleteFromCache(method, data, preSets, true));
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
protected async deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean): Promise<void> {
|
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);
|
const id = this.getCacheId(method, data);
|
||||||
|
|
||||||
if (allCacheKey) {
|
if (allCacheKey) {
|
||||||
await this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, { key: preSets.cacheKey });
|
await this.cacheTable.delete({ key: preSets.cacheKey });
|
||||||
} else {
|
} else {
|
||||||
await this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, { id });
|
await this.cacheTable.deleteByPrimaryKey({ id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1084,18 +1091,13 @@ export class CoreSite {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.db) {
|
const params = { component };
|
||||||
throw new CoreError('Site DB not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
component,
|
|
||||||
};
|
|
||||||
if (componentId) {
|
if (componentId) {
|
||||||
params['componentId'] = 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.
|
* @return Promise resolved when the cache entries are invalidated.
|
||||||
*/
|
*/
|
||||||
async invalidateWsCache(): Promise<void> {
|
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);
|
this.logger.debug('Invalidate all the cache for site: ' + this.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.db.updateRecords(CoreSite.WS_CACHE_TABLE, { expirationTime: 0 });
|
await this.cacheTable.update({ expirationTime: 0 });
|
||||||
} finally {
|
} finally {
|
||||||
CoreEvents.trigger(CoreEvents.WS_CACHE_INVALIDATED, {}, this.getId());
|
CoreEvents.trigger(CoreEvents.WS_CACHE_INVALIDATED, {}, this.getId());
|
||||||
}
|
}
|
||||||
|
@ -1147,16 +1145,13 @@ export class CoreSite {
|
||||||
* @return Promise resolved when the cache entries are invalidated.
|
* @return Promise resolved when the cache entries are invalidated.
|
||||||
*/
|
*/
|
||||||
async invalidateWsCacheForKey(key: string): Promise<void> {
|
async invalidateWsCacheForKey(key: string): Promise<void> {
|
||||||
if (!this.db) {
|
|
||||||
throw new CoreError('Site DB not initialized');
|
|
||||||
}
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug('Invalidate cache for key: ' + key);
|
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.
|
* @return Promise resolved when the cache entries are invalidated.
|
||||||
*/
|
*/
|
||||||
async invalidateWsCacheForKeyStartingWith(key: string): Promise<void> {
|
async invalidateWsCacheForKeyStartingWith(key: string): Promise<void> {
|
||||||
if (!this.db) {
|
|
||||||
throw new CoreError('Site DB not initialized');
|
|
||||||
}
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug('Invalidate cache for key starting with: ' + key);
|
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.cacheTable.updateWhere({ expirationTime: 0 }, {
|
||||||
|
sql: 'key LIKE ?',
|
||||||
await this.db.execute(sql, [key + '%']);
|
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)
|
* @return Promise resolved with the total size of all data in the cache table (bytes)
|
||||||
*/
|
*/
|
||||||
async getCacheUsage(): Promise<number> {
|
async getCacheUsage(): Promise<number> {
|
||||||
const size = <number> await this.getDb().getFieldSql('SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE);
|
return this.cacheTable.reduce({
|
||||||
|
sql: 'SUM(length(data))',
|
||||||
return size;
|
js: (size, record) => size + record.data.length,
|
||||||
|
jsInitialValue: 0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1206,4 +1206,4 @@ export type SQLiteDBQueryParams = {
|
||||||
params: SQLiteDBRecordValue[];
|
params: SQLiteDBRecordValue[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type SQLiteDBRecordValue = number | string;
|
export type SQLiteDBRecordValue = number | string;
|
||||||
|
|
|
@ -13,7 +13,12 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { mock } from '@/testing/utils';
|
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';
|
import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb';
|
||||||
|
|
||||||
interface User extends SQLiteDBRecordValues {
|
interface User extends SQLiteDBRecordValues {
|
||||||
|
@ -22,63 +27,79 @@ interface User extends SQLiteDBRecordValues {
|
||||||
surname: string;
|
surname: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UsersTable extends CoreDatabaseTable<User> {
|
function userMatches(user: User, conditions: Partial<User>) {
|
||||||
|
return !Object.entries(conditions).some(([column, value]) => user[column] !== value);
|
||||||
protected table = 'users';
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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));
|
||||||
|
|
||||||
let records: User[];
|
if (!record) {
|
||||||
let db: SQLiteDB;
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
return record as unknown as T;
|
||||||
records = [];
|
},
|
||||||
db = mock<SQLiteDB>({
|
getRecords: async <T>(_, conditions) => records.filter(record => userMatches(record, conditions)) as unknown as T[],
|
||||||
getRecords: async <T>() => records as unknown as T[],
|
getAllRecords: async <T>() => records as unknown as T[],
|
||||||
deleteRecords: async () => 0,
|
deleteRecords: async (_, conditions) => {
|
||||||
insertRecord: async () => 0,
|
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');
|
||||||
|
|
||||||
it('reads all records on create', async () => {
|
return [records, database, table];
|
||||||
await UsersTable.create(db);
|
}
|
||||||
|
|
||||||
expect(db.getRecords).toHaveBeenCalledWith('users');
|
async function testFindItems(records: User[], table: CoreDatabaseTable<User>) {
|
||||||
});
|
|
||||||
|
|
||||||
it('finds items', async () => {
|
|
||||||
const john = { id: 1, name: 'John', surname: 'Doe' };
|
const john = { id: 1, name: 'John', surname: 'Doe' };
|
||||||
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
|
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
|
||||||
|
|
||||||
records.push(john);
|
records.push(john);
|
||||||
records.push(amy);
|
records.push(amy);
|
||||||
|
|
||||||
const table = await UsersTable.create(db);
|
await table.initialize();
|
||||||
|
|
||||||
expect(table.findByPrimaryKey({ id: 1 })).toEqual(john);
|
await expect(table.getOneByPrimaryKey({ id: 1 })).resolves.toEqual(john);
|
||||||
expect(table.findByPrimaryKey({ id: 2 })).toEqual(amy);
|
await expect(table.getOneByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
|
||||||
expect(table.find({ surname: 'Doe', name: 'John' })).toEqual(john);
|
await expect(table.getOne({ surname: 'Doe', name: 'John' })).resolves.toEqual(john);
|
||||||
expect(table.find({ surname: 'Doe', name: 'Amy' })).toEqual(amy);
|
await expect(table.getOne({ surname: 'Doe', name: 'Amy' })).resolves.toEqual(amy);
|
||||||
});
|
}
|
||||||
|
|
||||||
it('inserts items', async () => {
|
async function testInsertItems(records: User[], database: SQLiteDB, table: CoreDatabaseTable<User>) {
|
||||||
// Arrange.
|
// Arrange.
|
||||||
const john = { id: 1, name: 'John', surname: 'Doe' };
|
const john = { id: 1, name: 'John', surname: 'Doe' };
|
||||||
|
|
||||||
// Act.
|
await table.initialize();
|
||||||
const table = await UsersTable.create(db);
|
|
||||||
|
|
||||||
|
// Act.
|
||||||
await table.insert(john);
|
await table.insert(john);
|
||||||
|
|
||||||
// Assert.
|
// Assert.
|
||||||
expect(db.insertRecord).toHaveBeenCalledWith('users', john);
|
expect(database.insertRecord).toHaveBeenCalledWith('users', john);
|
||||||
|
|
||||||
expect(table.findByPrimaryKey({ id: 1 })).toEqual(john);
|
await expect(table.getOneByPrimaryKey({ id: 1 })).resolves.toEqual(john);
|
||||||
});
|
}
|
||||||
|
|
||||||
it('deletes items', async () => {
|
async function testDeleteItems(records: User[], database: SQLiteDB, table: CoreDatabaseTable<User>) {
|
||||||
// Arrange.
|
// Arrange.
|
||||||
const john = { id: 1, name: 'John', surname: 'Doe' };
|
const john = { id: 1, name: 'John', surname: 'Doe' };
|
||||||
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
|
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
|
||||||
|
@ -88,20 +109,20 @@ describe('CoreDatabaseTable', () => {
|
||||||
records.push(amy);
|
records.push(amy);
|
||||||
records.push(jane);
|
records.push(jane);
|
||||||
|
|
||||||
// Act.
|
await table.initialize();
|
||||||
const table = await UsersTable.create(db);
|
|
||||||
|
|
||||||
|
// Act.
|
||||||
await table.delete({ surname: 'Doe' });
|
await table.delete({ surname: 'Doe' });
|
||||||
|
|
||||||
// Assert.
|
// Assert.
|
||||||
expect(db.deleteRecords).toHaveBeenCalledWith('users', { surname: 'Doe' });
|
expect(database.deleteRecords).toHaveBeenCalledWith('users', { surname: 'Doe' });
|
||||||
|
|
||||||
expect(table.findByPrimaryKey({ id: 1 })).toBeNull();
|
await expect(table.getOneByPrimaryKey({ id: 1 })).rejects.toThrow();
|
||||||
expect(table.findByPrimaryKey({ id: 2 })).toBeNull();
|
await expect(table.getOneByPrimaryKey({ id: 2 })).rejects.toThrow();
|
||||||
expect(table.findByPrimaryKey({ id: 3 })).toEqual(jane);
|
await expect(table.getOneByPrimaryKey({ id: 3 })).resolves.toEqual(jane);
|
||||||
});
|
}
|
||||||
|
|
||||||
it('deletes items by primary key', async () => {
|
async function testDeleteItemsByPrimaryKey(records: User[], database: SQLiteDB, table: CoreDatabaseTable<User>) {
|
||||||
// Arrange.
|
// Arrange.
|
||||||
const john = { id: 1, name: 'John', surname: 'Doe' };
|
const john = { id: 1, name: 'John', surname: 'Doe' };
|
||||||
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
|
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
|
||||||
|
@ -109,16 +130,94 @@ describe('CoreDatabaseTable', () => {
|
||||||
records.push(john);
|
records.push(john);
|
||||||
records.push(amy);
|
records.push(amy);
|
||||||
|
|
||||||
// Act.
|
await table.initialize();
|
||||||
const table = await UsersTable.create(db);
|
|
||||||
|
|
||||||
|
// Act.
|
||||||
await table.deleteByPrimaryKey({ id: 1 });
|
await table.deleteByPrimaryKey({ id: 1 });
|
||||||
|
|
||||||
// Assert.
|
// Assert.
|
||||||
expect(db.deleteRecords).toHaveBeenCalledWith('users', { id: 1 });
|
expect(database.deleteRecords).toHaveBeenCalledWith('users', { id: 1 });
|
||||||
|
|
||||||
expect(table.findByPrimaryKey({ id: 1 })).toBeNull();
|
await expect(table.getOneByPrimaryKey({ id: 1 })).rejects.toThrow();
|
||||||
expect(table.findByPrimaryKey({ id: 2 })).toEqual(amy);
|
await expect(table.getOneByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CoreDatabaseTable with eager caching', () => {
|
||||||
|
|
||||||
|
let records: User[];
|
||||||
|
let database: SQLiteDB;
|
||||||
|
let table: CoreDatabaseTable<User>;
|
||||||
|
|
||||||
|
beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.Eager }));
|
||||||
|
|
||||||
|
it('reads all records on initialization', async () => {
|
||||||
|
await table.initialize();
|
||||||
|
|
||||||
|
expect(database.getAllRecords).toHaveBeenCalledWith('users');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('finds items', async () => {
|
||||||
|
await testFindItems(records, table);
|
||||||
|
|
||||||
|
expect(database.getRecord).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
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 CONFIG = { ...envJson.config } as unknown as EnvironmentConfig; // Data parsed from config.json files.
|
||||||
static readonly BUILD = envJson.build as unknown as EnvironmentBuild; // Build info.
|
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 {
|
interface EnvironmentBuild {
|
||||||
|
|
|
@ -15,11 +15,13 @@
|
||||||
import { CoreFilepool } from '@services/filepool';
|
import { CoreFilepool } from '@services/filepool';
|
||||||
import { CoreLang } from '@services/lang';
|
import { CoreLang } from '@services/lang';
|
||||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreUpdateManager } from '@services/update-manager';
|
import { CoreUpdateManager } from '@services/update-manager';
|
||||||
|
|
||||||
export default async function(): Promise<void> {
|
export default async function(): Promise<void> {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
CoreFilepool.initialize(),
|
CoreFilepool.initialize(),
|
||||||
|
CoreSites.initialize(),
|
||||||
CoreLang.initialize(),
|
CoreLang.initialize(),
|
||||||
CoreLocalNotifications.initialize(),
|
CoreLocalNotifications.initialize(),
|
||||||
CoreUpdateManager.initialize(),
|
CoreUpdateManager.initialize(),
|
||||||
|
|
|
@ -12,13 +12,29 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { EnvironmentConfig } from '@/types/config';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy';
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
|
import { APP_SCHEMA, ConfigDBEntry, CONFIG_TABLE_NAME } from '@services/database/config';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
import { CONFIG_TABLE_NAME, APP_SCHEMA, ConfigDBEntry } from '@services/database/config';
|
import { CoreConstants } from '../constants';
|
||||||
import { CoreDatabaseTable } from '@classes/database-table';
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { CorePromisedValue } from '@classes/promised-value';
|
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.
|
* Factory to provide access to dynamic and permanent config and settings.
|
||||||
|
@ -27,11 +43,10 @@ import { CorePromisedValue } from '@classes/promised-value';
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class CoreConfigProvider {
|
export class CoreConfigProvider {
|
||||||
|
|
||||||
protected dbTable: CorePromisedValue<CoreConfigTable>;
|
static readonly ENVIRONMENT_UPDATED = 'environment_updated';
|
||||||
|
|
||||||
constructor() {
|
protected table = asyncInstance<CoreDatabaseTable<ConfigDBEntry, 'name'>>();
|
||||||
this.dbTable = new CorePromisedValue();
|
protected defaultEnvironment?: EnvironmentConfig;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize database.
|
* Initialize database.
|
||||||
|
@ -43,10 +58,16 @@ export class CoreConfigProvider {
|
||||||
// Ignore errors.
|
// Ignore errors.
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = CoreApp.getDB();
|
const table = new CoreDatabaseTableProxy<ConfigDBEntry, 'name'>(
|
||||||
const table = await CoreConfigTable.create(db);
|
{ 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.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async delete(name: string): Promise<void> {
|
async delete(name: string): Promise<void> {
|
||||||
const table = await this.dbTable;
|
await this.table.deleteByPrimaryKey({ name });
|
||||||
|
|
||||||
await table.deleteByPrimaryKey({ name });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -69,18 +88,17 @@ export class CoreConfigProvider {
|
||||||
* @return Resolves upon success along with the config data. Reject on failure.
|
* @return Resolves upon success along with the config data. Reject on failure.
|
||||||
*/
|
*/
|
||||||
async get<T>(name: string, defaultValue?: T): Promise<T> {
|
async get<T>(name: string, defaultValue?: T): Promise<T> {
|
||||||
const table = await this.dbTable;
|
try {
|
||||||
const record = table.findByPrimaryKey({ name });
|
const record = await this.table.getOneByPrimaryKey({ name });
|
||||||
|
|
||||||
if (record !== null) {
|
|
||||||
return record.value;
|
return record.value;
|
||||||
}
|
} catch (error) {
|
||||||
|
|
||||||
if (defaultValue !== undefined) {
|
if (defaultValue !== undefined) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Couldn't get config with name '${name}'`);
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,21 +109,36 @@ export class CoreConfigProvider {
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async set(name: string, value: number | string): Promise<void> {
|
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);
|
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();
|
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.
|
* Log a query.
|
||||||
*
|
*
|
||||||
|
|
|
@ -48,6 +48,10 @@ import {
|
||||||
} from '@services/database/filepool';
|
} from '@services/database/filepool';
|
||||||
import { CoreFileHelper } from './file-helper';
|
import { CoreFileHelper } from './file-helper';
|
||||||
import { CoreUrl } from '@singletons/url';
|
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.
|
* 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_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 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))';
|
'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 logger: CoreLogger;
|
||||||
protected queueState = CoreFilepoolProvider.QUEUE_PAUSED;
|
protected queueState = CoreFilepoolProvider.QUEUE_PAUSED;
|
||||||
protected urlAttributes: RegExp[] = [
|
protected urlAttributes: RegExp[] = [
|
||||||
|
@ -94,10 +102,20 @@ export class CoreFilepoolProvider {
|
||||||
// Variables for DB.
|
// Variables for DB.
|
||||||
protected appDB: Promise<SQLiteDB>;
|
protected appDB: Promise<SQLiteDB>;
|
||||||
protected resolveAppDB!: (appDB: SQLiteDB) => void;
|
protected resolveAppDB!: (appDB: SQLiteDB) => void;
|
||||||
|
protected filesTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreFilepoolFileEntry, 'fileId'>>>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
|
this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
|
||||||
this.logger = CoreLogger.getInstance('CoreFilepoolProvider');
|
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());
|
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,
|
...data,
|
||||||
};
|
};
|
||||||
|
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
await this.filesTables[siteId].insert(record);
|
||||||
|
|
||||||
await db.insertRecord(FILES_TABLE_NAME, record);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -560,11 +586,11 @@ export class CoreFilepoolProvider {
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
const db = await CoreSites.getSiteDb(siteId);
|
||||||
|
|
||||||
// Read the data first to be able to notify the deletions.
|
// 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);
|
const filesLinks = await db.getAllRecords<CoreFilepoolLinksRecord>(LINKS_TABLE_NAME);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.deleteRecords(FILES_TABLE_NAME),
|
this.filesTables[siteId].delete(),
|
||||||
db.deleteRecords(LINKS_TABLE_NAME),
|
db.deleteRecords(LINKS_TABLE_NAME),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -1125,7 +1151,7 @@ export class CoreFilepoolProvider {
|
||||||
// Minor problem: file will remain in the filesystem once downloaded again.
|
// Minor problem: file will remain in the filesystem once downloaded again.
|
||||||
this.logger.debug('Staled file with no extension ' + entry.fileId);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1135,7 +1161,7 @@ export class CoreFilepoolProvider {
|
||||||
entry.fileId = CoreMimetypeUtils.removeExtension(fileId);
|
entry.fileId = CoreMimetypeUtils.removeExtension(fileId);
|
||||||
entry.extension = extension;
|
entry.extension = extension;
|
||||||
|
|
||||||
await db.updateRecords(FILES_TABLE_NAME, entry, { fileId });
|
await this.filesTables[siteId].update(entry, { fileId });
|
||||||
if (entry.fileId == fileId) {
|
if (entry.fileId == fileId) {
|
||||||
// File ID hasn't changed, we're done.
|
// File ID hasn't changed, we're done.
|
||||||
this.logger.debug('Removed extesion ' + extension + ' from file ' + entry.fileId);
|
this.logger.debug('Removed extesion ' + extension + ' from file ' + entry.fileId);
|
||||||
|
@ -1401,10 +1427,7 @@ export class CoreFilepoolProvider {
|
||||||
|
|
||||||
await Promise.all(items.map(async (item) => {
|
await Promise.all(items.map(async (item) => {
|
||||||
try {
|
try {
|
||||||
const fileEntry = await db.getRecord<CoreFilepoolFileEntry>(
|
const fileEntry = await this.filesTables[siteId].getOneByPrimaryKey({ fileId: item.fileId });
|
||||||
FILES_TABLE_NAME,
|
|
||||||
{ fileId: item.fileId },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fileEntry) {
|
if (!fileEntry) {
|
||||||
return;
|
return;
|
||||||
|
@ -2137,14 +2160,7 @@ export class CoreFilepoolProvider {
|
||||||
* @return Resolved with file object from DB on success, rejected otherwise.
|
* @return Resolved with file object from DB on success, rejected otherwise.
|
||||||
*/
|
*/
|
||||||
protected async hasFileInPool(siteId: string, fileId: string): Promise<CoreFilepoolFileEntry> {
|
protected async hasFileInPool(siteId: string, fileId: string): Promise<CoreFilepoolFileEntry> {
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
return this.filesTables[siteId].getOneByPrimaryKey({ fileId });
|
||||||
const entry = await db.getRecord<CoreFilepoolFileEntry>(FILES_TABLE_NAME, { fileId });
|
|
||||||
|
|
||||||
if (entry === undefined) {
|
|
||||||
throw new CoreError('File not found in filepool.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2176,11 +2192,15 @@ export class CoreFilepoolProvider {
|
||||||
* @return Resolved on success.
|
* @return Resolved on success.
|
||||||
*/
|
*/
|
||||||
async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise<void> {
|
async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise<void> {
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
onlyUnknown
|
||||||
|
? await this.filesTables[siteId].updateWhere(
|
||||||
const where = onlyUnknown ? CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE : undefined;
|
{ stale: 1 },
|
||||||
|
{
|
||||||
await db.updateRecordsWhere(FILES_TABLE_NAME, { stale: 1 }, where);
|
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 file = await this.fixPluginfileURL(siteId, fileUrl);
|
||||||
const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file));
|
const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file));
|
||||||
|
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
await this.filesTables[siteId].update({ stale: 1 }, { fileId });
|
||||||
|
|
||||||
await db.updateRecords(FILES_TABLE_NAME, { stale: 1 }, { fileId });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2221,7 +2239,6 @@ export class CoreFilepoolProvider {
|
||||||
onlyUnknown: boolean = true,
|
onlyUnknown: boolean = true,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
const db = await CoreSites.getSiteDb(siteId);
|
||||||
|
|
||||||
const items = await this.getComponentFiles(db, component, componentId);
|
const items = await this.getComponentFiles(db, component, componentId);
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
|
@ -2229,6 +2246,8 @@ export class CoreFilepoolProvider {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
siteId = siteId ?? CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
const fileIds = items.map((item) => item.fileId);
|
const fileIds = items.map((item) => item.fileId);
|
||||||
|
|
||||||
const whereAndParams = db.getInOrEqual(fileIds);
|
const whereAndParams = db.getInOrEqual(fileIds);
|
||||||
|
@ -2236,10 +2255,19 @@ export class CoreFilepoolProvider {
|
||||||
whereAndParams.sql = 'fileId ' + whereAndParams.sql;
|
whereAndParams.sql = 'fileId ' + whereAndParams.sql;
|
||||||
|
|
||||||
if (onlyUnknown) {
|
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> {
|
protected async removeFileById(siteId: string, fileId: string): Promise<void> {
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
const db = await CoreSites.getSiteDb(siteId);
|
||||||
|
|
||||||
// Get the path to the file first since it relies on the file object stored in the pool.
|
// 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.
|
// Don't use getFilePath to prevent performing 2 DB requests.
|
||||||
let path = this.getFilepoolFolderPath(siteId) + '/' + fileId;
|
let path = this.getFilepoolFolderPath(siteId) + '/' + fileId;
|
||||||
|
@ -2682,7 +2711,7 @@ export class CoreFilepoolProvider {
|
||||||
const promises: Promise<unknown>[] = [];
|
const promises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
// Remove entry from filepool store.
|
// Remove entry from filepool store.
|
||||||
promises.push(db.deleteRecords(FILES_TABLE_NAME, conditions));
|
promises.push(this.filesTables[siteId].delete(conditions));
|
||||||
|
|
||||||
// Remove links.
|
// Remove links.
|
||||||
promises.push(db.deleteRecords(LINKS_TABLE_NAME, conditions));
|
promises.push(db.deleteRecords(LINKS_TABLE_NAME, conditions));
|
||||||
|
|
|
@ -32,7 +32,7 @@ import {
|
||||||
CoreSitePublicConfigResponse,
|
CoreSitePublicConfigResponse,
|
||||||
CoreSiteInfoResponse,
|
CoreSiteInfoResponse,
|
||||||
} from '@classes/site';
|
} from '@classes/site';
|
||||||
import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb';
|
import { SQLiteDB, SQLiteDBRecordValues, SQLiteDBTableSchema } from '@classes/sqlitedb';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import { CoreSiteError } from '@classes/errors/siteerror';
|
import { CoreSiteError } from '@classes/errors/siteerror';
|
||||||
import { makeSingleton, Translate, Http } from '@singletons';
|
import { makeSingleton, Translate, Http } from '@singletons';
|
||||||
|
@ -57,6 +57,9 @@ import { CoreErrorWithTitle } from '@classes/errors/errorwithtitle';
|
||||||
import { CoreAjaxError } from '@classes/errors/ajaxerror';
|
import { CoreAjaxError } from '@classes/errors/ajaxerror';
|
||||||
import { CoreAjaxWSError } from '@classes/errors/ajaxwserror';
|
import { CoreAjaxWSError } from '@classes/errors/ajaxwserror';
|
||||||
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
|
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');
|
export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
|
||||||
|
|
||||||
|
@ -85,6 +88,7 @@ export class CoreSitesProvider {
|
||||||
// Variables for DB.
|
// Variables for DB.
|
||||||
protected appDB: Promise<SQLiteDB>;
|
protected appDB: Promise<SQLiteDB>;
|
||||||
protected resolveAppDB!: (appDB: SQLiteDB) => void;
|
protected resolveAppDB!: (appDB: SQLiteDB) => void;
|
||||||
|
protected siteTables: Record<string, Record<string, CorePromisedValue<CoreDatabaseTable>>> = {};
|
||||||
|
|
||||||
constructor(@Optional() @Inject(CORE_SITE_SCHEMAS) siteSchemas: CoreSiteSchema[][] = []) {
|
constructor(@Optional() @Inject(CORE_SITE_SCHEMAS) siteSchemas: CoreSiteSchema[][] = []) {
|
||||||
this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
|
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.
|
* Initialize database.
|
||||||
*/
|
*/
|
||||||
|
@ -112,6 +135,49 @@ export class CoreSitesProvider {
|
||||||
this.resolveAppDB(CoreApp.getDB());
|
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.
|
* Get the demo data for a certain "name" if it is a demo site.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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>;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import { CoreMainMenuLocalizedCustomItem } from '@features/mainmenu/services/mai
|
||||||
import { CoreSitesDemoSiteData } from '@services/sites';
|
import { CoreSitesDemoSiteData } from '@services/sites';
|
||||||
import { OpenFileAction } from '@services/utils/utils';
|
import { OpenFileAction } from '@services/utils/utils';
|
||||||
import { CoreLoginSiteSelectorListMethod } from '@features/login/services/login-helper';
|
import { CoreLoginSiteSelectorListMethod } from '@features/login/services/login-helper';
|
||||||
|
import { CoreDatabaseConfiguration } from '@classes/database/database-table-proxy';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
|
@ -31,6 +32,8 @@ export interface EnvironmentConfig {
|
||||||
cache_update_frequency_rarely: number;
|
cache_update_frequency_rarely: number;
|
||||||
default_lang: string;
|
default_lang: string;
|
||||||
languages: Record<string, string>;
|
languages: Record<string, string>;
|
||||||
|
databaseOptimizations?: Partial<CoreDatabaseConfiguration>;
|
||||||
|
databaseTableOptimizations?: Record<string, Partial<CoreDatabaseConfiguration>>;
|
||||||
wsservice: string;
|
wsservice: string;
|
||||||
demo_sites: Record<string, CoreSitesDemoSiteData>;
|
demo_sites: Record<string, CoreSitesDemoSiteData>;
|
||||||
zoomlevels: Record<CoreZoomLevel, number>;
|
zoomlevels: Record<CoreZoomLevel, number>;
|
||||||
|
|
Loading…
Reference in New Issue