/
alt3rmann
/
cloudbeaver
Обзор
Документация
Войти
/
alt3rmann
/
cloudbeaver
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
devel
webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts
396 строк
12 KB
Alexey Potsetsuev
dbeaver/pro#7080 refactor: grid API (#3799)
13 окт 2025, 14:30
Не верифицирован
13 окт 2025, 14:30
682a387
Код
Авторство
О чём код?
/* * CloudBeaver - Cloud Database Manager * Copyright (C) 2020-2025 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 { withExternal, type IServiceProvider, type IServiceScope, type SingleServiceType } from '@cloudbeaver/core-di'; import { Executor, ExecutorInterrupter, type IExecutor, type ISyncExecutor, type ITask, SyncExecutor, Task } from '@cloudbeaver/core-executor'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { IDatabaseDataActions } from './IDatabaseDataActions.js'; import type { IDatabaseDataResult } from './IDatabaseDataResult.js'; import { DatabaseDataAccessMode, DatabaseDataSourceOperation, 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>; readonly onResultsUpdate: ISyncExecutor<TResult[]>; private get activeOperation(): Promise<void> | null { return this.activeOperationStack[this.activeOperationStack.length - 1] ?? null; } private readonly activeOperationStack: Array<Promise<any>>; private readonly dataSourceScope: IServiceScope; constructor(serviceProvider: IServiceProvider) { this.serviceProvider = serviceProvider; this.dataSourceScope = this.serviceProvider.createScope(); this.actions = this.dataSourceScope.serviceProvider.getService( withExternal(IDatabaseDataActions).set(IDatabaseDataSource, this as unknown), ) as unknown as IDatabaseDataActions<TOptions, TResult>; 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.onResultsUpdate = new SyncExecutor(); 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>(resultIndex: number, action: SingleServiceType<T, any[]>): T | undefined; tryGetAction<T>(result: TResult, action: SingleServiceType<T, any[]>): T | undefined; tryGetAction<T>(resultIndex: number, action: SingleServiceType<unknown, unknown[]>, implementation: SingleServiceType<T, any[]>): T; tryGetAction<T>(result: TResult, action: SingleServiceType<unknown, unknown[]>, implementation: SingleServiceType<T, any[]>): T; tryGetAction<T>(resultIndex: number | TResult, action: SingleServiceType<T, any[]>, implementation?: SingleServiceType<unknown>): T | undefined { if (typeof resultIndex === 'number') { if (!this.hasResult(resultIndex)) { return undefined; } return this.actions.tryGet(this.results[resultIndex]!, action, implementation!) as T; } return this.actions.tryGet(resultIndex, action, implementation!) as T; } getAction<T>(resultIndex: number, action: SingleServiceType<T, any[]>): T; getAction<T>(result: TResult, action: SingleServiceType<T, any[]>): T; getAction<T>(resultIndex: number, action: SingleServiceType<unknown, unknown[]>, implementation: SingleServiceType<T, any[]>): T; getAction<T>(result: TResult, action: SingleServiceType<unknown, unknown[]>, implementation: SingleServiceType<T, any[]>): T; getAction<T>(resultIndex: number | TResult, action: SingleServiceType<T, any[]>, implementation?: SingleServiceType<unknown>): 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, implementation!) as T; } return this.actions.get(resultIndex, action, implementation!) as T; } async cancel(): Promise<void> { 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.onResultsUpdate.execute(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; } // TODO: probably should be moved to the DataResultAction hasElementIdentifier(resultIndex: number): boolean { return false; } 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 { this.constraintsAvailable = value; return this; } setError(error: Error): this { this.error = error; return this; } async retry(): Promise<void> { await this.lastAction(); } 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); //TODO: Remove this when we have virtual keys. We need to refresh the data in tables without a primary key to avoid UI glitch #5140. if (!this.hasElementIdentifier(0)) { this.setOutdated(); } }); }); } async canSafelyDispose(): Promise<boolean> { try { const result = await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, () => Promise.resolve(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(); this.dataSourceScope[Symbol.dispose]?.(); } abstract request(prevResults: TResult[]): Promise<TResult[]>; abstract save(prevResults: TResult[]): Promise<TResult[]>; private 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; }); }); } }