mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
* CB-6063 feat: add ability to import data without PK * CB-6063 feat: add ability to insert rows to tables w/o PK using actions removes isImportAllowed flag, adds hasRowIdentifier, uses it in place of forbidden actions * CB-6063 refactor: use only false value for hasRowIdentifier flag * CB-6063 fix: revert logic in hasRowIdentifier * CB-6063 refactor: rename property * CB-6063 fix: return lock icon back for tables w/o identifier * CB-6063 fix: hasRowIdentifier in getResult --------- Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com> Co-authored-by: Ainur <59531286+yagudin10@users.noreply.github.com>
401 lines
12 KiB
TypeScript
401 lines
12 KiB
TypeScript
/*
|
|
* CloudBeaver - Cloud Database Manager
|
|
* Copyright (C) 2020-2024 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 { action, makeObservable, observable, toJS } from 'mobx';
|
|
|
|
import type { IServiceProvider } from '@cloudbeaver/core-di';
|
|
import { Executor, ExecutorInterrupter, type IExecutor, type ITask, Task } from '@cloudbeaver/core-executor';
|
|
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
|
|
|
|
import { DatabaseDataActions } from './DatabaseDataActions.js';
|
|
import type { IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction.js';
|
|
import type { IDatabaseDataActions } from './IDatabaseDataActions.js';
|
|
import type { IDatabaseDataResult } from './IDatabaseDataResult.js';
|
|
import {
|
|
DatabaseDataAccessMode,
|
|
DatabaseDataSourceOperation,
|
|
type IDatabaseDataSource,
|
|
type IDatabaseDataSourceOperationEvent,
|
|
type IRequestInfo,
|
|
} from './IDatabaseDataSource.js';
|
|
|
|
export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseDataResult> implements IDatabaseDataSource<TOptions, TResult> {
|
|
access: DatabaseDataAccessMode;
|
|
dataFormat: ResultDataFormat;
|
|
supportedDataFormats: ResultDataFormat[];
|
|
constraintsAvailable: boolean;
|
|
actions: IDatabaseDataActions<TOptions, TResult>;
|
|
results: TResult[];
|
|
offset: number;
|
|
count: number;
|
|
prevOptions: Readonly<TOptions> | null;
|
|
options: TOptions | null;
|
|
requestInfo: IRequestInfo;
|
|
error: Error | null;
|
|
|
|
get canCancel(): boolean {
|
|
if (this.activeOperation instanceof Task) {
|
|
return this.activeOperation.cancellable;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
get cancelled(): boolean {
|
|
if (this.activeOperation instanceof Task) {
|
|
return this.activeOperation.cancelled;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
readonly serviceProvider: IServiceProvider;
|
|
protected disabled: boolean;
|
|
private lastAction: () => Promise<void>;
|
|
private outdated: boolean;
|
|
|
|
readonly onOperation: IExecutor<IDatabaseDataSourceOperationEvent>;
|
|
private get activeOperation(): Promise<void> | null {
|
|
return this.activeOperationStack[this.activeOperationStack.length - 1] ?? null;
|
|
}
|
|
private readonly activeOperationStack: Array<Promise<any>>;
|
|
|
|
constructor(serviceProvider: IServiceProvider) {
|
|
this.serviceProvider = serviceProvider;
|
|
this.actions = new DatabaseDataActions(this);
|
|
this.access = DatabaseDataAccessMode.Default;
|
|
this.results = [];
|
|
this.activeOperationStack = [];
|
|
this.offset = 0;
|
|
this.count = 0;
|
|
this.prevOptions = null;
|
|
this.options = null;
|
|
this.disabled = false;
|
|
this.outdated = true;
|
|
this.constraintsAvailable = true;
|
|
this.onOperation = new Executor();
|
|
this.dataFormat = ResultDataFormat.Resultset;
|
|
this.supportedDataFormats = [];
|
|
this.requestInfo = {
|
|
originalQuery: '',
|
|
requestDuration: 0,
|
|
requestMessage: '',
|
|
requestFilter: '',
|
|
source: null,
|
|
};
|
|
this.error = null;
|
|
this.lastAction = this.requestData.bind(this);
|
|
|
|
makeObservable<DatabaseDataSource<TOptions, TResult>, 'disabled' | 'activeOperationStack' | 'outdated'>(this, {
|
|
access: observable,
|
|
dataFormat: observable,
|
|
supportedDataFormats: observable,
|
|
results: observable,
|
|
offset: observable,
|
|
count: observable,
|
|
prevOptions: observable,
|
|
options: observable,
|
|
requestInfo: observable,
|
|
error: observable.ref,
|
|
disabled: observable,
|
|
constraintsAvailable: observable.ref,
|
|
outdated: observable.ref,
|
|
activeOperationStack: observable.shallow,
|
|
setResults: action,
|
|
setSupportedDataFormats: action,
|
|
resetData: action,
|
|
});
|
|
}
|
|
|
|
tryGetAction<T extends IDatabaseDataActionClass<TOptions, TResult, any>>(resultIndex: number, action: T): InstanceType<T> | undefined;
|
|
tryGetAction<T extends IDatabaseDataActionClass<TOptions, TResult, any>>(result: TResult, action: T): InstanceType<T> | undefined;
|
|
tryGetAction<T extends IDatabaseDataActionClass<TOptions, TResult, any>>(resultIndex: number | TResult, action: T): InstanceType<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<T extends IDatabaseDataActionClass<TOptions, TResult, any>>(resultIndex: number, action: T): InstanceType<T>;
|
|
getAction<T extends IDatabaseDataActionClass<TOptions, TResult, any>>(result: TResult, action: T): InstanceType<T>;
|
|
getAction<T extends IDatabaseDataActionClass<TOptions, TResult, any>>(resultIndex: number | TResult, action: T): InstanceType<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<T extends IDatabaseDataActionInterface<TOptions, TResult, any>>(
|
|
resultIndex: number,
|
|
action: T,
|
|
): InstanceType<T> | undefined;
|
|
getActionImplementation<T extends IDatabaseDataActionInterface<TOptions, TResult, any>>(result: TResult, action: T): InstanceType<T> | undefined;
|
|
getActionImplementation<T extends IDatabaseDataActionInterface<TOptions, TResult, any>>(
|
|
resultIndex: number | TResult,
|
|
action: T,
|
|
): InstanceType<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);
|
|
}
|
|
|
|
async cancel() {
|
|
if (this.activeOperation instanceof Task) {
|
|
await this.activeOperation.cancel();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
getResults(): TResult[] {
|
|
return this.results;
|
|
}
|
|
|
|
setOutdated(): this {
|
|
this.outdated = true;
|
|
return this;
|
|
}
|
|
|
|
setResults(results: TResult[]): this {
|
|
results = observable(results);
|
|
this.actions.updateResults(results);
|
|
this.results = results;
|
|
return this;
|
|
}
|
|
|
|
isError(): boolean {
|
|
return this.error !== null;
|
|
}
|
|
|
|
isOutdated(): boolean {
|
|
return this.outdated;
|
|
}
|
|
|
|
isDataAvailable(offset: number, count: number): boolean {
|
|
return this.offset <= offset && this.count >= count;
|
|
}
|
|
|
|
isLoadable(): boolean {
|
|
return !this.isLoading() && !this.disabled;
|
|
}
|
|
|
|
hasElementIdentifier(resultIndex: number): boolean {
|
|
return this.getResult(resultIndex)?.data?.hasRowIdentifier === true;
|
|
}
|
|
|
|
isReadonly(resultIndex: number): boolean {
|
|
return this.access === DatabaseDataAccessMode.Readonly || this.results.length > 1 || this.disabled;
|
|
}
|
|
|
|
isLoading(): boolean {
|
|
return !!this.activeOperation;
|
|
}
|
|
|
|
isDisabled(resultIndex?: number): boolean {
|
|
if (resultIndex === undefined) {
|
|
return !this.results.length && this.error === null;
|
|
}
|
|
return this.hasResult(resultIndex) && !this.getResult(resultIndex)?.data && this.error === null;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
setConstraintsAvailable(value: boolean) {
|
|
this.constraintsAvailable = value;
|
|
return this;
|
|
}
|
|
|
|
setError(error: Error): this {
|
|
this.error = error;
|
|
return this;
|
|
}
|
|
|
|
async retry(): Promise<void> {
|
|
await this.lastAction();
|
|
}
|
|
|
|
async runOperation<T>(task: () => Promise<T>): Promise<T | null> {
|
|
return this.tryExecuteOperation(DatabaseDataSourceOperation.Task, task);
|
|
}
|
|
|
|
async requestData(mutation?: () => void): Promise<void> {
|
|
await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, () => {
|
|
this.lastAction = this.requestData.bind(this);
|
|
|
|
mutation?.();
|
|
return this.requestDataAction();
|
|
});
|
|
}
|
|
|
|
async requestDataPortion(offset: number, count: number): Promise<void> {
|
|
await this.tryExecuteOperation<TResult[] | void>(DatabaseDataSourceOperation.Request, () => {
|
|
if (!this.isDataAvailable(offset, count)) {
|
|
this.lastAction = this.requestDataPortion.bind(this, offset, count);
|
|
|
|
this.setSlice(offset, count);
|
|
return this.requestDataAction();
|
|
}
|
|
return Promise.resolve();
|
|
});
|
|
}
|
|
|
|
async refreshData(): Promise<void> {
|
|
await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, () => {
|
|
this.lastAction = this.refreshData.bind(this);
|
|
|
|
if (this.prevOptions) {
|
|
this.options = toJS(this.prevOptions);
|
|
}
|
|
|
|
return this.requestDataAction();
|
|
});
|
|
}
|
|
|
|
async saveData(): Promise<void> {
|
|
await this.tryExecuteOperation(DatabaseDataSourceOperation.Save, () => {
|
|
this.lastAction = this.saveData.bind(this);
|
|
|
|
return this.save(this.results).then(data => {
|
|
this.setResults(data);
|
|
});
|
|
});
|
|
}
|
|
|
|
async canSafelyDispose(): Promise<boolean> {
|
|
try {
|
|
const result = await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, async () => true);
|
|
return result || false;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
clearError(): this {
|
|
this.error = null;
|
|
return this;
|
|
}
|
|
|
|
resetData(): this {
|
|
this.clearError();
|
|
this.setResults([]);
|
|
this.setOutdated();
|
|
return this;
|
|
}
|
|
|
|
async dispose(): Promise<void> {
|
|
await this.cancel();
|
|
}
|
|
|
|
abstract request(prevResults: TResult[]): Promise<TResult[]>;
|
|
abstract save(prevResults: TResult[]): Promise<TResult[]>;
|
|
|
|
private async requestDataAction(): Promise<TResult[]> {
|
|
this.prevOptions = toJS(this.options);
|
|
return this.request(this.results)
|
|
.finally(() => {
|
|
this.outdated = false;
|
|
})
|
|
.then(data => {
|
|
if (data !== null) {
|
|
this.setResults(data);
|
|
}
|
|
return data;
|
|
});
|
|
}
|
|
|
|
private async tryExecuteOperation<T>(type: DatabaseDataSourceOperation, operation: () => Promise<T>): Promise<T | null> {
|
|
if (this.activeOperation && type === DatabaseDataSourceOperation.Request) {
|
|
await this.activeOperation;
|
|
}
|
|
|
|
const operationTask = this.executeOperation(type, operation);
|
|
try {
|
|
this.activeOperationStack.push(operationTask);
|
|
return await operationTask;
|
|
} finally {
|
|
const index = this.activeOperationStack.indexOf(operationTask);
|
|
if (index !== -1) {
|
|
this.activeOperationStack.splice(index, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
private executeOperation<T>(type: DatabaseDataSourceOperation, operation: () => Promise<T> | T): ITask<T | null> {
|
|
return new Task(async () => await this.onOperation.execute({ stage: 'request', operation: type })).run().then(contexts => {
|
|
// TODO: maybe it's better to throw an exception instead, so we will not have unexpected undefined results
|
|
if (ExecutorInterrupter.isInterrupted(contexts)) {
|
|
return null;
|
|
}
|
|
|
|
return new Task(async () => await this.onOperation.execute({ stage: 'before', operation: type }))
|
|
.run()
|
|
.then(contexts => {
|
|
if (ExecutorInterrupter.isInterrupted(contexts)) {
|
|
return null;
|
|
}
|
|
|
|
return operation();
|
|
})
|
|
.then(async result => {
|
|
await this.onOperation.execute({ stage: 'after', operation: type });
|
|
return result;
|
|
});
|
|
});
|
|
}
|
|
}
|