/* * CloudBeaver - Cloud Database Manager * Copyright (C) 2020-2021 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observable, makeObservable, action } from 'mobx'; import type { IConnectionExecutionContext } from '@cloudbeaver/core-connections'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { DatabaseDataActions } from './DatabaseDataActions'; import type { IDatabaseDataAction, IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction'; import type { IDatabaseDataActions } from './IDatabaseDataActions'; import type { IDatabaseDataResult } from './IDatabaseDataResult'; import { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; export abstract class DatabaseDataSource implements IDatabaseDataSource { access: DatabaseDataAccessMode; dataFormat: ResultDataFormat; supportedDataFormats: ResultDataFormat[]; actions: IDatabaseDataActions; results: TResult[]; offset: number; count: number; options: TOptions | null; requestInfo: IRequestInfo; error: Error | null; executionContext: IConnectionExecutionContext | null; abstract get canCancel(): boolean; protected disabled: boolean; private activeRequest: Promise | null; private activeSave: Promise | null; private activeTask: Promise | null; private lastAction: () => Promise; constructor() { this.actions = new DatabaseDataActions(this); this.access = DatabaseDataAccessMode.Default; this.results = []; this.offset = 0; this.count = 0; this.options = null; this.disabled = false; this.activeRequest = null; this.activeSave = null; this.activeTask = null; this.executionContext = null; this.dataFormat = ResultDataFormat.Resultset; this.supportedDataFormats = []; this.requestInfo = { requestDuration: 0, requestMessage: '', requestFilter: '', source: null, }; this.error = null; this.lastAction = this.requestData.bind(this); makeObservable, 'activeRequest' | 'activeSave' | 'activeTask' | 'disabled'>(this, { access: observable, dataFormat: observable, supportedDataFormats: observable, results: observable, offset: observable, count: observable, options: observable, requestInfo: observable, error: observable.ref, executionContext: observable, disabled: observable, activeRequest: observable.ref, activeSave: observable.ref, activeTask: observable.ref, setResults: action, setSupportedDataFormats: action, }); } tryGetAction>( resultIndex: number, action: IDatabaseDataActionClass ): T | undefined tryGetAction>( result: TResult, action: IDatabaseDataActionClass ): T | undefined tryGetAction>( resultIndex: number | TResult, action: IDatabaseDataActionClass ): T | undefined { if (typeof resultIndex === 'number') { if (!this.hasResult(resultIndex)) { return undefined; } return this.actions.tryGet(this.results[resultIndex], action); } return this.actions.tryGet(resultIndex, action); } getAction>( resultIndex: number, action: IDatabaseDataActionClass ): T getAction>( result: TResult, action: IDatabaseDataActionClass ): T getAction>( resultIndex: number | TResult, action: IDatabaseDataActionClass ): T { if (typeof resultIndex === 'number') { if (!this.hasResult(resultIndex)) { throw new Error('Result index out of range'); } return this.actions.get(this.results[resultIndex], action); } return this.actions.get(resultIndex, action); } getActionImplementation>( resultIndex: number, action: IDatabaseDataActionInterface ): T | undefined getActionImplementation>( result: TResult, action: IDatabaseDataActionInterface ): T | undefined getActionImplementation>( resultIndex: number | TResult, action: IDatabaseDataActionInterface ): T | undefined { if (typeof resultIndex === 'number') { if (!this.hasResult(resultIndex)) { return undefined; } return this.actions.getImplementation(this.results[resultIndex], action); } return this.actions.getImplementation(resultIndex, action); } abstract cancel(): Promise | void; hasResult(resultIndex: number): boolean { return resultIndex < this.results.length; } getResult(index: number): TResult | null { if (this.results.length > index) { return this.results[index]; } return null; } setResults(results: TResult[]): this { this.actions.updateResults(results); this.results = results; return this; } isReadonly(): boolean { return this.access === DatabaseDataAccessMode.Readonly || this.results.length > 1 || this.disabled; } isLoading(): boolean { return !!this.activeRequest || !!this.activeSave || !!this.activeTask; } isDisabled(resultIndex: number): boolean { return this.isLoading() || this.disabled; } setAccess(access: DatabaseDataAccessMode): this { this.access = access; return this; } setSlice(offset: number, count: number): this { this.offset = offset; this.count = count; return this; } setOptions(options: TOptions): this { this.options = options; return this; } setDataFormat(dataFormat: ResultDataFormat): this { this.dataFormat = dataFormat; return this; } setSupportedDataFormats(dataFormats: ResultDataFormat[]): this { this.supportedDataFormats = dataFormats; if (!this.supportedDataFormats.includes(this.dataFormat)) { this.dataFormat = dataFormats[0]; // set's default format based on supported list, but maybe should be moved to separate method } return this; } setExecutionContext(context: IConnectionExecutionContext | null): this { this.executionContext = context; return this; } async retry(): Promise { await this.lastAction(); } async runTask(task: () => Promise): Promise { if (this.activeTask) { try { await this.activeTask; } catch {} } if (this.activeSave) { try { await this.activeSave; } catch { } } if (this.activeRequest) { try { await this.activeRequest; } catch { } } this.activeTask = task(); try { return await this.activeTask; } finally { this.activeTask = null; } } async requestData(): Promise { if (this.activeSave) { try { await this.activeSave; } finally { } } if (this.activeRequest) { await this.activeRequest; return; } this.lastAction = this.requestData.bind(this); try { const promise = this.request(this.results); if (promise instanceof Promise) { this.activeRequest = promise; } this.setResults(await promise); } finally { this.activeRequest = null; } } async saveData(): Promise { if (this.activeRequest) { try { await this.activeRequest; } finally { } } if (this.activeSave) { await this.activeSave; return; } this.lastAction = this.saveData.bind(this); try { const promise = this.save(this.results); if (promise instanceof Promise) { this.activeSave = promise; } this.setResults(await promise); } finally { this.activeSave = null; } } clearError(): void { this.error = null; } abstract request(prevResults: TResult[]): TResult[] | Promise; abstract save(prevResults: TResult[]): Promise | TResult[]; abstract dispose(): Promise; }