UNPKG

@finos/legend-application-studio

Version:
508 lines 27.7 kB
/** * 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 { getMappingElementSource, getMappingElementTarget, generateMappingTestName, } from './MappingEditorState.js'; import { observable, action, flow, computed, makeObservable, flowResult, } from 'mobx'; import { assertErrorThrown, LogEvent, guaranteeNonNullable, assertTrue, IllegalStateError, UnsupportedOperationError, uuid, tryToMinifyJSONString, toGrammarString, isValidJSONString, createUrlStringFromData, stringifyLosslessJSON, guaranteeType, ContentType, generateEnumerableNameFromToken, tryToFormatLosslessJSONString, StopWatch, } from '@finos/legend-shared'; import { createMockDataForMappingElementSource } from '../../../utils/MockDataUtils.js'; import { DEFAULT_TEST_ASSERTION_PREFIX, DEFAULT_TEST_PREFIX, EqualToJson, ServiceTest, LAMBDA_PIPE, GRAPH_MANAGER_EVENT, Class, DEPRECATED__ObjectInputData, ObjectInputType, DEPRECATED__ExpectedOutputMappingTestAssert, IdentifiedConnection, EngineRuntime, JsonModelConnection, FlatDataConnection, FlatDataInputData, Service, PureSingleExecution, RootFlatDataRecordType, PackageableElementExplicitReference, DatabaseType, RelationalDatabaseConnection, LocalH2DatasourceSpecification, DefaultH2AuthenticationStrategy, RelationalInputData, RelationalInputType, OperationSetImplementation, buildSourceInformationSourceId, TableAlias, stub_RawLambda, isStubbed_RawLambda, generateIdentifiedConnectionId, ServiceTestSuite, TestData, ConnectionTestData, DEFAULT_TEST_SUITE_PREFIX, DEPRECATED__MappingTest, ModelStore, reportGraphAnalytics, } from '@finos/legend-graph'; import { ActionAlertActionType, ActionAlertType, DEFAULT_TAB_SIZE, } from '@finos/legend-application'; import { objectInputData_setData, runtime_addIdentifiedConnection, runtime_addMapping, } from '../../../../graph-modifier/DSL_Mapping_GraphModifierHelper.js'; import { flatData_setData } from '../../../../graph-modifier/STO_FlatData_GraphModifierHelper.js'; import { service_addTestSuite, service_initNewService, service_setExecution, } from '../../../../graph-modifier/DSL_Service_GraphModifierHelper.js'; import { localH2DatasourceSpecification_setTestDataSetupCsv, localH2DatasourceSpecification_setTestDataSetupSqls, relationalInputData_setData, } from '../../../../graph-modifier/STO_Relational_GraphModifierHelper.js'; import { createEmptyEqualToJsonAssertion, createBareExternalFormat, } from '../../../utils/TestableUtils.js'; import { SERIALIZATION_FORMAT } from '../service/testable/ServiceTestEditorState.js'; import { LambdaEditorState, QueryBuilderTelemetryHelper, QUERY_BUILDER_EVENT, ExecutionPlanState, } from '@finos/legend-query-builder'; import { MappingEditorTabState } from './MappingTabManagerState.js'; export class MappingExecutionQueryState extends LambdaEditorState { editorStore; isInitializingLambda = false; query; constructor(editorStore, query) { super('', LAMBDA_PIPE); makeObservable(this, { query: observable, isInitializingLambda: observable, setIsInitializingLambda: action, updateLamba: flow, }); this.editorStore = editorStore; this.query = query; } get lambdaId() { return buildSourceInformationSourceId([this.uuid]); } setIsInitializingLambda(val) { this.isInitializingLambda = val; } *updateLamba(val) { this.query = val; yield flowResult(this.convertLambdaObjectToGrammarString({ pretty: true })); } *convertLambdaObjectToGrammarString(options) { if (!isStubbed_RawLambda(this.query)) { try { const lambdas = new Map(); lambdas.set(this.lambdaId, this.query); const isolatedLambdas = (yield this.editorStore.graphManagerState.graphManager.lambdasToPureCode(lambdas, options?.pretty)); const grammarText = isolatedLambdas.get(this.lambdaId); this.setLambdaString(grammarText !== undefined ? this.extractLambdaString(grammarText) : ''); this.clearErrors(); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.PARSING_FAILURE), error); } } else { this.clearErrors(); this.setLambdaString(''); } } // NOTE: since we don't allow edition in text mode, we don't need to implement this *convertLambdaGrammarStringToObject() { throw new UnsupportedOperationError(); } } class MappingExecutionInputDataState { uuid = uuid(); editorStore; mapping; inputData; constructor(editorStore, mapping, inputData) { this.editorStore = editorStore; this.mapping = mapping; this.inputData = inputData; } createEmbeddedData() { return undefined; } createAssertion(executionResult) { return undefined; } } export const createRuntimeForExecution = (mapping, connection, editorStore) => { const runtime = new EngineRuntime(); runtime_addMapping(runtime, PackageableElementExplicitReference.create(mapping)); runtime_addIdentifiedConnection(runtime, new IdentifiedConnection(generateIdentifiedConnectionId(runtime), connection), editorStore.changeDetectionState.observerContext); return runtime; }; export class MappingExecutionEmptyInputDataState extends MappingExecutionInputDataState { get isValid() { return false; } get runtime() { throw new IllegalStateError('Mapping execution runtime information is not specified'); } buildInputDataForTest() { throw new IllegalStateError('Mapping execution runtime information is not specified'); } } // TODO?: handle XML export class MappingExecutionObjectInputDataState extends MappingExecutionInputDataState { constructor(editorStore, mapping, _class) { super(editorStore, mapping, new DEPRECATED__ObjectInputData(PackageableElementExplicitReference.create(guaranteeNonNullable(_class)), ObjectInputType.JSON, tryToMinifyJSONString('{}'))); makeObservable(this, { isValid: computed, }); } get isValid() { return isValidJSONString(this.inputData.data); } get runtime() { assertTrue(this.isValid, 'Model-to-model mapping execution test data is not a valid JSON string'); const engineConfig = this.editorStore.graphManagerState.graphManager.TEMPORARY__getEngineConfig(); return createRuntimeForExecution(this.mapping, new JsonModelConnection(PackageableElementExplicitReference.create(ModelStore.INSTANCE), PackageableElementExplicitReference.create(guaranteeNonNullable(this.inputData.sourceClass.value)), createUrlStringFromData(tryToMinifyJSONString(this.inputData.data), ContentType.APPLICATION_JSON, engineConfig.useBase64ForAdhocConnectionDataUrls)), this.editorStore); } createEmbeddedData() { const embeddedData = createBareExternalFormat(); embeddedData.data = tryToFormatLosslessJSONString(tryToMinifyJSONString(this.inputData.data)); return embeddedData; } createAssertion(executionResult) { const jsonAssertion = new EqualToJson(); jsonAssertion.id = generateEnumerableNameFromToken([], DEFAULT_TEST_ASSERTION_PREFIX); const expected = createBareExternalFormat(); expected.data = toGrammarString(executionResult); jsonAssertion.expected = expected; return jsonAssertion; } buildInputDataForTest() { return new DEPRECATED__ObjectInputData(PackageableElementExplicitReference.create(guaranteeNonNullable(this.inputData.sourceClass.value)), this.inputData.inputType, tryToMinifyJSONString(this.inputData.data)); } } export class MappingExecutionFlatDataInputDataState extends MappingExecutionInputDataState { constructor(editorStore, mapping, rootFlatDataRecordType) { super(editorStore, mapping, new FlatDataInputData(PackageableElementExplicitReference.create(guaranteeNonNullable(rootFlatDataRecordType._OWNER._OWNER)), '')); makeObservable(this, { isValid: computed, }); } get isValid() { return true; } get runtime() { const engineConfig = this.editorStore.graphManagerState.graphManager.TEMPORARY__getEngineConfig(); return createRuntimeForExecution(this.mapping, new FlatDataConnection(PackageableElementExplicitReference.create(guaranteeNonNullable(this.inputData.sourceFlatData.value)), createUrlStringFromData(this.inputData.data, ContentType.TEXT_PLAIN, engineConfig.useBase64ForAdhocConnectionDataUrls)), this.editorStore); } buildInputDataForTest() { return new FlatDataInputData(PackageableElementExplicitReference.create(guaranteeNonNullable(this.inputData.sourceFlatData.value)), this.inputData.data); } } export class MappingExecutionRelationalInputDataState extends MappingExecutionInputDataState { constructor(editorStore, mapping, tableOrView) { super(editorStore, mapping, new RelationalInputData(PackageableElementExplicitReference.create(guaranteeNonNullable(tableOrView.schema._OWNER)), '', RelationalInputType.SQL)); makeObservable(this, { isValid: computed, }); } get isValid() { return true; } get runtime() { const datasourceSpecification = new LocalH2DatasourceSpecification(); switch (this.inputData.inputType) { case RelationalInputType.SQL: localH2DatasourceSpecification_setTestDataSetupSqls(datasourceSpecification, // NOTE: this is a gross simplification of handling the input for relational input data [this.inputData.data]); break; case RelationalInputType.CSV: localH2DatasourceSpecification_setTestDataSetupCsv(datasourceSpecification, this.inputData.data); break; default: throw new UnsupportedOperationError(`Invalid input data type`); } return createRuntimeForExecution(this.mapping, new RelationalDatabaseConnection(PackageableElementExplicitReference.create(guaranteeNonNullable(this.inputData.database.value)), DatabaseType.H2, datasourceSpecification, new DefaultH2AuthenticationStrategy()), this.editorStore); } buildInputDataForTest() { return new RelationalInputData(PackageableElementExplicitReference.create(guaranteeNonNullable(this.inputData.database.value)), this.inputData.data, this.inputData.inputType); } } export class MappingExecutionState extends MappingEditorTabState { editorStore; mappingEditorState; name; queryState; inputDataState; showServicePathModal = false; executionResultText; // NOTE: stored as lossless JSON text isExecuting = false; isGeneratingPlan = false; executionPlanState; planGenerationDebugText; executionRunPromise = undefined; constructor(editorStore, mappingEditorState, name) { super(); makeObservable(this, { name: observable, queryState: observable, inputDataState: observable, showServicePathModal: observable, executionPlanState: observable, isExecuting: observable, isGeneratingPlan: observable, planGenerationDebugText: observable, executionRunPromise: observable, setExecutionRunPromise: action, setQueryState: action, setInputDataState: action, setExecutionResultText: action, setShowServicePathModal: action, setPlanGenerationDebugText: action, setInputDataStateBasedOnSource: action, reset: action, promoteToTest: flow, promoteToService: flow, cancelExecution: flow, executeMapping: flow, generatePlan: flow, buildQueryWithClassMapping: flow, }); this.editorStore = editorStore; this.mappingEditorState = mappingEditorState; this.name = name; this.queryState = new MappingExecutionQueryState(editorStore, stub_RawLambda()); this.inputDataState = new MappingExecutionEmptyInputDataState(editorStore, mappingEditorState.mapping, undefined); this.executionPlanState = new ExecutionPlanState(this.editorStore.applicationStore, this.editorStore.graphManagerState); } get label() { return this.name; } setExecutionRunPromise(promise) { this.executionRunPromise = promise; } setQueryState(val) { this.queryState = val; } setInputDataState(val) { this.inputDataState = val; } setExecutionResultText(val) { this.executionResultText = val; } setShowServicePathModal(val) { this.showServicePathModal = val; } setPlanGenerationDebugText(val) { this.planGenerationDebugText = val; } reset() { this.queryState = new MappingExecutionQueryState(this.editorStore, stub_RawLambda()); this.inputDataState = new MappingExecutionEmptyInputDataState(this.editorStore, this.mappingEditorState.mapping, undefined); this.setExecutionResultText(undefined); } setInputDataStateBasedOnSource(source, populateWithMockData) { if (source instanceof Class) { const newRuntimeState = new MappingExecutionObjectInputDataState(this.editorStore, this.mappingEditorState.mapping, source); if (populateWithMockData) { objectInputData_setData(newRuntimeState.inputData, createMockDataForMappingElementSource(source, this.editorStore)); } this.setInputDataState(newRuntimeState); } else if (source instanceof RootFlatDataRecordType) { const newRuntimeState = new MappingExecutionFlatDataInputDataState(this.editorStore, this.mappingEditorState.mapping, source); if (populateWithMockData) { flatData_setData(newRuntimeState.inputData, createMockDataForMappingElementSource(source, this.editorStore)); } this.setInputDataState(newRuntimeState); } else if (source instanceof TableAlias) { const newRuntimeState = new MappingExecutionRelationalInputDataState(this.editorStore, this.mappingEditorState.mapping, source.relation.value); if (populateWithMockData) { relationalInputData_setData(newRuntimeState.inputData, createMockDataForMappingElementSource(source, this.editorStore)); } this.setInputDataState(newRuntimeState); } else if (source === undefined) { this.setInputDataState(new MappingExecutionEmptyInputDataState(this.editorStore, this.mappingEditorState.mapping, undefined)); } else { this.editorStore.applicationStore.notificationService.notifyWarning(new UnsupportedOperationError(`Can't build input data for the specified source`, source)); } } *promoteToTest() { try { const query = this.queryState.query; if (!isStubbed_RawLambda(this.queryState.query) && this.inputDataState.isValid && this.inputDataState.inputData && this.executionResultText) { const inputData = this.inputDataState.buildInputDataForTest(); const assert = new DEPRECATED__ExpectedOutputMappingTestAssert(toGrammarString(this.executionResultText)); const mappingTest = new DEPRECATED__MappingTest(generateMappingTestName(this.mappingEditorState.mapping), query, [inputData], assert); yield flowResult(this.mappingEditorState.addTest(mappingTest)); this.mappingEditorState.closeTab(this); // after promoting to test, remove the execution state } } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error); this.editorStore.applicationStore.notificationService.notifyError(error); } } *promoteToService(packagePath, serviceName) { try { const query = this.queryState.query; if (!isStubbed_RawLambda(this.queryState.query) && this.inputDataState.isValid && this.executionResultText) { if (this.inputDataState instanceof MappingExecutionObjectInputDataState) { const service = new Service(serviceName); const engineRuntime = this.inputDataState.runtime; service_initNewService(service); const pureSingleExecution = new PureSingleExecution(query, service, PackageableElementExplicitReference.create(this.mappingEditorState.mapping), engineRuntime); service_setExecution(service, pureSingleExecution, this.editorStore.changeDetectionState.observerContext); const suite = new ServiceTestSuite(); suite.id = generateEnumerableNameFromToken([], DEFAULT_TEST_SUITE_PREFIX); suite.testData = new TestData(); const embeddedData = this.inputDataState.createEmbeddedData(); const connection = engineRuntime.connections[0]?.storeConnections[0]; if (embeddedData && connection) { const connectionTestData = new ConnectionTestData(); connectionTestData.connectionId = connection.id; connectionTestData.testData = embeddedData; suite.testData.connectionsTestData = [connectionTestData]; } const test = new ServiceTest(); test.serializationFormat = SERIALIZATION_FORMAT.PURE; test.id = generateEnumerableNameFromToken([], DEFAULT_TEST_PREFIX); test.__parent = suite; suite.tests = [test]; const assertion = this.inputDataState.createAssertion(this.executionResultText) ?? createEmptyEqualToJsonAssertion(test); test.assertions = [assertion]; assertion.parentTest = test; service_addTestSuite(service, suite, this.editorStore.changeDetectionState.observerContext); yield flowResult(this.editorStore.graphEditorMode.addElement(service, packagePath, true)); } else { throw new UnsupportedOperationError(`Can't build service from input data state`, this.inputDataState); } } } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error); this.editorStore.applicationStore.notificationService.notifyError(error); } } *cancelExecution() { this.isExecuting = false; this.setExecutionRunPromise(undefined); try { yield this.editorStore.graphManagerState.graphManager.cancelUserExecutions(true); } catch (error) { // don't notify users about success or failure this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error); } } *executeMapping() { let promise; try { const query = this.queryState.query; const runtime = this.inputDataState.runtime; if (!isStubbed_RawLambda(this.queryState.query) && this.inputDataState.isValid && !this.isExecuting) { this.isExecuting = true; QueryBuilderTelemetryHelper.logEvent_QueryRunLaunched(this.editorStore.applicationStore.telemetryService); const stopWatch = new StopWatch(); const report = reportGraphAnalytics(this.editorStore.graphManagerState.graph); promise = this.editorStore.graphManagerState.graphManager.runQuery(query, this.mappingEditorState.mapping, runtime, this.editorStore.graphManagerState.graph, { useLosslessParse: true, }, report); this.setExecutionRunPromise(promise); const result = (yield promise); if (this.executionRunPromise === promise) { this.setExecutionResultText(stringifyLosslessJSON(result.executionResult, undefined, DEFAULT_TAB_SIZE)); // report report.timings = this.editorStore.applicationStore.timeService.finalizeTimingsRecord(stopWatch, report.timings); QueryBuilderTelemetryHelper.logEvent_QueryRunSucceeded(this.editorStore.applicationStore.telemetryService, report); } } } catch (error) { if (this.executionRunPromise === promise) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error); this.editorStore.applicationStore.notificationService.notifyError(error); this.setExecutionResultText(''); } } finally { this.isExecuting = false; } } *generatePlan(debug) { try { const query = this.queryState.query; const runtime = this.inputDataState.runtime; if (!isStubbed_RawLambda(this.queryState.query) && this.inputDataState.isValid && !this.isGeneratingPlan) { this.isGeneratingPlan = true; let rawPlan; const stopWatch = new StopWatch(); const report = reportGraphAnalytics(this.editorStore.graphManagerState.graph); if (debug) { QueryBuilderTelemetryHelper.logEvent_ExecutionPlanDebugLaunched(this.editorStore.applicationStore.telemetryService); const debugResult = (yield this.editorStore.graphManagerState.graphManager.debugExecutionPlanGeneration(query, this.mappingEditorState.mapping, runtime, this.editorStore.graphManagerState.graph, undefined, report)); rawPlan = debugResult.plan; this.executionPlanState.setDebugText(debugResult.debug); } else { QueryBuilderTelemetryHelper.logEvent_ExecutionPlanGenerationLaunched(this.editorStore.applicationStore.telemetryService); rawPlan = (yield this.editorStore.graphManagerState.graphManager.generateExecutionPlan(query, this.mappingEditorState.mapping, runtime, this.editorStore.graphManagerState.graph, undefined, report)); } stopWatch.record(); try { this.executionPlanState.setRawPlan(rawPlan); const plan = this.editorStore.graphManagerState.graphManager.buildExecutionPlan(rawPlan, this.editorStore.graphManagerState.graph); this.executionPlanState.initialize(plan); } catch { // do nothing } stopWatch.record(QUERY_BUILDER_EVENT.BUILD_EXECUTION_PLAN__SUCCESS); // report report.timings = this.editorStore.applicationStore.timeService.finalizeTimingsRecord(stopWatch, report.timings); if (debug) { QueryBuilderTelemetryHelper.logEvent_ExecutionPlanDebugSucceeded(this.editorStore.applicationStore.telemetryService, report); } else { QueryBuilderTelemetryHelper.logEvent_ExecutionPlanGenerationSucceeded(this.editorStore.applicationStore.telemetryService, report); } } } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error); this.editorStore.applicationStore.notificationService.notifyError(error); } finally { this.isGeneratingPlan = false; } } *buildQueryWithClassMapping(setImplementation) { // do all the necessary updates this.setExecutionResultText(undefined); yield flowResult(this.queryState.updateLamba(setImplementation ? this.editorStore.graphManagerState.graphManager.createGetAllRawLambda(guaranteeType(getMappingElementTarget(setImplementation), Class)) : stub_RawLambda())); // Attempt to generate data for input data panel as we pick the class mapping: // - If the source panel is empty right now, automatically try to generate input data: // - We generate based on the class mapping, if it's concrete // - If the class mapping is operation, output a warning message // - If the source panel is non-empty (show modal), show an option to keep current input data if (setImplementation) { if (this.inputDataState instanceof MappingExecutionEmptyInputDataState) { if (setImplementation instanceof OperationSetImplementation) { this.editorStore.applicationStore.notificationService.notifyWarning(`Can't auto-generate input data for operation class mapping. Please pick a concrete class mapping instead`); } else { this.setInputDataStateBasedOnSource(getMappingElementSource(setImplementation, this.editorStore.pluginManager.getApplicationPlugins()), true); } } else { this.editorStore.applicationStore.alertService.setActionAlertInfo({ message: 'Mapping execution input data is already set', prompt: 'Do you want to regenerate the input data?', type: ActionAlertType.CAUTION, actions: [ { label: 'Regenerate', type: ActionAlertActionType.PROCEED_WITH_CAUTION, handler: () => this.setInputDataStateBasedOnSource(getMappingElementSource(setImplementation, this.editorStore.pluginManager.getApplicationPlugins()), true), }, { label: 'Keep my input data', type: ActionAlertActionType.PROCEED, default: true, }, ], }); } } } } //# sourceMappingURL=MappingExecutionState.js.map