/* * 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 } from 'mobx'; import type { NotificationService } from '@cloudbeaver/core-events'; import type { GraphQLService, SqlDataFilterConstraint, SqlResultSet } from '@cloudbeaver/core-sdk'; import { EDeferredState } from '@cloudbeaver/core-utils'; import { DatabaseDataEditor } from './DatabaseDataModel/DatabaseDataEditor'; import { DatabaseDataSource } from './DatabaseDataModel/DatabaseDataSource'; import type { IDatabaseDataResult } from './DatabaseDataModel/IDatabaseDataResult'; import { FetchTableDataAsyncProcess } from './FetchTableDataAsyncProcess'; import type { IExecutionContext } from './IExecutionContext'; import type { RowDiff } from './TableViewer/TableDataModel/EditedRow'; import type { IRequestDataResult } from './TableViewer/TableViewerModel'; export interface IDataContainerOptions { containerNodePath: string; sourceName?: string; // TODO: should be refactored, used only in QueryDataSource connectionId: string; whereFilter: string; constraints: SqlDataFilterConstraint[]; } export interface IDataContainerResult extends IDatabaseDataResult { data: SqlResultSet | undefined; } export class ContainerDataSource extends DatabaseDataSource { currentFetchTableProcess: FetchTableDataAsyncProcess | null; get canCancel(): boolean { return this.currentFetchTableProcess?.getState() === EDeferredState.PENDING; } constructor( private graphQLService: GraphQLService, private notificationService: NotificationService, ) { super(); makeObservable(this, { currentFetchTableProcess: observable, }); this.currentFetchTableProcess = null; this.executionContext = null; this.editor = new DatabaseDataEditor(); } cancel(): boolean { if (this.currentFetchTableProcess) { return this.currentFetchTableProcess.cancel(); } return false; } async request( prevResults: IDataContainerResult[] ): Promise { if (!this.options?.containerNodePath) { throw new Error('containerNodePath must be provided for table'); } const executionContext = await this.ensureContextCreated(); const offset = this.offset; const limit = this.count; const fetchTableProcess = new FetchTableDataAsyncProcess(this.graphQLService, this.notificationService); fetchTableProcess.start( { connectionId: executionContext.connectionId, contextId: executionContext.contextId, containerNodePath: this.options.containerNodePath, }, { offset, limit, constraints: this.options.constraints, where: this.options.whereFilter || undefined, }, this.dataFormat, ); this.currentFetchTableProcess = fetchTableProcess; const response = await fetchTableProcess.promise; this.requestInfo = { requestDuration: response?.duration || 0, requestMessage: response?.statusMessage || '', }; if (!response?.results) { return prevResults; } return response.results.map(result => ({ id: result.resultSet?.id || '0', dataFormat: result.dataFormat!, loadedFully: (result.resultSet?.rows?.length || 0) < limit, data: result.resultSet, })); } async save( prevResults: IDataContainerResult[] ): Promise { const executionContext = await this.ensureContextCreated(); const changes = this.editor?.getChanges(); if (!changes) { return prevResults; } for (const update of changes) { const response = await this.graphQLService.sdk.updateResultsDataBatch({ connectionId: executionContext.connectionId, contextId: executionContext.contextId, resultsId: update.resultId, updatedRows: Array.from(update.diff.values()).map(diff => ({ data: diff.source, updateValues: diff.update.reduce((obj, value, index) => { if (value !== diff.source[index]) { obj[index] = value; } return obj; }, {}), })), }); this.requestInfo = { requestDuration: response.result?.duration || 0, requestMessage: 'Saved successfully', }; const result = prevResults.find(result => result.id === update.resultId)!; const responseResult = response.result?.results.find(result => result.resultSet?.id === update.resultId); if (responseResult?.resultSet?.rows && result.data?.rows) { let i = 0; for (const row of update.diff.keys()) { result.data.rows[row] = responseResult.resultSet.rows[i]; i++; } } this.editor?.cancelResultChanges(result); } return prevResults; } /** * @deprecated will be refactored */ async saveDeprecated(resultId: string, rows: RowDiff[]): Promise { const executionContext = await this.ensureContextCreated(); const response = await this.graphQLService.sdk.updateResultsDataBatch({ connectionId: executionContext.connectionId, contextId: executionContext.contextId, resultsId: resultId, updatedRows: rows.map(row => ({ data: row.source, updateValues: row.values })), }); const dataSet = response.result!.results[0].resultSet!; // we expect only one dataset for a table this.requestInfo = { requestDuration: response.result?.duration || 0, requestMessage: 'Saved successfully', }; return { rows: dataSet.rows!, columns: [], // not in use while saving data duration: response.result!.duration, isFullyLoaded: false, // not in use while saving data statusMessage: 'Saved successfully', }; } async dispose(): Promise { if (this.executionContext) { await this.graphQLService.sdk.sqlContextDestroy({ connectionId: this.executionContext.connectionId, contextId: this.executionContext.contextId, }); } } private async ensureContextCreated(): Promise { if (!this.executionContext) { if (!this.options) { throw new Error('Options must be provided'); } this.executionContext = await this.createExecutionContext(this.options.connectionId); } return this.executionContext; } private async createExecutionContext( connectionId: string, defaultCatalog?: string, defaultSchema?: string ): Promise { const response = await this.graphQLService.sdk.sqlContextCreate({ connectionId, defaultCatalog, defaultSchema, }); return { contextId: response.context.id, connectionId, objectCatalogId: response.context.defaultCatalog, objectSchemaId: response.context.defaultSchema, }; } }