@finos/legend-application-studio
Version:
Legend Studio application core
508 lines • 27.7 kB
JavaScript
/**
* 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