MOBILE-3977 core: Refactor database table wrapper
Decouple caching behaviour into subclasses using a configurable proxymain
parent
cb5508fc72
commit
808a242cbc
|
@ -1,194 +0,0 @@
|
||||||
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import { SQLiteDB, SQLiteDBRecordValues } from './sqlitedb';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database table wrapper used to improve performance by caching all data in memory
|
|
||||||
* for faster read operations.
|
|
||||||
*/
|
|
||||||
export abstract class CoreDatabaseTable<
|
|
||||||
DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues,
|
|
||||||
PrimaryKeyColumns extends keyof DBRecord = 'id',
|
|
||||||
PrimaryKey extends GetPrimaryKey<DBRecord, PrimaryKeyColumns> = GetPrimaryKey<DBRecord, PrimaryKeyColumns>
|
|
||||||
> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an instance.
|
|
||||||
*
|
|
||||||
* @param db Database connection.
|
|
||||||
* @returns Instance.
|
|
||||||
*/
|
|
||||||
static async create<This extends AnyCoreDatabaseTable>(this: CoreDatabaseTableConstructor<This>, db: SQLiteDB): Promise<This> {
|
|
||||||
const instance = new this(db);
|
|
||||||
|
|
||||||
await instance.initialize();
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected db: SQLiteDB;
|
|
||||||
protected data: Record<string, DBRecord>;
|
|
||||||
protected primaryKeys: string[] = ['id'];
|
|
||||||
|
|
||||||
constructor(db: SQLiteDB) {
|
|
||||||
this.db = db;
|
|
||||||
this.data = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a record matching the given conditions.
|
|
||||||
*
|
|
||||||
* @param conditions Matching conditions.
|
|
||||||
* @returns Database record.
|
|
||||||
*/
|
|
||||||
find(conditions: Partial<DBRecord>): DBRecord | null {
|
|
||||||
return Object.values(this.data).find(record => this.recordMatches(record, conditions)) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a record by its primary key.
|
|
||||||
*
|
|
||||||
* @param primaryKey Primary key.
|
|
||||||
* @returns Database record.
|
|
||||||
*/
|
|
||||||
findByPrimaryKey(primaryKey: PrimaryKey): DBRecord | null {
|
|
||||||
return this.data[this.serializePrimaryKey(primaryKey)] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert a new record.
|
|
||||||
*
|
|
||||||
* @param record Database record.
|
|
||||||
*/
|
|
||||||
async insert(record: DBRecord): Promise<void> {
|
|
||||||
await this.db.insertRecord(this.table, record);
|
|
||||||
|
|
||||||
const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record));
|
|
||||||
|
|
||||||
this.data[primaryKey] = record;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete records matching the given conditions.
|
|
||||||
*
|
|
||||||
* @param conditions Matching conditions. If this argument is missing, all records will be deleted.
|
|
||||||
*/
|
|
||||||
async delete(conditions?: Partial<DBRecord>): Promise<void> {
|
|
||||||
if (!conditions) {
|
|
||||||
await this.db.deleteRecords(this.table);
|
|
||||||
|
|
||||||
this.data = {};
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.db.deleteRecords(this.table, conditions);
|
|
||||||
|
|
||||||
Object.entries(this.data).forEach(([id, record]) => {
|
|
||||||
if (!this.recordMatches(record, conditions)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this.data[id];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a single record identified by its primary key.
|
|
||||||
*
|
|
||||||
* @param primaryKey Record primary key.
|
|
||||||
*/
|
|
||||||
async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> {
|
|
||||||
await this.db.deleteRecords(this.table, primaryKey);
|
|
||||||
|
|
||||||
delete this.data[this.serializePrimaryKey(primaryKey)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database table name.
|
|
||||||
*/
|
|
||||||
protected abstract get table(): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize object by getting the current state of the database table.
|
|
||||||
*/
|
|
||||||
protected async initialize(): Promise<void> {
|
|
||||||
const records = await this.db.getRecords<DBRecord>(this.table);
|
|
||||||
|
|
||||||
this.data = records.reduce((data, record) => {
|
|
||||||
const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record));
|
|
||||||
|
|
||||||
data[primaryKey] = record;
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an object with the columns representing the primary key of a database record.
|
|
||||||
*
|
|
||||||
* @param record Database record.
|
|
||||||
* @returns Primary key column-value pairs.
|
|
||||||
*/
|
|
||||||
protected getPrimaryKeyFromRecord(record: DBRecord): PrimaryKey {
|
|
||||||
return this.primaryKeys.reduce((primaryKey, column) => {
|
|
||||||
primaryKey[column] = record[column];
|
|
||||||
|
|
||||||
return primaryKey;
|
|
||||||
}, {} as Record<PrimaryKeyColumns, unknown>) as PrimaryKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize a primary key with a string representation.
|
|
||||||
*
|
|
||||||
* @param primaryKey Database record primary key.
|
|
||||||
* @returns Serialized primary key.
|
|
||||||
*/
|
|
||||||
protected serializePrimaryKey(primaryKey: PrimaryKey): string {
|
|
||||||
return Object.values(primaryKey).map(value => String(value)).join('-');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether a given record matches the given conditions.
|
|
||||||
*
|
|
||||||
* @param record Database record.
|
|
||||||
* @param conditions Conditions.
|
|
||||||
* @returns Whether the record matches the conditions.
|
|
||||||
*/
|
|
||||||
protected recordMatches(record: DBRecord, conditions: Partial<DBRecord>): boolean {
|
|
||||||
return !Object.entries(conditions).some(([column, value]) => record[column] !== value);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic type to match against any concrete database table type.
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
type AnyCoreDatabaseTable = CoreDatabaseTable<SQLiteDBRecordValues, string, Record<string, any>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database table constructor.
|
|
||||||
*/
|
|
||||||
type CoreDatabaseTableConstructor<T extends AnyCoreDatabaseTable> = {
|
|
||||||
new (db: SQLiteDB): T;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Infer primary key type from database record and columns types.
|
|
||||||
*/
|
|
||||||
type GetPrimaryKey<DBRecord extends SQLiteDBRecordValues, PrimaryKeyColumns extends keyof DBRecord> = {
|
|
||||||
[column in PrimaryKeyColumns]: DBRecord[column];
|
|
||||||
};
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
|
import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb';
|
||||||
|
import { CoreDatabaseReducer, CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey } from './database-table';
|
||||||
|
import { CoreEagerDatabaseTable } from './eager-database-table';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database table proxy used to route database interactions through different implementations.
|
||||||
|
*
|
||||||
|
* This class allows using a database wrapper with different optimization strategies that can be changed at runtime.
|
||||||
|
*/
|
||||||
|
export class CoreDatabaseTableProxy<
|
||||||
|
DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues,
|
||||||
|
PrimaryKeyColumn extends keyof DBRecord = 'id',
|
||||||
|
PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn>
|
||||||
|
> extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> {
|
||||||
|
|
||||||
|
protected config: CoreDatabaseConfiguration;
|
||||||
|
protected target: CorePromisedValue<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>> = new CorePromisedValue();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
config: Partial<CoreDatabaseConfiguration>,
|
||||||
|
database: SQLiteDB,
|
||||||
|
tableName: string,
|
||||||
|
primaryKeyColumns?: PrimaryKeyColumn[],
|
||||||
|
) {
|
||||||
|
super(database, tableName, primaryKeyColumns);
|
||||||
|
|
||||||
|
this.config = { ...this.getConfigDefaults(), ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
const target = this.createTarget();
|
||||||
|
|
||||||
|
await target.initialize();
|
||||||
|
|
||||||
|
this.target.resolve(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async all(conditions?: Partial<DBRecord>): Promise<DBRecord[]> {
|
||||||
|
const target = await this.target;
|
||||||
|
|
||||||
|
return target.all(conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async find(conditions: Partial<DBRecord>): Promise<DBRecord> {
|
||||||
|
const target = await this.target;
|
||||||
|
|
||||||
|
return target.find(conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async findByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> {
|
||||||
|
const target = await this.target;
|
||||||
|
|
||||||
|
return target.findByPrimaryKey(primaryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async reduce<T>(reducer: CoreDatabaseReducer<DBRecord, T>, conditions?: CoreDatabaseConditions<DBRecord>): Promise<T> {
|
||||||
|
const target = await this.target;
|
||||||
|
|
||||||
|
return target.reduce<T>(reducer, conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async insert(record: DBRecord): Promise<void> {
|
||||||
|
const target = await this.target;
|
||||||
|
|
||||||
|
return target.insert(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async update(updates: Partial<DBRecord>, conditions?: Partial<DBRecord>): Promise<void> {
|
||||||
|
const target = await this.target;
|
||||||
|
|
||||||
|
return target.update(updates, conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async updateWhere(updates: Partial<DBRecord>, conditions: CoreDatabaseConditions<DBRecord>): Promise<void> {
|
||||||
|
const target = await this.target;
|
||||||
|
|
||||||
|
return target.updateWhere(updates, conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async delete(conditions?: Partial<DBRecord>): Promise<void> {
|
||||||
|
const target = await this.target;
|
||||||
|
|
||||||
|
return target.delete(conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> {
|
||||||
|
const target = await this.target;
|
||||||
|
|
||||||
|
return target.deleteByPrimaryKey(primaryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default configuration values.
|
||||||
|
*
|
||||||
|
* @returns Config defaults.
|
||||||
|
*/
|
||||||
|
protected getConfigDefaults(): CoreDatabaseConfiguration {
|
||||||
|
return {
|
||||||
|
cachingStrategy: CoreDatabaseCachingStrategy.None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create proxy target.
|
||||||
|
*
|
||||||
|
* @returns Target instance.
|
||||||
|
*/
|
||||||
|
protected createTarget(): CoreDatabaseTable<DBRecord, PrimaryKeyColumn> {
|
||||||
|
return this.createTable(this.config.cachingStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a database table using the given caching strategy.
|
||||||
|
*
|
||||||
|
* @param cachingStrategy Caching strategy.
|
||||||
|
* @returns Database table.
|
||||||
|
*/
|
||||||
|
protected createTable(cachingStrategy: CoreDatabaseCachingStrategy): CoreDatabaseTable<DBRecord, PrimaryKeyColumn> {
|
||||||
|
switch (cachingStrategy) {
|
||||||
|
case CoreDatabaseCachingStrategy.Eager:
|
||||||
|
return new CoreEagerDatabaseTable(this.database, this.tableName, this.primaryKeyColumns);
|
||||||
|
case CoreDatabaseCachingStrategy.None:
|
||||||
|
return new CoreDatabaseTable(this.database, this.tableName, this.primaryKeyColumns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database proxy configuration.
|
||||||
|
*/
|
||||||
|
export interface CoreDatabaseConfiguration {
|
||||||
|
cachingStrategy: CoreDatabaseCachingStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database caching strategies.
|
||||||
|
*/
|
||||||
|
export enum CoreDatabaseCachingStrategy {
|
||||||
|
Eager = 'eager',
|
||||||
|
None = 'none',
|
||||||
|
}
|
|
@ -0,0 +1,240 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { SQLiteDB, SQLiteDBRecordValue, SQLiteDBRecordValues } from '@classes/sqlitedb';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper used to interact with a database table.
|
||||||
|
*/
|
||||||
|
export class CoreDatabaseTable<
|
||||||
|
DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues,
|
||||||
|
PrimaryKeyColumn extends keyof DBRecord = 'id',
|
||||||
|
PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn>
|
||||||
|
> {
|
||||||
|
|
||||||
|
protected database: SQLiteDB;
|
||||||
|
protected tableName: string;
|
||||||
|
protected primaryKeyColumns: PrimaryKeyColumn[];
|
||||||
|
|
||||||
|
constructor(database: SQLiteDB, tableName: string, primaryKeyColumns?: PrimaryKeyColumn[]) {
|
||||||
|
this.database = database;
|
||||||
|
this.tableName = tableName;
|
||||||
|
this.primaryKeyColumns = primaryKeyColumns ?? ['id'] as PrimaryKeyColumn[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database connection.
|
||||||
|
*
|
||||||
|
* @returns Database connection.
|
||||||
|
*/
|
||||||
|
getDatabase(): SQLiteDB {
|
||||||
|
return this.database;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get table name.
|
||||||
|
*
|
||||||
|
* @returns Table name.
|
||||||
|
*/
|
||||||
|
getTableName(): string {
|
||||||
|
return this.tableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get primary key columns.
|
||||||
|
*
|
||||||
|
* @returns Primary key columns.
|
||||||
|
*/
|
||||||
|
getPrimaryKeyColumns(): PrimaryKeyColumn[] {
|
||||||
|
return this.primaryKeyColumns.slice(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize.
|
||||||
|
*/
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
// Nothing to initialize by default, override this method if necessary.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy.
|
||||||
|
*/
|
||||||
|
async destroy(): Promise<void> {
|
||||||
|
// Nothing to destroy by default, override this method if necessary.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get records matching the given conditions.
|
||||||
|
*
|
||||||
|
* @param conditions Matching conditions. If this argument is missing, all records in the table will be returned.
|
||||||
|
* @returns Database records.
|
||||||
|
*/
|
||||||
|
all(conditions?: Partial<DBRecord>): Promise<DBRecord[]> {
|
||||||
|
return conditions
|
||||||
|
? this.database.getRecords(this.tableName, conditions)
|
||||||
|
: this.database.getAllRecords(this.tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find one record matching the given conditions.
|
||||||
|
*
|
||||||
|
* @param conditions Matching conditions.
|
||||||
|
* @returns Database record.
|
||||||
|
*/
|
||||||
|
find(conditions: Partial<DBRecord>): Promise<DBRecord> {
|
||||||
|
return this.database.getRecord<DBRecord>(this.tableName, conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find one record by its primary key.
|
||||||
|
*
|
||||||
|
* @param primaryKey Primary key.
|
||||||
|
* @returns Database record.
|
||||||
|
*/
|
||||||
|
findByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> {
|
||||||
|
return this.database.getRecord<DBRecord>(this.tableName, primaryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce some records into a single value.
|
||||||
|
*
|
||||||
|
* @param reducer Reducer functions in SQL and JavaScript.
|
||||||
|
* @param conditions Matching conditions in SQL and JavaScript. If this argument is missing, all records in the table
|
||||||
|
* will be used.
|
||||||
|
* @returns Reduced value.
|
||||||
|
*/
|
||||||
|
reduce<T>(reducer: CoreDatabaseReducer<DBRecord, T>, conditions?: CoreDatabaseConditions<DBRecord>): Promise<T> {
|
||||||
|
return this.database.getFieldSql(
|
||||||
|
`SELECT ${reducer.sql} FROM ${this.tableName} ${conditions?.sql ?? ''}`,
|
||||||
|
conditions?.sqlParams,
|
||||||
|
) as unknown as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new record.
|
||||||
|
*
|
||||||
|
* @param record Database record.
|
||||||
|
*/
|
||||||
|
async insert(record: DBRecord): Promise<void> {
|
||||||
|
await this.database.insertRecord(this.tableName, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update records matching the given conditions.
|
||||||
|
*
|
||||||
|
* @param updates Record updates.
|
||||||
|
* @param conditions Matching conditions. If this argument is missing, all records will be updated.
|
||||||
|
*/
|
||||||
|
async update(updates: Partial<DBRecord>, conditions?: Partial<DBRecord>): Promise<void> {
|
||||||
|
await this.database.updateRecords(this.tableName, updates, conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update records matching the given conditions.
|
||||||
|
*
|
||||||
|
* This method should be used when it's necessary to apply complex conditions; the simple `update`
|
||||||
|
* method should be favored otherwise for better performance.
|
||||||
|
*
|
||||||
|
* @param updates Record updates.
|
||||||
|
* @param conditions Matching conditions in SQL and JavaScript.
|
||||||
|
*/
|
||||||
|
async updateWhere(updates: Partial<DBRecord>, conditions: CoreDatabaseConditions<DBRecord>): Promise<void> {
|
||||||
|
await this.database.updateRecordsWhere(this.tableName, updates, conditions.sql, conditions.sqlParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete records matching the given conditions.
|
||||||
|
*
|
||||||
|
* @param conditions Matching conditions. If this argument is missing, all records will be deleted.
|
||||||
|
*/
|
||||||
|
async delete(conditions?: Partial<DBRecord>): Promise<void> {
|
||||||
|
conditions
|
||||||
|
? await this.database.deleteRecords(this.tableName, conditions)
|
||||||
|
: await this.database.deleteRecords(this.tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a single record identified by its primary key.
|
||||||
|
*
|
||||||
|
* @param primaryKey Record primary key.
|
||||||
|
*/
|
||||||
|
async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> {
|
||||||
|
await this.database.deleteRecords(this.tableName, primaryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the primary key from a database record.
|
||||||
|
*
|
||||||
|
* @param record Database record.
|
||||||
|
* @returns Primary key.
|
||||||
|
*/
|
||||||
|
protected getPrimaryKeyFromRecord(record: DBRecord): PrimaryKey {
|
||||||
|
return this.primaryKeyColumns.reduce((primaryKey, column) => {
|
||||||
|
primaryKey[column] = record[column];
|
||||||
|
|
||||||
|
return primaryKey;
|
||||||
|
}, {} as Record<PrimaryKeyColumn, unknown>) as PrimaryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize a primary key with a string representation.
|
||||||
|
*
|
||||||
|
* @param primaryKey Primary key.
|
||||||
|
* @returns Serialized primary key.
|
||||||
|
*/
|
||||||
|
protected serializePrimaryKey(primaryKey: PrimaryKey): string {
|
||||||
|
return Object.values(primaryKey).map(value => String(value)).join('-');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a given record matches the given conditions.
|
||||||
|
*
|
||||||
|
* @param record Database record.
|
||||||
|
* @param conditions Matching conditions.
|
||||||
|
* @returns Whether the record matches the conditions.
|
||||||
|
*/
|
||||||
|
protected recordMatches(record: DBRecord, conditions: Partial<DBRecord>): boolean {
|
||||||
|
return !Object.entries(conditions).some(([column, value]) => record[column] !== value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infer primary key type from database record and primary key column types.
|
||||||
|
*/
|
||||||
|
export type GetDBRecordPrimaryKey<DBRecord extends SQLiteDBRecordValues, PrimaryKeyColumn extends keyof DBRecord> = {
|
||||||
|
[column in PrimaryKeyColumn]: DBRecord[column];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reducer used to accumulate a value from multiple records both in SQL and JavaScript.
|
||||||
|
*
|
||||||
|
* Both operations should be equivalent.
|
||||||
|
*/
|
||||||
|
export type CoreDatabaseReducer<DBRecord, T> = {
|
||||||
|
sql: string;
|
||||||
|
js: (previousValue: T, record: DBRecord) => T;
|
||||||
|
jsInitialValue: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditions to match database records both in SQL and JavaScript.
|
||||||
|
*
|
||||||
|
* Both conditions should be equivalent.
|
||||||
|
*/
|
||||||
|
export type CoreDatabaseConditions<DBRecord> = {
|
||||||
|
sql: string;
|
||||||
|
sqlParams?: SQLiteDBRecordValue[];
|
||||||
|
js: (record: DBRecord) => boolean;
|
||||||
|
};
|
|
@ -0,0 +1,168 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
import { SQLiteDBRecordValues } from '@classes/sqlitedb';
|
||||||
|
import { CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey, CoreDatabaseReducer } from './database-table';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper used to improve performance by caching all the records for faster read operations.
|
||||||
|
*
|
||||||
|
* This implementation works best for tables that don't have a lot of records and are read often; for tables with too many
|
||||||
|
* records use CoreLazyDatabaseTable instead.
|
||||||
|
*/
|
||||||
|
export class CoreEagerDatabaseTable<
|
||||||
|
DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues,
|
||||||
|
PrimaryKeyColumn extends keyof DBRecord = 'id',
|
||||||
|
PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn>
|
||||||
|
> extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> {
|
||||||
|
|
||||||
|
protected records: Record<string, DBRecord> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
const records = await super.all();
|
||||||
|
|
||||||
|
this.records = records.reduce((data, record) => {
|
||||||
|
const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record));
|
||||||
|
|
||||||
|
data[primaryKey] = record;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async all(conditions?: Partial<DBRecord>): Promise<DBRecord[]> {
|
||||||
|
const records = Object.values(this.records);
|
||||||
|
|
||||||
|
return conditions
|
||||||
|
? records.filter(record => this.recordMatches(record, conditions))
|
||||||
|
: records;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async find(conditions: Partial<DBRecord>): Promise<DBRecord> {
|
||||||
|
const record = Object.values(this.records).find(record => this.recordMatches(record, conditions)) ?? null;
|
||||||
|
|
||||||
|
if (record === null) {
|
||||||
|
throw new CoreError('No records found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async findByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> {
|
||||||
|
const record = this.records[this.serializePrimaryKey(primaryKey)] ?? null;
|
||||||
|
|
||||||
|
if (record === null) {
|
||||||
|
throw new CoreError('No records found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async reduce<T>(reducer: CoreDatabaseReducer<DBRecord, T>, conditions?: CoreDatabaseConditions<DBRecord>): Promise<T> {
|
||||||
|
return Object
|
||||||
|
.values(this.records)
|
||||||
|
.reduce(
|
||||||
|
(result, record) => (!conditions || conditions.js(record)) ? reducer.js(result, record) : result,
|
||||||
|
reducer.jsInitialValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async insert(record: DBRecord): Promise<void> {
|
||||||
|
await super.insert(record);
|
||||||
|
|
||||||
|
const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record));
|
||||||
|
|
||||||
|
this.records[primaryKey] = record;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async update(updates: Partial<DBRecord>, conditions?: Partial<DBRecord>): Promise<void> {
|
||||||
|
await super.update(updates, conditions);
|
||||||
|
|
||||||
|
for (const record of Object.values(this.records)) {
|
||||||
|
if (conditions && !this.recordMatches(record, conditions)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(record, updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async updateWhere(updates: Partial<DBRecord>, conditions: CoreDatabaseConditions<DBRecord>): Promise<void> {
|
||||||
|
await super.updateWhere(updates, conditions);
|
||||||
|
|
||||||
|
for (const record of Object.values(this.records)) {
|
||||||
|
if (!conditions.js(record)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(record, updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async delete(conditions?: Partial<DBRecord>): Promise<void> {
|
||||||
|
await super.delete(conditions);
|
||||||
|
|
||||||
|
if (!conditions) {
|
||||||
|
this.records = {};
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(this.records).forEach(([id, record]) => {
|
||||||
|
if (!this.recordMatches(record, conditions)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.records[id];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> {
|
||||||
|
await super.deleteByPrimaryKey(primaryKey);
|
||||||
|
|
||||||
|
delete this.records[this.serializePrimaryKey(primaryKey)];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1206,4 +1206,4 @@ export type SQLiteDBQueryParams = {
|
||||||
params: SQLiteDBRecordValue[];
|
params: SQLiteDBRecordValue[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type SQLiteDBRecordValue = number | string;
|
export type SQLiteDBRecordValue = number | string;
|
||||||
|
|
|
@ -13,7 +13,11 @@
|
||||||
// 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 {
|
||||||
|
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,30 +26,60 @@ 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, CoreDatabaseTableProxy<User>] {
|
||||||
|
const records: User[] = [];
|
||||||
|
const database = mock<SQLiteDB>({
|
||||||
|
getRecord: async <T>(_, conditions) => {
|
||||||
|
const record = records.find(record => userMatches(record, conditions));
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
return record as unknown as T;
|
||||||
|
},
|
||||||
|
getRecords: async <T>(_, conditions) => records.filter(record => userMatches(record, conditions)) as unknown as T[],
|
||||||
|
getAllRecords: async <T>() => records as unknown as T[],
|
||||||
|
deleteRecords: async (_, conditions) => {
|
||||||
|
const usersToDelete: User[] = [];
|
||||||
|
|
||||||
|
for (const user of records) {
|
||||||
|
if (conditions && !userMatches(user, conditions)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
usersToDelete.push(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const user of usersToDelete) {
|
||||||
|
records.splice(records.indexOf(user), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return usersToDelete.length;
|
||||||
|
},
|
||||||
|
insertRecord: async (_, user: User) => records.push(user) && 1,
|
||||||
|
});
|
||||||
|
const table = new CoreDatabaseTableProxy<User>(config, database, 'users');
|
||||||
|
|
||||||
|
return [records, database, table];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CoreDatabaseTable with eager caching', () => {
|
||||||
|
|
||||||
let records: User[];
|
let records: User[];
|
||||||
let db: SQLiteDB;
|
let database: SQLiteDB;
|
||||||
|
let table: CoreDatabaseTableProxy<User>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.Eager }));
|
||||||
records = [];
|
|
||||||
db = mock<SQLiteDB>({
|
|
||||||
getRecords: async <T>() => records as unknown as T[],
|
|
||||||
deleteRecords: async () => 0,
|
|
||||||
insertRecord: async () => 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reads all records on create', async () => {
|
it('reads all records on initialization', async () => {
|
||||||
await UsersTable.create(db);
|
await table.initialize();
|
||||||
|
|
||||||
expect(db.getRecords).toHaveBeenCalledWith('users');
|
expect(database.getAllRecords).toHaveBeenCalledWith('users');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finds items', async () => {
|
it('finds items', async () => {
|
||||||
|
@ -55,27 +89,29 @@ describe('CoreDatabaseTable', () => {
|
||||||
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.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
|
||||||
expect(table.findByPrimaryKey({ id: 2 })).toEqual(amy);
|
await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
|
||||||
expect(table.find({ surname: 'Doe', name: 'John' })).toEqual(john);
|
await expect(table.find({ surname: 'Doe', name: 'John' })).resolves.toEqual(john);
|
||||||
expect(table.find({ surname: 'Doe', name: 'Amy' })).toEqual(amy);
|
await expect(table.find({ surname: 'Doe', name: 'Amy' })).resolves.toEqual(amy);
|
||||||
|
|
||||||
|
expect(database.getRecord).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('inserts items', async () => {
|
it('inserts items', async () => {
|
||||||
// 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.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes items', async () => {
|
it('deletes items', async () => {
|
||||||
|
@ -88,17 +124,17 @@ 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.findByPrimaryKey({ id: 1 })).rejects.toThrow();
|
||||||
expect(table.findByPrimaryKey({ id: 2 })).toBeNull();
|
await expect(table.findByPrimaryKey({ id: 2 })).rejects.toThrow();
|
||||||
expect(table.findByPrimaryKey({ id: 3 })).toEqual(jane);
|
await expect(table.findByPrimaryKey({ id: 3 })).resolves.toEqual(jane);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes items by primary key', async () => {
|
it('deletes items by primary key', async () => {
|
||||||
|
@ -109,16 +145,108 @@ 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.findByPrimaryKey({ id: 1 })).rejects.toThrow();
|
||||||
expect(table.findByPrimaryKey({ id: 2 })).toEqual(amy);
|
await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CoreDatabaseTable with no caching', () => {
|
||||||
|
|
||||||
|
let records: User[];
|
||||||
|
let database: SQLiteDB;
|
||||||
|
let table: CoreDatabaseTableProxy<User>;
|
||||||
|
|
||||||
|
beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.None }));
|
||||||
|
|
||||||
|
it('reads no records on initialization', async () => {
|
||||||
|
await table.initialize();
|
||||||
|
|
||||||
|
expect(database.getRecords).not.toHaveBeenCalled();
|
||||||
|
expect(database.getAllRecords).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds items', async () => {
|
||||||
|
const john = { id: 1, name: 'John', surname: 'Doe' };
|
||||||
|
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
|
||||||
|
|
||||||
|
records.push(john);
|
||||||
|
records.push(amy);
|
||||||
|
|
||||||
|
await table.initialize();
|
||||||
|
|
||||||
|
await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
|
||||||
|
await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
|
||||||
|
await expect(table.find({ surname: 'Doe', name: 'John' })).resolves.toEqual(john);
|
||||||
|
await expect(table.find({ surname: 'Doe', name: 'Amy' })).resolves.toEqual(amy);
|
||||||
|
|
||||||
|
expect(database.getRecord).toHaveBeenCalledTimes(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inserts items', async () => {
|
||||||
|
// Arrange.
|
||||||
|
const john = { id: 1, name: 'John', surname: 'Doe' };
|
||||||
|
|
||||||
|
await table.initialize();
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
await table.insert(john);
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
expect(database.insertRecord).toHaveBeenCalledWith('users', john);
|
||||||
|
|
||||||
|
await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes items', async () => {
|
||||||
|
// Arrange.
|
||||||
|
const john = { id: 1, name: 'John', surname: 'Doe' };
|
||||||
|
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
|
||||||
|
const jane = { id: 3, name: 'Jane', surname: 'Smith' };
|
||||||
|
|
||||||
|
records.push(john);
|
||||||
|
records.push(amy);
|
||||||
|
records.push(jane);
|
||||||
|
|
||||||
|
await table.initialize();
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
await table.delete({ surname: 'Doe' });
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
expect(database.deleteRecords).toHaveBeenCalledWith('users', { surname: 'Doe' });
|
||||||
|
|
||||||
|
await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
|
||||||
|
await expect(table.findByPrimaryKey({ id: 2 })).rejects.toThrow();
|
||||||
|
await expect(table.findByPrimaryKey({ id: 3 })).resolves.toEqual(jane);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes items by primary key', async () => {
|
||||||
|
// Arrange.
|
||||||
|
const john = { id: 1, name: 'John', surname: 'Doe' };
|
||||||
|
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
|
||||||
|
|
||||||
|
records.push(john);
|
||||||
|
records.push(amy);
|
||||||
|
|
||||||
|
await table.initialize();
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
await table.deleteByPrimaryKey({ id: 1 });
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
expect(database.deleteRecords).toHaveBeenCalledWith('users', { id: 1 });
|
||||||
|
|
||||||
|
await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
|
||||||
|
await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,11 +13,11 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
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 { CoreDatabaseTable } from '@classes/database/database-table';
|
||||||
import { CoreDatabaseTable } from '@classes/database-table';
|
|
||||||
import { CorePromisedValue } from '@classes/promised-value';
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,11 +27,7 @@ import { CorePromisedValue } from '@classes/promised-value';
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class CoreConfigProvider {
|
export class CoreConfigProvider {
|
||||||
|
|
||||||
protected dbTable: CorePromisedValue<CoreConfigTable>;
|
protected table: CorePromisedValue<CoreDatabaseTable<ConfigDBEntry, 'name'>> = new CorePromisedValue();
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.dbTable = new CorePromisedValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize database.
|
* Initialize database.
|
||||||
|
@ -43,10 +39,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.resolve(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,7 +58,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;
|
const table = await this.table;
|
||||||
|
|
||||||
await table.deleteByPrimaryKey({ name });
|
await table.deleteByPrimaryKey({ name });
|
||||||
}
|
}
|
||||||
|
@ -69,18 +71,18 @@ 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 table = await this.table;
|
||||||
|
const record = await table.findByPrimaryKey({ name });
|
||||||
|
|
||||||
if (record !== null) {
|
|
||||||
return record.value;
|
return record.value;
|
||||||
}
|
} catch (error) {
|
||||||
|
if (defaultValue !== undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
if (defaultValue !== undefined) {
|
throw error;
|
||||||
return defaultValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Couldn't get config with name '${name}'`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,7 +93,7 @@ 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;
|
const table = await this.table;
|
||||||
|
|
||||||
await table.insert({ name, value });
|
await table.insert({ name, value });
|
||||||
}
|
}
|
||||||
|
@ -99,13 +101,3 @@ export class CoreConfigProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
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'];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue