MOBILE-3977 filepool: Optimize files table
parent
808a242cbc
commit
7c834281ce
|
@ -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');
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { CorePromisedValue } from '@classes/promised-value';
|
||||||
import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb';
|
import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb';
|
||||||
import { CoreDatabaseReducer, CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey } from './database-table';
|
import { CoreDatabaseReducer, CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey } from './database-table';
|
||||||
import { CoreEagerDatabaseTable } from './eager-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.
|
* Database table proxy used to route database interactions through different implementations.
|
||||||
|
@ -164,6 +165,8 @@ export class CoreDatabaseTableProxy<
|
||||||
switch (cachingStrategy) {
|
switch (cachingStrategy) {
|
||||||
case CoreDatabaseCachingStrategy.Eager:
|
case CoreDatabaseCachingStrategy.Eager:
|
||||||
return new CoreEagerDatabaseTable(this.database, this.tableName, this.primaryKeyColumns);
|
return new CoreEagerDatabaseTable(this.database, this.tableName, this.primaryKeyColumns);
|
||||||
|
case CoreDatabaseCachingStrategy.Lazy:
|
||||||
|
return new CoreLazyDatabaseTable(this.database, this.tableName, this.primaryKeyColumns);
|
||||||
case CoreDatabaseCachingStrategy.None:
|
case CoreDatabaseCachingStrategy.None:
|
||||||
return new CoreDatabaseTable(this.database, this.tableName, this.primaryKeyColumns);
|
return new CoreDatabaseTable(this.database, this.tableName, this.primaryKeyColumns);
|
||||||
}
|
}
|
||||||
|
@ -183,5 +186,6 @@ export interface CoreDatabaseConfiguration {
|
||||||
*/
|
*/
|
||||||
export enum CoreDatabaseCachingStrategy {
|
export enum CoreDatabaseCachingStrategy {
|
||||||
Eager = 'eager',
|
Eager = 'eager',
|
||||||
|
Lazy = 'lazy',
|
||||||
None = 'none',
|
None = 'none',
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 find(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.find(conditions);
|
||||||
|
|
||||||
|
this.records[this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record))] = record;
|
||||||
|
}
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async findByPrimaryKey(primaryKey: PrimaryKey): Promise<DBRecord> {
|
||||||
|
const serializePrimaryKey = this.serializePrimaryKey(primaryKey);
|
||||||
|
|
||||||
|
if (!(serializePrimaryKey in this.records)) {
|
||||||
|
try {
|
||||||
|
const record = await super.findByPrimaryKey(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -13,6 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { mock } from '@/testing/utils';
|
import { mock } from '@/testing/utils';
|
||||||
|
import { CoreDatabaseTable } from '@classes/database/database-table';
|
||||||
import {
|
import {
|
||||||
CoreDatabaseCachingStrategy,
|
CoreDatabaseCachingStrategy,
|
||||||
CoreDatabaseConfiguration,
|
CoreDatabaseConfiguration,
|
||||||
|
@ -30,7 +31,7 @@ function userMatches(user: User, conditions: Partial<User>) {
|
||||||
return !Object.entries(conditions).some(([column, value]) => user[column] !== value);
|
return !Object.entries(conditions).some(([column, value]) => user[column] !== value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareStubs(config: Partial<CoreDatabaseConfiguration> = {}): [User[], SQLiteDB, CoreDatabaseTableProxy<User>] {
|
function prepareStubs(config: Partial<CoreDatabaseConfiguration> = {}): [User[], SQLiteDB, CoreDatabaseTable<User>] {
|
||||||
const records: User[] = [];
|
const records: User[] = [];
|
||||||
const database = mock<SQLiteDB>({
|
const database = mock<SQLiteDB>({
|
||||||
getRecord: async <T>(_, conditions) => {
|
getRecord: async <T>(_, conditions) => {
|
||||||
|
@ -68,11 +69,84 @@ function prepareStubs(config: Partial<CoreDatabaseConfiguration> = {}): [User[],
|
||||||
return [records, database, table];
|
return [records, database, table];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testFindItems(records: User[], table: CoreDatabaseTable<User>) {
|
||||||
|
const john = { id: 1, name: 'John', surname: 'Doe' };
|
||||||
|
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
|
||||||
|
|
||||||
|
records.push(john);
|
||||||
|
records.push(amy);
|
||||||
|
|
||||||
|
await table.initialize();
|
||||||
|
|
||||||
|
await expect(table.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testInsertItems(records: User[], database: SQLiteDB, table: CoreDatabaseTable<User>) {
|
||||||
|
// Arrange.
|
||||||
|
const john = { id: 1, name: 'John', surname: 'Doe' };
|
||||||
|
|
||||||
|
await table.initialize();
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
await table.insert(john);
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
expect(database.insertRecord).toHaveBeenCalledWith('users', john);
|
||||||
|
|
||||||
|
await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testDeleteItems(records: User[], database: SQLiteDB, table: CoreDatabaseTable<User>) {
|
||||||
|
// Arrange.
|
||||||
|
const john = { id: 1, name: 'John', surname: 'Doe' };
|
||||||
|
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
|
||||||
|
const jane = { id: 3, name: 'Jane', surname: 'Smith' };
|
||||||
|
|
||||||
|
records.push(john);
|
||||||
|
records.push(amy);
|
||||||
|
records.push(jane);
|
||||||
|
|
||||||
|
await table.initialize();
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
await table.delete({ surname: 'Doe' });
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
expect(database.deleteRecords).toHaveBeenCalledWith('users', { surname: 'Doe' });
|
||||||
|
|
||||||
|
await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
|
||||||
|
await expect(table.findByPrimaryKey({ id: 2 })).rejects.toThrow();
|
||||||
|
await expect(table.findByPrimaryKey({ id: 3 })).resolves.toEqual(jane);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testDeleteItemsByPrimaryKey(records: User[], database: SQLiteDB, table: CoreDatabaseTable<User>) {
|
||||||
|
// Arrange.
|
||||||
|
const john = { id: 1, name: 'John', surname: 'Doe' };
|
||||||
|
const amy = { id: 2, name: 'Amy', surname: 'Doe' };
|
||||||
|
|
||||||
|
records.push(john);
|
||||||
|
records.push(amy);
|
||||||
|
|
||||||
|
await table.initialize();
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
await table.deleteByPrimaryKey({ id: 1 });
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
expect(database.deleteRecords).toHaveBeenCalledWith('users', { id: 1 });
|
||||||
|
|
||||||
|
await expect(table.findByPrimaryKey({ id: 1 })).rejects.toThrow();
|
||||||
|
await expect(table.findByPrimaryKey({ id: 2 })).resolves.toEqual(amy);
|
||||||
|
}
|
||||||
|
|
||||||
describe('CoreDatabaseTable with eager caching', () => {
|
describe('CoreDatabaseTable with eager caching', () => {
|
||||||
|
|
||||||
let records: User[];
|
let records: User[];
|
||||||
let database: SQLiteDB;
|
let database: SQLiteDB;
|
||||||
let table: CoreDatabaseTableProxy<User>;
|
let table: CoreDatabaseTable<User>;
|
||||||
|
|
||||||
beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.Eager }));
|
beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.Eager }));
|
||||||
|
|
||||||
|
@ -83,79 +157,41 @@ describe('CoreDatabaseTable with eager caching', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finds items', async () => {
|
it('finds items', async () => {
|
||||||
const john = { id: 1, name: 'John', surname: 'Doe' };
|
await testFindItems(records, table);
|
||||||
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).not.toHaveBeenCalled();
|
expect(database.getRecord).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('inserts items', async () => {
|
it('inserts items', () => testInsertItems(records, database, table));
|
||||||
// Arrange.
|
it('deletes items', () => testDeleteItems(records, database, table));
|
||||||
const john = { id: 1, name: 'John', surname: 'Doe' };
|
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();
|
await table.initialize();
|
||||||
|
|
||||||
// Act.
|
expect(database.getRecords).not.toHaveBeenCalled();
|
||||||
await table.insert(john);
|
expect(database.getAllRecords).not.toHaveBeenCalled();
|
||||||
|
|
||||||
// Assert.
|
|
||||||
expect(database.insertRecord).toHaveBeenCalledWith('users', john);
|
|
||||||
|
|
||||||
await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes items', async () => {
|
it('finds items', async () => {
|
||||||
// Arrange.
|
await testFindItems(records, table);
|
||||||
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);
|
expect(database.getRecord).toHaveBeenCalledTimes(2);
|
||||||
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 () => {
|
it('inserts items', () => testInsertItems(records, database, table));
|
||||||
// Arrange.
|
it('deletes items', () => testDeleteItems(records, database, table));
|
||||||
const john = { id: 1, name: 'John', surname: 'Doe' };
|
it('deletes items by primary key', () => testDeleteItemsByPrimaryKey(records, database, table));
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -163,7 +199,7 @@ describe('CoreDatabaseTable with no caching', () => {
|
||||||
|
|
||||||
let records: User[];
|
let records: User[];
|
||||||
let database: SQLiteDB;
|
let database: SQLiteDB;
|
||||||
let table: CoreDatabaseTableProxy<User>;
|
let table: CoreDatabaseTable<User>;
|
||||||
|
|
||||||
beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.None }));
|
beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.None }));
|
||||||
|
|
||||||
|
@ -175,78 +211,13 @@ describe('CoreDatabaseTable with no caching', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finds items', async () => {
|
it('finds items', async () => {
|
||||||
const john = { id: 1, name: 'John', surname: 'Doe' };
|
await testFindItems(records, table);
|
||||||
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);
|
expect(database.getRecord).toHaveBeenCalledTimes(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('inserts items', async () => {
|
it('inserts items', () => testInsertItems(records, database, table));
|
||||||
// Arrange.
|
it('deletes items', () => testDeleteItems(records, database, table));
|
||||||
const john = { id: 1, name: 'John', surname: 'Doe' };
|
it('deletes items by primary key', () => testDeleteItemsByPrimaryKey(records, database, table));
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -48,6 +48,9 @@ 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 { CorePromisedValue } from '@classes/promised-value';
|
||||||
|
import { CoreDatabaseTable } from '@classes/database/database-table';
|
||||||
|
import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Factory for handling downloading files and retrieve downloaded files.
|
* Factory for handling downloading files and retrieve downloaded files.
|
||||||
|
@ -72,9 +75,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,6 +101,7 @@ 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: Record<string, CorePromisedValue<CoreDatabaseTable<CoreFilepoolFileEntry, 'fileId'>>> = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
|
this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
|
||||||
|
@ -114,6 +122,18 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filesTable = await this.filesTables[siteId];
|
||||||
|
|
||||||
|
delete this.filesTables[siteId];
|
||||||
|
|
||||||
|
await filesTable.destroy();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -129,6 +149,33 @@ export class CoreFilepoolProvider {
|
||||||
this.resolveAppDB(CoreApp.getDB());
|
this.resolveAppDB(CoreApp.getDB());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get files table.
|
||||||
|
*
|
||||||
|
* @param siteId Site id.
|
||||||
|
* @returns Files table.
|
||||||
|
*/
|
||||||
|
async getFilesTable(siteId?: string): Promise<CoreDatabaseTable<CoreFilepoolFileEntry, 'fileId'>> {
|
||||||
|
siteId = siteId ?? CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (!(siteId in this.filesTables)) {
|
||||||
|
const filesTable = this.filesTables[siteId] = new CorePromisedValue();
|
||||||
|
const database = await CoreSites.getSiteDb(siteId);
|
||||||
|
const table = new CoreDatabaseTableProxy<CoreFilepoolFileEntry, 'fileId'>(
|
||||||
|
{ cachingStrategy: CoreDatabaseCachingStrategy.Lazy },
|
||||||
|
database,
|
||||||
|
FILES_TABLE_NAME,
|
||||||
|
['fileId'],
|
||||||
|
);
|
||||||
|
|
||||||
|
await table.initialize();
|
||||||
|
|
||||||
|
filesTable.resolve(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.filesTables[siteId];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Link a file with a component.
|
* Link a file with a component.
|
||||||
*
|
*
|
||||||
|
@ -215,9 +262,9 @@ export class CoreFilepoolProvider {
|
||||||
...data,
|
...data,
|
||||||
};
|
};
|
||||||
|
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
const filesTable = await this.getFilesTable(siteId);
|
||||||
|
|
||||||
await db.insertRecord(FILES_TABLE_NAME, record);
|
await filesTable.insert(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -558,13 +605,14 @@ export class CoreFilepoolProvider {
|
||||||
*/
|
*/
|
||||||
async clearFilepool(siteId: string): Promise<void> {
|
async clearFilepool(siteId: string): Promise<void> {
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
const db = await CoreSites.getSiteDb(siteId);
|
||||||
|
const filesTable = await this.getFilesTable(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 filesTable.all();
|
||||||
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),
|
filesTable.delete(),
|
||||||
db.deleteRecords(LINKS_TABLE_NAME),
|
db.deleteRecords(LINKS_TABLE_NAME),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -1119,13 +1167,14 @@ export class CoreFilepoolProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
const db = await CoreSites.getSiteDb(siteId);
|
||||||
|
const filesTable = await this.getFilesTable(siteId);
|
||||||
const extension = CoreMimetypeUtils.getFileExtension(entry.path);
|
const extension = CoreMimetypeUtils.getFileExtension(entry.path);
|
||||||
if (!extension) {
|
if (!extension) {
|
||||||
// Files does not have extension. Invalidate file (stale = true).
|
// Files does not have extension. Invalidate file (stale = true).
|
||||||
// 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 filesTable.update({ stale: 1 }, { fileId: entry.fileId });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1135,7 +1184,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 filesTable.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);
|
||||||
|
@ -1396,15 +1445,13 @@ export class CoreFilepoolProvider {
|
||||||
*/
|
*/
|
||||||
async getFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise<CoreFilepoolFileEntry[]> {
|
async getFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise<CoreFilepoolFileEntry[]> {
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
const db = await CoreSites.getSiteDb(siteId);
|
||||||
|
const filesTable = await this.getFilesTable(siteId);
|
||||||
const items = await this.getComponentFiles(db, component, componentId);
|
const items = await this.getComponentFiles(db, component, componentId);
|
||||||
const files: CoreFilepoolFileEntry[] = [];
|
const files: CoreFilepoolFileEntry[] = [];
|
||||||
|
|
||||||
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 filesTable.findByPrimaryKey({ fileId: item.fileId });
|
||||||
FILES_TABLE_NAME,
|
|
||||||
{ fileId: item.fileId },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fileEntry) {
|
if (!fileEntry) {
|
||||||
return;
|
return;
|
||||||
|
@ -2137,14 +2184,9 @@ 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);
|
const filesTable = await this.getFilesTable(siteId);
|
||||||
const entry = await db.getRecord<CoreFilepoolFileEntry>(FILES_TABLE_NAME, { fileId });
|
|
||||||
|
|
||||||
if (entry === undefined) {
|
return filesTable.findByPrimaryKey({ fileId });
|
||||||
throw new CoreError('File not found in filepool.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2176,11 +2218,17 @@ 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);
|
const filesTable = await this.getFilesTable(siteId);
|
||||||
|
|
||||||
const where = onlyUnknown ? CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE : undefined;
|
onlyUnknown
|
||||||
|
? await filesTable.updateWhere(
|
||||||
await db.updateRecordsWhere(FILES_TABLE_NAME, { stale: 1 }, where);
|
{ stale: 1 },
|
||||||
|
{
|
||||||
|
sql: CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL,
|
||||||
|
js: CoreFilepoolProvider.FILE_IS_UNKNOWN_JS,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: await filesTable.update({ stale: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2199,9 +2247,9 @@ 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);
|
const filesTable = await this.getFilesTable(siteId);
|
||||||
|
|
||||||
await db.updateRecords(FILES_TABLE_NAME, { stale: 1 }, { fileId });
|
await filesTable.update({ stale: 1 }, { fileId });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2221,7 +2269,7 @@ 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 filesTable = await this.getFilesTable(siteId);
|
||||||
const items = await this.getComponentFiles(db, component, componentId);
|
const items = await this.getComponentFiles(db, component, componentId);
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
|
@ -2236,10 +2284,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 filesTable.updateWhere(
|
||||||
|
{ stale: 1 },
|
||||||
|
{
|
||||||
|
sql: whereAndParams.sql,
|
||||||
|
sqlParams: whereAndParams.params,
|
||||||
|
js: record => fileIds.includes(record.fileId) && (
|
||||||
|
!onlyUnknown || CoreFilepoolProvider.FILE_IS_UNKNOWN_JS(record)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2657,6 +2714,8 @@ 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);
|
||||||
|
const filesTable = await this.getFilesTable(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 +2741,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(filesTable.delete(conditions));
|
||||||
|
|
||||||
// Remove links.
|
// Remove links.
|
||||||
promises.push(db.deleteRecords(LINKS_TABLE_NAME, conditions));
|
promises.push(db.deleteRecords(LINKS_TABLE_NAME, conditions));
|
||||||
|
|
Loading…
Reference in New Issue