forked from CIT/Vmeda.Online
		
	MOBILE-3977 filepool: Optimize files table
This commit is contained in:
		
							parent
							
								
									808a242cbc
								
							
						
					
					
						commit
						7c834281ce
					
				@ -271,6 +271,7 @@ testsConfig['rules']['padded-blocks'] = [
 | 
			
		||||
        switches: 'never',
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
testsConfig['rules']['jest/expect-expect'] = 'off';
 | 
			
		||||
testsConfig['plugins'].push('jest');
 | 
			
		||||
testsConfig['extends'].push('plugin:jest/recommended');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ 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';
 | 
			
		||||
import { CoreLazyDatabaseTable } from './lazy-database-table';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Database table proxy used to route database interactions through different implementations.
 | 
			
		||||
@ -164,6 +165,8 @@ export class CoreDatabaseTableProxy<
 | 
			
		||||
        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);
 | 
			
		||||
        }
 | 
			
		||||
@ -183,5 +186,6 @@ export interface CoreDatabaseConfiguration {
 | 
			
		||||
 */
 | 
			
		||||
export enum CoreDatabaseCachingStrategy {
 | 
			
		||||
    Eager = 'eager',
 | 
			
		||||
    Lazy = 'lazy',
 | 
			
		||||
    None = 'none',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										141
									
								
								src/core/classes/database/lazy-database-table.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/core/classes/database/lazy-database-table.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,141 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { SQLiteDBRecordValues } from '@classes/sqlitedb';
 | 
			
		||||
import { CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey } from './database-table';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Wrapper used to improve performance by caching records that are used often for faster read operations.
 | 
			
		||||
 *
 | 
			
		||||
 * This implementation works best for tables that have a lot of records and are read often; for tables with a few records use
 | 
			
		||||
 * CoreEagerDatabaseTable instead.
 | 
			
		||||
 */
 | 
			
		||||
export class CoreLazyDatabaseTable<
 | 
			
		||||
    DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues,
 | 
			
		||||
    PrimaryKeyColumn extends keyof DBRecord = 'id',
 | 
			
		||||
    PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn>
 | 
			
		||||
> extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> {
 | 
			
		||||
 | 
			
		||||
    protected records: Record<string, DBRecord | null> = {};
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async 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.
 | 
			
		||||
 | 
			
		||||
import { mock } from '@/testing/utils';
 | 
			
		||||
import { CoreDatabaseTable } from '@classes/database/database-table';
 | 
			
		||||
import {
 | 
			
		||||
    CoreDatabaseCachingStrategy,
 | 
			
		||||
    CoreDatabaseConfiguration,
 | 
			
		||||
@ -30,7 +31,7 @@ function userMatches(user: User, conditions: Partial<User>) {
 | 
			
		||||
    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 database = mock<SQLiteDB>({
 | 
			
		||||
        getRecord: async <T>(_, conditions) => {
 | 
			
		||||
@ -68,11 +69,84 @@ function prepareStubs(config: Partial<CoreDatabaseConfiguration> = {}): [User[],
 | 
			
		||||
    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', () => {
 | 
			
		||||
 | 
			
		||||
    let records: User[];
 | 
			
		||||
    let database: SQLiteDB;
 | 
			
		||||
    let table: CoreDatabaseTableProxy<User>;
 | 
			
		||||
    let table: CoreDatabaseTable<User>;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.Eager }));
 | 
			
		||||
 | 
			
		||||
@ -83,79 +157,41 @@ describe('CoreDatabaseTable with eager caching', () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
        await testFindItems(records, table);
 | 
			
		||||
 | 
			
		||||
        expect(database.getRecord).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('inserts items', async () => {
 | 
			
		||||
        // Arrange.
 | 
			
		||||
        const john = { id: 1, name: 'John', surname: 'Doe' };
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
        // Act.
 | 
			
		||||
        await table.insert(john);
 | 
			
		||||
 | 
			
		||||
        // Assert.
 | 
			
		||||
        expect(database.insertRecord).toHaveBeenCalledWith('users', john);
 | 
			
		||||
 | 
			
		||||
        await expect(table.findByPrimaryKey({ id: 1 })).resolves.toEqual(john);
 | 
			
		||||
        expect(database.getRecords).not.toHaveBeenCalled();
 | 
			
		||||
        expect(database.getAllRecords).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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' };
 | 
			
		||||
    it('finds items', async () => {
 | 
			
		||||
        await testFindItems(records, table);
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
        expect(database.getRecord).toHaveBeenCalledTimes(2);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
    });
 | 
			
		||||
    it('inserts items', () => testInsertItems(records, database, table));
 | 
			
		||||
    it('deletes items', () => testDeleteItems(records, database, table));
 | 
			
		||||
    it('deletes items by primary key', () => testDeleteItemsByPrimaryKey(records, database, table));
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -163,7 +199,7 @@ describe('CoreDatabaseTable with no caching', () => {
 | 
			
		||||
 | 
			
		||||
    let records: User[];
 | 
			
		||||
    let database: SQLiteDB;
 | 
			
		||||
    let table: CoreDatabaseTableProxy<User>;
 | 
			
		||||
    let table: CoreDatabaseTable<User>;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => [records, database, table] = prepareStubs({ cachingStrategy: CoreDatabaseCachingStrategy.None }));
 | 
			
		||||
 | 
			
		||||
@ -175,78 +211,13 @@ describe('CoreDatabaseTable with no caching', () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
        await testFindItems(records, table);
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
    });
 | 
			
		||||
    it('inserts items', () => testInsertItems(records, database, table));
 | 
			
		||||
    it('deletes items', () => testDeleteItems(records, database, table));
 | 
			
		||||
    it('deletes items by primary key', () => testDeleteItemsByPrimaryKey(records, database, table));
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,9 @@ import {
 | 
			
		||||
} from '@services/database/filepool';
 | 
			
		||||
import { CoreFileHelper } from './file-helper';
 | 
			
		||||
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.
 | 
			
		||||
@ -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_QUEUE_ON_PAUSE = 'CoreFilepoolError:ERR_QUEUE_ON_PAUSE';
 | 
			
		||||
 | 
			
		||||
    protected static readonly FILE_UPDATE_UNKNOWN_WHERE_CLAUSE =
 | 
			
		||||
    protected static readonly FILE_IS_UNKNOWN_SQL =
 | 
			
		||||
        'isexternalfile = 1 OR ((revision IS NULL OR revision = 0) AND (timemodified IS NULL OR timemodified = 0))';
 | 
			
		||||
 | 
			
		||||
    protected static readonly FILE_IS_UNKNOWN_JS =
 | 
			
		||||
        ({ isexternalfile, revision, timemodified }: CoreFilepoolFileEntry): boolean =>
 | 
			
		||||
            isexternalfile === 1 || ((revision === null || revision === 0) && (timemodified === null || timemodified === 0));
 | 
			
		||||
 | 
			
		||||
    protected logger: CoreLogger;
 | 
			
		||||
    protected queueState = CoreFilepoolProvider.QUEUE_PAUSED;
 | 
			
		||||
    protected urlAttributes: RegExp[] = [
 | 
			
		||||
@ -94,6 +101,7 @@ export class CoreFilepoolProvider {
 | 
			
		||||
    // Variables for DB.
 | 
			
		||||
    protected appDB: Promise<SQLiteDB>;
 | 
			
		||||
    protected resolveAppDB!: (appDB: SQLiteDB) => void;
 | 
			
		||||
    protected filesTables: Record<string, CorePromisedValue<CoreDatabaseTable<CoreFilepoolFileEntry, 'fileId'>>> = {};
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
 | 
			
		||||
@ -114,6 +122,18 @@ export class CoreFilepoolProvider {
 | 
			
		||||
                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());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     *
 | 
			
		||||
@ -215,9 +262,9 @@ export class CoreFilepoolProvider {
 | 
			
		||||
            ...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> {
 | 
			
		||||
        const db = await CoreSites.getSiteDb(siteId);
 | 
			
		||||
        const filesTable = await this.getFilesTable(siteId);
 | 
			
		||||
 | 
			
		||||
        // 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);
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            db.deleteRecords(FILES_TABLE_NAME),
 | 
			
		||||
            filesTable.delete(),
 | 
			
		||||
            db.deleteRecords(LINKS_TABLE_NAME),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
@ -1119,13 +1167,14 @@ export class CoreFilepoolProvider {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const db = await CoreSites.getSiteDb(siteId);
 | 
			
		||||
        const filesTable = await this.getFilesTable(siteId);
 | 
			
		||||
        const extension = CoreMimetypeUtils.getFileExtension(entry.path);
 | 
			
		||||
        if (!extension) {
 | 
			
		||||
            // Files does not have extension. Invalidate file (stale = true).
 | 
			
		||||
            // Minor problem: file will remain in the filesystem once downloaded again.
 | 
			
		||||
            this.logger.debug('Staled file with no extension ' + entry.fileId);
 | 
			
		||||
 | 
			
		||||
            await db.updateRecords(FILES_TABLE_NAME, { stale: 1 }, { fileId: entry.fileId });
 | 
			
		||||
            await filesTable.update({ stale: 1 }, { fileId: entry.fileId });
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@ -1135,7 +1184,7 @@ export class CoreFilepoolProvider {
 | 
			
		||||
        entry.fileId = CoreMimetypeUtils.removeExtension(fileId);
 | 
			
		||||
        entry.extension = extension;
 | 
			
		||||
 | 
			
		||||
        await db.updateRecords(FILES_TABLE_NAME, entry, { fileId });
 | 
			
		||||
        await filesTable.update(entry, { fileId });
 | 
			
		||||
        if (entry.fileId == fileId) {
 | 
			
		||||
            // File ID hasn't changed, we're done.
 | 
			
		||||
            this.logger.debug('Removed extesion ' + extension + ' from file ' + entry.fileId);
 | 
			
		||||
@ -1396,15 +1445,13 @@ export class CoreFilepoolProvider {
 | 
			
		||||
     */
 | 
			
		||||
    async getFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise<CoreFilepoolFileEntry[]> {
 | 
			
		||||
        const db = await CoreSites.getSiteDb(siteId);
 | 
			
		||||
        const filesTable = await this.getFilesTable(siteId);
 | 
			
		||||
        const items = await this.getComponentFiles(db, component, componentId);
 | 
			
		||||
        const files: CoreFilepoolFileEntry[] = [];
 | 
			
		||||
 | 
			
		||||
        await Promise.all(items.map(async (item) => {
 | 
			
		||||
            try {
 | 
			
		||||
                const fileEntry = await db.getRecord<CoreFilepoolFileEntry>(
 | 
			
		||||
                    FILES_TABLE_NAME,
 | 
			
		||||
                    { fileId: item.fileId },
 | 
			
		||||
                );
 | 
			
		||||
                const fileEntry = await filesTable.findByPrimaryKey({ fileId: item.fileId });
 | 
			
		||||
 | 
			
		||||
                if (!fileEntry) {
 | 
			
		||||
                    return;
 | 
			
		||||
@ -2137,14 +2184,9 @@ export class CoreFilepoolProvider {
 | 
			
		||||
     * @return Resolved with file object from DB on success, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    protected async hasFileInPool(siteId: string, fileId: string): Promise<CoreFilepoolFileEntry> {
 | 
			
		||||
        const db = await CoreSites.getSiteDb(siteId);
 | 
			
		||||
        const entry = await db.getRecord<CoreFilepoolFileEntry>(FILES_TABLE_NAME, { fileId });
 | 
			
		||||
        const filesTable = await this.getFilesTable(siteId);
 | 
			
		||||
 | 
			
		||||
        if (entry === undefined) {
 | 
			
		||||
            throw new CoreError('File not found in filepool.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return entry;
 | 
			
		||||
        return filesTable.findByPrimaryKey({ fileId });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -2176,11 +2218,17 @@ export class CoreFilepoolProvider {
 | 
			
		||||
     * @return Resolved on success.
 | 
			
		||||
     */
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
        await db.updateRecordsWhere(FILES_TABLE_NAME, { stale: 1 }, where);
 | 
			
		||||
        onlyUnknown
 | 
			
		||||
            ? await filesTable.updateWhere(
 | 
			
		||||
                { 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 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,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        const db = await CoreSites.getSiteDb(siteId);
 | 
			
		||||
 | 
			
		||||
        const filesTable = await this.getFilesTable(siteId);
 | 
			
		||||
        const items = await this.getComponentFiles(db, component, componentId);
 | 
			
		||||
 | 
			
		||||
        if (!items.length) {
 | 
			
		||||
@ -2236,10 +2284,19 @@ export class CoreFilepoolProvider {
 | 
			
		||||
        whereAndParams.sql = 'fileId ' + whereAndParams.sql;
 | 
			
		||||
 | 
			
		||||
        if (onlyUnknown) {
 | 
			
		||||
            whereAndParams.sql += ' AND (' + CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE + ')';
 | 
			
		||||
            whereAndParams.sql += ' AND (' + CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL + ')';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await db.updateRecordsWhere(FILES_TABLE_NAME, { stale: 1 }, whereAndParams.sql, whereAndParams.params);
 | 
			
		||||
        await 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> {
 | 
			
		||||
        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.
 | 
			
		||||
        // Don't use getFilePath to prevent performing 2 DB requests.
 | 
			
		||||
        let path = this.getFilepoolFolderPath(siteId) + '/' + fileId;
 | 
			
		||||
@ -2682,7 +2741,7 @@ export class CoreFilepoolProvider {
 | 
			
		||||
        const promises: Promise<unknown>[] = [];
 | 
			
		||||
 | 
			
		||||
        // Remove entry from filepool store.
 | 
			
		||||
        promises.push(db.deleteRecords(FILES_TABLE_NAME, conditions));
 | 
			
		||||
        promises.push(filesTable.delete(conditions));
 | 
			
		||||
 | 
			
		||||
        // Remove links.
 | 
			
		||||
        promises.push(db.deleteRecords(LINKS_TABLE_NAME, conditions));
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user