@finos/legend-extension-dsl-data-quality
Version:
Legend extension for Data Quality
417 lines (384 loc) • 14.6 kB
text/typescript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* 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 {
type ExportDataInfo,
buildExecutionParameterValues,
ExecutionPlanState,
QUERY_BUILDER_EVENT,
QueryBuilderTelemetryHelper,
PARAMETER_SUBMIT_ACTION,
} from '@finos/legend-query-builder';
import { action, flow, flowResult, makeObservable, observable } from 'mobx';
import {
type GeneratorFn,
assertErrorThrown,
LogEvent,
StopWatch,
getContentTypeFileExtension,
ActionState,
ContentType,
UnsupportedOperationError,
} from '@finos/legend-shared';
import {
type ExecutionResult,
type RawExecutionPlan,
GRAPH_MANAGER_EVENT,
V1_DELEGATED_EXPORT_HEADER,
EXECUTION_SERIALIZATION_FORMAT,
} from '@finos/legend-graph';
import { getDataQualityPureGraphManagerExtension } from '../../graph-manager/protocol/pure/DSL_DataQuality_PureGraphManagerExtension.js';
import type { DataQualityRelationValidationConfigurationState } from './DataQualityRelationValidationConfigurationState.js';
import type { DataQualityRelationValidation } from '../../graph/metamodel/pure/packageableElements/data-quality/DataQualityValidationConfiguration.js';
import { downloadStream } from '@finos/legend-application';
export type DataQualityRelationResultCellDataType =
| string
| number
| boolean
| null
| undefined;
export interface DataQualityRelationRowDataType {
[key: string]: DataQualityRelationResultCellDataType;
}
export interface DataQualityRelationResultCellCoordinate {
rowIndex: number;
colIndex: number;
}
export const DEFAULT_LIMIT = 1000;
export class DataQualityRelationResultState {
readonly dataQualityRelationValidationConfigurationState: DataQualityRelationValidationConfigurationState;
readonly executionPlanState: ExecutionPlanState;
readonly exportState = ActionState.create();
previewLimit = DEFAULT_LIMIT;
isRunningValidation = false;
isGeneratingPlan = false;
allValidationsChecked = false;
executionResult?: ExecutionResult | undefined;
executionDuration?: number | undefined;
latestRunHashCode?: string | undefined;
validationRunPromise: Promise<ExecutionResult> | undefined = undefined;
validationToRun: DataQualityRelationValidation | undefined;
constructor(
dataQualityRelationValidationState: DataQualityRelationValidationConfigurationState,
) {
makeObservable(this, {
executionResult: observable,
previewLimit: observable,
executionDuration: observable,
latestRunHashCode: observable,
validationRunPromise: observable,
isGeneratingPlan: observable,
isRunningValidation: observable,
validationToRun: observable,
allValidationsChecked: observable,
setIsRunningValidation: action,
setExecutionResult: action,
setExecutionDuration: action,
setPreviewLimit: action,
setValidationRunPromise: action,
setValidationToRun: action,
handleRunValidation: action,
handleExport: action,
onToggleAllValidationsChecked: action,
runValidation: flow,
cancelValidation: flow,
generatePlan: flow,
exportData: flow,
});
this.dataQualityRelationValidationConfigurationState =
dataQualityRelationValidationState;
this.executionPlanState = new ExecutionPlanState(
this.dataQualityRelationValidationConfigurationState.editorStore.applicationStore,
this.dataQualityRelationValidationConfigurationState.editorStore.graphManagerState,
);
}
setIsRunningValidation(val: boolean): void {
this.isRunningValidation = val;
}
setExecutionResult(val: ExecutionResult | undefined): void {
this.executionResult = val;
}
setExecutionDuration(val: number | undefined): void {
this.executionDuration = val;
}
onToggleAllValidationsChecked(val: boolean): void {
this.allValidationsChecked = !val;
}
setPreviewLimit(val: number): void {
this.previewLimit = Math.max(1, val);
}
setValidationRunPromise(promise: Promise<ExecutionResult> | undefined): void {
this.validationRunPromise = promise;
}
setValidationToRun(validation: DataQualityRelationValidation): void {
this.validationToRun = validation;
}
get checkForStaleResults(): boolean {
if (
this.latestRunHashCode !==
this.dataQualityRelationValidationConfigurationState.hashCode
) {
return true;
}
return false;
}
handleRunValidation() {
const queryLambda =
this.dataQualityRelationValidationConfigurationState
.bodyExpressionSequence;
const parameters = (queryLambda.parameters ?? []) as object[];
if (parameters.length) {
this.dataQualityRelationValidationConfigurationState.parametersState.openModal(
queryLambda,
() => this.runValidation(),
);
} else {
flowResult(this.runValidation()).catch(
this.dataQualityRelationValidationConfigurationState.editorStore
.applicationStore.alertUnhandledError,
);
}
}
handleExport(format: string) {
const queryLambda =
this.dataQualityRelationValidationConfigurationState
.bodyExpressionSequence;
const parameters = (queryLambda.parameters ?? []) as object[];
if (parameters.length) {
this.dataQualityRelationValidationConfigurationState.parametersState.parameterStates =
this.dataQualityRelationValidationConfigurationState.parametersState.build(
queryLambda,
);
this.dataQualityRelationValidationConfigurationState.parametersState.parameterValuesEditorState.open(
(): Promise<void> =>
flowResult(this.exportData(format)).catch(
this.dataQualityRelationValidationConfigurationState.editorStore
.applicationStore.alertUnhandledError,
),
PARAMETER_SUBMIT_ACTION.EXPORT,
);
} else {
flowResult(this.exportData(format)).catch(
this.dataQualityRelationValidationConfigurationState.editorStore
.applicationStore.alertUnhandledError,
);
}
}
*runValidation(): GeneratorFn<void> {
let promise;
try {
this.setIsRunningValidation(true);
const currentHashCode =
this.dataQualityRelationValidationConfigurationState.hashCode;
const packagePath =
this.dataQualityRelationValidationConfigurationState.validationElement
.path;
const model =
this.dataQualityRelationValidationConfigurationState.editorStore
.graphManagerState.graph;
const stopWatch = new StopWatch();
promise = getDataQualityPureGraphManagerExtension(
this.dataQualityRelationValidationConfigurationState.editorStore
.graphManagerState.graphManager,
).execute(model, packagePath, {
previewLimit: this.previewLimit,
allValidationsChecked: this.allValidationsChecked,
validationName: this.validationToRun?.name,
lambdaParameterValues: buildExecutionParameterValues(
this.dataQualityRelationValidationConfigurationState.parametersState
.parameterStates,
this.dataQualityRelationValidationConfigurationState.editorStore
.graphManagerState,
),
});
this.setValidationRunPromise(promise);
const result = (yield promise) as ExecutionResult;
if (this.validationRunPromise === promise) {
this.setExecutionResult(result);
this.latestRunHashCode = currentHashCode;
this.setExecutionDuration(stopWatch.elapsed);
}
} catch (error) {
if (this.validationRunPromise === promise) {
assertErrorThrown(error);
this.setExecutionResult(undefined);
this.dataQualityRelationValidationConfigurationState.editorStore.applicationStore.logService.error(
LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE),
error,
);
this.dataQualityRelationValidationConfigurationState.editorStore.applicationStore.notificationService.notifyError(
error,
);
}
} finally {
this.setIsRunningValidation(false);
}
}
*cancelValidation(): GeneratorFn<void> {
this.setIsRunningValidation(false);
this.setValidationRunPromise(undefined);
try {
yield this.dataQualityRelationValidationConfigurationState.editorStore.graphManagerState.graphManager.cancelUserExecutions(
true,
);
} catch (error) {
// Don't notify users about success or failure
this.dataQualityRelationValidationConfigurationState.editorStore.applicationStore.logService.error(
LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE),
error,
);
}
}
*generatePlan(debug: boolean): GeneratorFn<void> {
const packagePath =
this.dataQualityRelationValidationConfigurationState.validationElement
.path;
const model =
this.dataQualityRelationValidationConfigurationState.editorStore
.graphManagerState.graph;
try {
this.isGeneratingPlan = true;
let rawPlan: RawExecutionPlan;
const stopWatch = new StopWatch();
if (debug) {
const debugResult = (yield getDataQualityPureGraphManagerExtension(
this.dataQualityRelationValidationConfigurationState.editorStore
.graphManagerState.graphManager,
).debugExecutionPlanGeneration(model, packagePath, {
validationName: this.validationToRun?.name,
allValidationsChecked: this.allValidationsChecked,
previewLimit: this.previewLimit,
})) as {
plan: RawExecutionPlan;
debug: string;
};
rawPlan = debugResult.plan;
this.executionPlanState.setDebugText(debugResult.debug);
} else {
QueryBuilderTelemetryHelper.logEvent_ExecutionPlanGenerationLaunched(
this.dataQualityRelationValidationConfigurationState.editorStore
.applicationStore.telemetryService,
);
rawPlan = (yield getDataQualityPureGraphManagerExtension(
this.dataQualityRelationValidationConfigurationState.editorStore
.graphManagerState.graphManager,
).generatePlan(model, packagePath, {
validationName: this.validationToRun?.name,
allValidationsChecked: this.allValidationsChecked,
previewLimit: this.previewLimit,
})) as RawExecutionPlan;
}
stopWatch.record();
try {
this.executionPlanState.setRawPlan(rawPlan);
const plan =
this.dataQualityRelationValidationConfigurationState.editorStore.graphManagerState.graphManager.buildExecutionPlan(
rawPlan,
this.dataQualityRelationValidationConfigurationState.editorStore
.graphManagerState.graph,
);
this.executionPlanState.initialize(plan);
} catch {
//do nothing
}
stopWatch.record(QUERY_BUILDER_EVENT.BUILD_EXECUTION_PLAN__SUCCESS);
} catch (error) {
assertErrorThrown(error);
this.dataQualityRelationValidationConfigurationState.editorStore.applicationStore.logService.error(
LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE),
error,
);
this.dataQualityRelationValidationConfigurationState.editorStore.applicationStore.notificationService.notifyError(
error,
);
} finally {
this.isGeneratingPlan = false;
}
}
getExportDataInfo(format: string): ExportDataInfo {
switch (format) {
case EXECUTION_SERIALIZATION_FORMAT.CSV:
return {
contentType: ContentType.TEXT_CSV,
serializationFormat: EXECUTION_SERIALIZATION_FORMAT.CSV,
};
default:
throw new UnsupportedOperationError(
`Unsupported TDS export type ${format}`,
);
}
}
get exportDataFormatOptions(): string[] {
return [EXECUTION_SERIALIZATION_FORMAT.CSV];
}
*exportData(format: string): GeneratorFn<void> {
try {
this.exportState.inProgress();
const packagePath =
this.dataQualityRelationValidationConfigurationState.validationElement
.path;
const model =
this.dataQualityRelationValidationConfigurationState.editorStore
.graphManagerState.graph;
this.dataQualityRelationValidationConfigurationState.editorStore.applicationStore.notificationService.notifySuccess(
`Export ${format} will run in background`,
);
const exportData = this.getExportDataInfo(format);
const contentType = exportData.contentType;
const serializationFormat = exportData.serializationFormat;
const result = (yield getDataQualityPureGraphManagerExtension(
this.dataQualityRelationValidationConfigurationState.editorStore
.graphManagerState.graphManager,
).exportData(model, packagePath, {
serializationFormat,
previewLimit: this.previewLimit,
validationName: this.validationToRun?.name,
allValidationsChecked: this.allValidationsChecked,
lambdaParameterValues: buildExecutionParameterValues(
this.dataQualityRelationValidationConfigurationState.parametersState
.parameterStates,
this.dataQualityRelationValidationConfigurationState.editorStore
.graphManagerState,
),
})) as Response;
if (result.headers.get(V1_DELEGATED_EXPORT_HEADER) === 'true') {
if (result.status === 200) {
this.exportState.pass();
} else {
this.exportState.fail();
}
return;
}
downloadStream(
result,
`result.${getContentTypeFileExtension(contentType)}`,
exportData.contentType,
)
.then(() => {
this.exportState.pass();
})
.catch((error) => {
assertErrorThrown(error);
});
} catch (error) {
this.exportState.fail();
assertErrorThrown(error);
this.dataQualityRelationValidationConfigurationState.editorStore.applicationStore.notificationService.notifyError(
error,
);
this.exportState.complete();
}
}
}