From b0ce7db86b0c5366a2bc5eb687747f6d2cf2c24b Mon Sep 17 00:00:00 2001 From: Mauricio Uyaguari Date: Fri, 15 Dec 2023 14:03:30 -0500 Subject: [PATCH] add migration tool to migrate legacy mapping tests (#2821) --- .changeset/seven-bears-trade.md | 5 + .changeset/thick-wasps-watch.md | 4 + .../mapping-editor/MappingEditor.tsx | 11 +- .../mapping-editor/MappingTestsExplorer.tsx | 42 +- .../DEPRECATED__MappingTestEditor.tsx | 18 +- .../legacy/MappingTestMigrationTool.tsx | 156 +++++++ .../legend-application-studio/src/index.ts | 2 +- .../mapping/MappingEditorState.ts | 47 ++- .../mapping/MappingExecutionState.ts | 28 +- .../DEPRECATED__MappingTestState.ts | 45 +- .../legacy/MappingTestMigrationState.ts | 383 ++++++++++++++++++ .../DSL_Mapping_GraphModifierHelper.ts | 16 +- .../components/editor/_mapping-editor.scss | 5 + packages/legend-graph/src/index.ts | 8 +- 14 files changed, 674 insertions(+), 96 deletions(-) create mode 100644 .changeset/seven-bears-trade.md create mode 100644 .changeset/thick-wasps-watch.md rename packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/{ => legacy}/DEPRECATED__MappingTestEditor.tsx (97%) create mode 100644 packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/legacy/MappingTestMigrationTool.tsx rename packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/{ => legacy}/DEPRECATED__MappingTestState.ts (95%) create mode 100644 packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/legacy/MappingTestMigrationState.ts diff --git a/.changeset/seven-bears-trade.md b/.changeset/seven-bears-trade.md new file mode 100644 index 0000000000..8acf788149 --- /dev/null +++ b/.changeset/seven-bears-trade.md @@ -0,0 +1,5 @@ +--- +'@finos/legend-application-studio': patch +--- + +Add mapping migration tool with to migrate legacy mapping test to new framework. diff --git a/.changeset/thick-wasps-watch.md b/.changeset/thick-wasps-watch.md new file mode 100644 index 0000000000..d978860cac --- /dev/null +++ b/.changeset/thick-wasps-watch.md @@ -0,0 +1,4 @@ +--- +'@finos/legend-application-studio': patch +'@finos/legend-graph': patch +--- diff --git a/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingEditor.tsx b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingEditor.tsx index dceaaded2b..b51f0ebfaf 100644 --- a/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingEditor.tsx +++ b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingEditor.tsx @@ -51,8 +51,8 @@ import { } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.js'; import { MappingElementState } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingElementState.js'; import { MappingExplorer } from './MappingExplorer.js'; -import { DEPRECATED__MappingTestEditor } from './DEPRECATED__MappingTestEditor.js'; -import { DEPRECATED__MappingTestState } from '../../../../stores/editor/editor-state/element-editor-state/mapping/DEPRECATED__MappingTestState.js'; +import { DEPRECATED__MappingTestEditor } from './legacy/DEPRECATED__MappingTestEditor.js'; +import { DEPRECATED__MappingTestState } from '../../../../stores/editor/editor-state/element-editor-state/mapping/legacy/DEPRECATED__MappingTestState.js'; import { MappingTestsExplorer } from './MappingTestsExplorer.js'; import { LEGEND_STUDIO_TEST_ID } from '../../../../__lib__/LegendStudioTesting.js'; import { MappingExecutionState } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingExecutionState.js'; @@ -75,6 +75,7 @@ import type { MappingEditorTabState } from '../../../../stores/editor/editor-sta import { MappingTestableEditor } from './MappingTestableEditor.js'; import { DocumentationLink } from '@finos/legend-lego/application'; import { LEGEND_STUDIO_DOCUMENTATION_KEY } from '../../../../__lib__/LegendStudioDocumentation.js'; +import { MappingTestMigrationTool } from './legacy/MappingTestMigrationTool.js'; export const MappingEditorSplashScreen: React.FC = () => { const logoWidth = 280; @@ -400,6 +401,12 @@ export const MappingEditor = observer(() => { mappingTestableState={mappingEditorState.mappingTestableState} /> )} + {mappingEditorState.migrationState && ( + + )} diff --git a/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingTestsExplorer.tsx b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingTestsExplorer.tsx index a7f2c47e73..ec29983dda 100644 --- a/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingTestsExplorer.tsx +++ b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingTestsExplorer.tsx @@ -21,7 +21,7 @@ import { type DEPRECATED__MappingTestState, MAPPING_TEST_EDITOR_TAB_TYPE, TEST_RESULT, -} from '../../../../stores/editor/editor-state/element-editor-state/mapping/DEPRECATED__MappingTestState.js'; +} from '../../../../stores/editor/editor-state/element-editor-state/mapping/legacy/DEPRECATED__MappingTestState.js'; import { clsx, ContextMenu, @@ -36,10 +36,10 @@ import { ExclamationCircleIcon, PauseCircleIcon, PanelDropZone, - BlankPanelPlaceholder, MenuContent, MenuContentItem, Panel, + WarningIcon, } from '@finos/legend-art'; import { type MappingElementDragSource, @@ -47,17 +47,11 @@ import { } from '../../../../stores/editor/utils/DnDUtils.js'; import { ClassMappingSelectorModal } from './MappingExecutionBuilder.js'; import { flowResult } from 'mobx'; -import { Randomizer } from '@finos/legend-shared'; import { useEditorStore } from '../../EditorStoreProvider.js'; import { useApplicationStore } from '@finos/legend-application'; import { SetImplementation } from '@finos/legend-graph'; import { MappingEditorState } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.js'; -const addTestPromps = [ - `Let's add some tests!`, - `"A test a day keeps the QA away"`, -]; - export const MappingTestExplorerContextMenu = observer( forwardRef< HTMLDivElement, @@ -370,6 +364,9 @@ export const MappingTestsExplorer = observer( // Class mapping selector const [openClassMappingSelectorModal, setOpenClassMappingSelectorModal] = useState(false); + + const openMigrationtool = (): void => + mappingEditorState.openMigrationTool(); const showClassMappingSelectorModal = (): void => setOpenClassMappingSelectorModal(true); const hideClassMappingSelectorModal = (): void => @@ -416,6 +413,17 @@ export const MappingTestsExplorer = observer(
+ {Boolean(mappingEditorState.mapping.test.length) && ( + + )}
diff --git a/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/DEPRECATED__MappingTestEditor.tsx b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/legacy/DEPRECATED__MappingTestEditor.tsx similarity index 97% rename from packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/DEPRECATED__MappingTestEditor.tsx rename to packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/legacy/DEPRECATED__MappingTestEditor.tsx index f355cd7478..62fe845a2d 100644 --- a/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/DEPRECATED__MappingTestEditor.tsx +++ b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/legacy/DEPRECATED__MappingTestEditor.tsx @@ -24,7 +24,7 @@ import { MappingTestFlatDataInputDataState, MappingTestExpectedOutputAssertionState, MappingTestRelationalInputDataState, -} from '../../../../stores/editor/editor-state/element-editor-state/mapping/DEPRECATED__MappingTestState.js'; +} from '../../../../../stores/editor/editor-state/element-editor-state/mapping/legacy/DEPRECATED__MappingTestState.js'; import { clsx, PanelLoadingIndicator, @@ -50,7 +50,7 @@ import { useDrop } from 'react-dnd'; import { type MappingElementDragSource, CORE_DND_TYPE, -} from '../../../../stores/editor/utils/DnDUtils.js'; +} from '../../../../../stores/editor/utils/DnDUtils.js'; import { IllegalStateError, guaranteeType, @@ -65,14 +65,14 @@ import { import { ClassMappingSelectorModal, getRelationalInputTestDataEditorLanguage, -} from './MappingExecutionBuilder.js'; +} from '../MappingExecutionBuilder.js'; import { flowResult } from 'mobx'; -import { MappingTestStatusIndicator } from './MappingTestsExplorer.js'; +import { MappingTestStatusIndicator } from '../MappingTestsExplorer.js'; import { getMappingElementSource, getMappingElementTarget, -} from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.js'; -import { useEditorStore } from '../../EditorStoreProvider.js'; +} from '../../../../../stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.js'; +import { useEditorStore } from '../../../EditorStoreProvider.js'; import { Class, SetImplementation, @@ -82,17 +82,17 @@ import { isStubbed_RawLambda, DEPRECATED__validate_MappingTestAssert, } from '@finos/legend-graph'; -import { flatData_setData } from '../../../../stores/graph-modifier/STO_FlatData_GraphModifierHelper.js'; +import { flatData_setData } from '../../../../../stores/graph-modifier/STO_FlatData_GraphModifierHelper.js'; import { relationalInputData_setData, relationalInputData_setInputType, -} from '../../../../stores/graph-modifier/STO_Relational_GraphModifierHelper.js'; +} from '../../../../../stores/graph-modifier/STO_Relational_GraphModifierHelper.js'; import { type QueryBuilderState, QueryBuilderTextEditorMode, ExecutionPlanViewer, } from '@finos/legend-query-builder'; -import { MappingExecutionQueryBuilderState } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingExecutionQueryBuilderState.js'; +import { MappingExecutionQueryBuilderState } from '../../../../../stores/editor/editor-state/element-editor-state/mapping/MappingExecutionQueryBuilderState.js'; import { CODE_EDITOR_LANGUAGE, CodeEditor, diff --git a/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/legacy/MappingTestMigrationTool.tsx b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/legacy/MappingTestMigrationTool.tsx new file mode 100644 index 0000000000..4a533c57a5 --- /dev/null +++ b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/legacy/MappingTestMigrationTool.tsx @@ -0,0 +1,156 @@ +/** + * 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 { + BaseStepper, + BlankPanelContent, + Dialog, + Modal, + ModalBody, + ModalFooter, + ModalFooterButton, + ModalHeader, + PanelFormListItems, + PanelLoadingIndicator, + clsx, +} from '@finos/legend-art'; +import { observer } from 'mobx-react-lite'; +import type { MappingEditorState } from '../../../../../stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.js'; +import { + MIGRATE_PHASE, + type MappingTestMigrationState, +} from '../../../../../stores/editor/editor-state/element-editor-state/mapping/legacy/MappingTestMigrationState.js'; +import { + CODE_EDITOR_LANGUAGE, + CodeDiffView, +} from '@finos/legend-lego/code-editor'; + +export const MappingTestMigrationTool = observer( + (props: { + mappingEditorState: MappingEditorState; + migrationState: MappingTestMigrationState; + }) => { + const { mappingEditorState, migrationState } = props; + const isLoading = + migrationState.confirmationState?.calculatingDiffs.isInProgress; + const close = (): void => { + mappingEditorState.closeMigrationTool(); + }; + const handleBack = (): void => { + migrationState.handleBack(); + }; + const handleNext = (): void => { + migrationState.handleNext(); + }; + const disabled = + !migrationState.migrateableTests.length && + !migrationState.unSupportedTestsToMigrate.length; + return ( + + + + + + + {!disabled ? ( +
+ {migrationState.currentStep === MIGRATE_PHASE.OVERVIEW && ( + <> + + {migrationState.migrateableTests.map((test) => ( +
+
+ {test.name} +
+
+ ))} +
+ + {migrationState.unSupportedTestsToMigrate.map((test) => ( +
+
+ {test.name} +
+
+ ))} +
+ + )} + {migrationState.currentStep === MIGRATE_PHASE.CONFIRM && + migrationState.confirmationState && ( +
+ +
+ )} +
+ ) : ( +
+ No Migrateable Tests +
+ )} +
+ + + Back + + + {migrationState.nextText} + + +
+
+ ); + }, +); diff --git a/packages/legend-application-studio/src/index.ts b/packages/legend-application-studio/src/index.ts index b582ae9b9e..4d11acf36b 100644 --- a/packages/legend-application-studio/src/index.ts +++ b/packages/legend-application-studio/src/index.ts @@ -74,7 +74,7 @@ export * from './stores/editor/sidebar-state/testable/GlobalTestRunnerState.js'; export { PostProcessorEditorState } from './stores/editor/editor-state/element-editor-state/connection/PostProcessorEditorState.js'; export { MappingExecutionState } from './stores/editor/editor-state/element-editor-state/mapping/MappingExecutionState.js'; -export { DEPRECATED__MappingTestState } from './stores/editor/editor-state/element-editor-state/mapping/DEPRECATED__MappingTestState.js'; +export { DEPRECATED__MappingTestState } from './stores/editor/editor-state/element-editor-state/mapping/legacy/DEPRECATED__MappingTestState.js'; export { ConnectionValueState, RelationalDatabaseConnectionValueState, diff --git a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.ts b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.ts index 2124e25449..c9ca526592 100644 --- a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.ts +++ b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.ts @@ -33,7 +33,7 @@ import { MAPPING_TEST_EDITOR_TAB_TYPE, DEPRECATED__MappingTestState, TEST_RESULT, -} from './DEPRECATED__MappingTestState.js'; +} from './legacy/DEPRECATED__MappingTestState.js'; import { createMockDataForMappingElementSource } from '../../../utils/MockDataUtils.js'; import { type GeneratorFn, @@ -59,7 +59,7 @@ import { RootRelationalInstanceSetImplementationState } from './relational/Relat import { type CompilationError, type PackageableElement, - type InputData, + type DEPRECATED__InputData, Type, type EmbeddedSetImplementation, type ExecutionResult, @@ -74,8 +74,8 @@ import { EnumerationMapping, SetImplementation, PureInstanceSetImplementation, - ExpectedOutputMappingTestAssert, - ObjectInputData, + DEPRECATED__ExpectedOutputMappingTestAssert, + DEPRECATED__ObjectInputData, ObjectInputType, FlatDataInstanceSetImplementation, InstanceSetImplementation, @@ -125,6 +125,7 @@ import { rootRelationalSetImp_setMainTableAlias } from '../../../../graph-modifi import { LambdaEditorState } from '@finos/legend-query-builder'; import type { MappingEditorTabState } from './MappingTabManagerState.js'; import { MappingTestableState } from './testable/MappingTestableState.js'; +import { MappingTestMigrationState } from './legacy/MappingTestMigrationState.js'; export interface MappingExplorerTreeNodeData extends TreeNodeData { mappingElement: MappingElement; @@ -626,6 +627,7 @@ export class MappingEditorState extends ElementEditorState { // DEPREACTED legacy tests: TO REMOVE once mapping testable dev work is complete DEPRECATED_mappingTestStates: DEPRECATED__MappingTestState[] = []; + migrationState: MappingTestMigrationState | undefined; isRunningAllTests = false; allTestRunTime = 0; @@ -640,11 +642,15 @@ export class MappingEditorState extends ElementEditorState { isRunningAllTests: observable, allTestRunTime: observable, selectedTab: observable, + migrationState: observable, mappingExplorerTreeData: observable.ref, mapping: computed, testSuiteResult: computed, setNewMappingElementSpec: action, + openMigrationTool: action, + closeMigrationTool: action, setMappingExplorerTreeNodeData: action, + buildLegacyTestsStates: action, openMappingElement: action, closeAllTabs: action, createMappingElement: action, @@ -666,9 +672,7 @@ export class MappingEditorState extends ElementEditorState { deleteMappingElement: flow, }); - this.DEPRECATED_mappingTestStates = this.mapping.test.map( - (t) => new DEPRECATED__MappingTestState(editorStore, t, this), - ); + this.DEPRECATED_mappingTestStates = this.buildLegacyTestsStates(); this.mappingExplorerTreeData = getMappingElementTreeData( this.mapping, editorStore, @@ -684,6 +688,12 @@ export class MappingEditorState extends ElementEditorState { ); } + buildLegacyTestsStates(): DEPRECATED__MappingTestState[] { + return this.mapping.test.map( + (t) => new DEPRECATED__MappingTestState(this.editorStore, t, this), + ); + } + /** * This method is used to check if a target is being mapped multiple times, so we can make * decision on things like whether we enforce the user to provide an ID for those mapping elements. @@ -1393,6 +1403,23 @@ export class MappingEditorState extends ElementEditorState { // -------------------------------------- Test --------------------------------------- + openMigrationTool(): void { + if (!this.mapping.test.length) { + this.editorStore.applicationStore.notificationService.notifyError( + 'No legacy tests to migrate', + ); + return; + } + this.migrationState = MappingTestMigrationState.build( + this.editorStore, + this, + ); + } + + closeMigrationTool(): void { + this.migrationState = undefined; + } + *openTest( test: DEPRECATED__MappingTest, openTab?: MAPPING_TEST_EDITOR_TAB_TYPE, @@ -1546,9 +1573,9 @@ export class MappingEditorState extends ElementEditorState { `Can't auto-generate input data for operation class mapping. Please pick a concrete class mapping instead`, ); } - let inputData: InputData; + let inputData: DEPRECATED__InputData; if (source === undefined || source instanceof Class) { - inputData = new ObjectInputData( + inputData = new DEPRECATED__ObjectInputData( PackageableElementExplicitReference.create(source ?? stub_Class()), ObjectInputType.JSON, source @@ -1578,7 +1605,7 @@ export class MappingEditorState extends ElementEditorState { generateMappingTestName(this.mapping), query, [inputData], - new ExpectedOutputMappingTestAssert('{}'), + new DEPRECATED__ExpectedOutputMappingTestAssert('{}'), ); mapping_addDEPRECATEDTest( this.mapping, diff --git a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/MappingExecutionState.ts b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/MappingExecutionState.ts index 59436f5887..d4a99b168c 100644 --- a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/MappingExecutionState.ts +++ b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/MappingExecutionState.ts @@ -51,7 +51,7 @@ import { } from '@finos/legend-shared'; import { createMockDataForMappingElementSource } from '../../../utils/MockDataUtils.js'; import { - type InputData, + type DEPRECATED__InputData, type Mapping, type Connection, type ExecutionResult, @@ -69,9 +69,9 @@ import { LAMBDA_PIPE, GRAPH_MANAGER_EVENT, Class, - ObjectInputData, + DEPRECATED__ObjectInputData, ObjectInputType, - ExpectedOutputMappingTestAssert, + DEPRECATED__ExpectedOutputMappingTestAssert, IdentifiedConnection, EngineRuntime, JsonModelConnection, @@ -209,12 +209,12 @@ abstract class MappingExecutionInputDataState { readonly uuid = uuid(); editorStore: EditorStore; mapping: Mapping; - inputData?: InputData | undefined; + inputData?: DEPRECATED__InputData | undefined; constructor( editorStore: EditorStore, mapping: Mapping, - inputData: InputData | undefined, + inputData: DEPRECATED__InputData | undefined, ) { this.editorStore = editorStore; this.mapping = mapping; @@ -232,7 +232,7 @@ abstract class MappingExecutionInputDataState { return undefined; } - abstract buildInputDataForTest(): InputData; + abstract buildInputDataForTest(): DEPRECATED__InputData; } export const createRuntimeForExecution = ( @@ -267,7 +267,7 @@ export class MappingExecutionEmptyInputDataState extends MappingExecutionInputDa ); } - buildInputDataForTest(): InputData { + buildInputDataForTest(): DEPRECATED__InputData { throw new IllegalStateError( 'Mapping execution runtime information is not specified', ); @@ -276,13 +276,13 @@ export class MappingExecutionEmptyInputDataState extends MappingExecutionInputDa // TODO?: handle XML export class MappingExecutionObjectInputDataState extends MappingExecutionInputDataState { - declare inputData: ObjectInputData; + declare inputData: DEPRECATED__ObjectInputData; constructor(editorStore: EditorStore, mapping: Mapping, _class: Class) { super( editorStore, mapping, - new ObjectInputData( + new DEPRECATED__ObjectInputData( PackageableElementExplicitReference.create( guaranteeNonNullable(_class), ), @@ -344,8 +344,8 @@ export class MappingExecutionObjectInputDataState extends MappingExecutionInputD return jsonAssertion; } - buildInputDataForTest(): InputData { - return new ObjectInputData( + buildInputDataForTest(): DEPRECATED__InputData { + return new DEPRECATED__ObjectInputData( PackageableElementExplicitReference.create( guaranteeNonNullable(this.inputData.sourceClass.value), ), @@ -402,7 +402,7 @@ export class MappingExecutionFlatDataInputDataState extends MappingExecutionInpu ); } - buildInputDataForTest(): InputData { + buildInputDataForTest(): DEPRECATED__InputData { return new FlatDataInputData( PackageableElementExplicitReference.create( guaranteeNonNullable(this.inputData.sourceFlatData.value), @@ -474,7 +474,7 @@ export class MappingExecutionRelationalInputDataState extends MappingExecutionIn ); } - buildInputDataForTest(): InputData { + buildInputDataForTest(): DEPRECATED__InputData { return new RelationalInputData( PackageableElementExplicitReference.create( guaranteeNonNullable(this.inputData.database.value), @@ -664,7 +664,7 @@ export class MappingExecutionState extends MappingEditorTabState { this.executionResultText ) { const inputData = this.inputDataState.buildInputDataForTest(); - const assert = new ExpectedOutputMappingTestAssert( + const assert = new DEPRECATED__ExpectedOutputMappingTestAssert( toGrammarString(this.executionResultText), ); const mappingTest = new DEPRECATED__MappingTest( diff --git a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/DEPRECATED__MappingTestState.ts b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/legacy/DEPRECATED__MappingTestState.ts similarity index 95% rename from packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/DEPRECATED__MappingTestState.ts rename to packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/legacy/DEPRECATED__MappingTestState.ts index 5eb7d90c49..8e78602497 100644 --- a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/DEPRECATED__MappingTestState.ts +++ b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/legacy/DEPRECATED__MappingTestState.ts @@ -17,7 +17,7 @@ import type { MappingEditorState, MappingElementSource, -} from './MappingEditorState.js'; +} from '../MappingEditorState.js'; import { type GeneratorFn, LogEvent, @@ -39,22 +39,22 @@ import { ContentType, StopWatch, } from '@finos/legend-shared'; -import type { EditorStore } from '../../../EditorStore.js'; +import type { EditorStore } from '../../../../EditorStore.js'; import { observable, flow, action, makeObservable, flowResult } from 'mobx'; -import { createMockDataForMappingElementSource } from '../../../utils/MockDataUtils.js'; +import { createMockDataForMappingElementSource } from '../../../../utils/MockDataUtils.js'; import { type RawLambda, type Runtime, - type InputData, - type MappingTestAssert, + type DEPRECATED__InputData, + type DEPRECATED__MappingTestAssert, type Mapping, type ExecutionResult, extractExecutionResultValues, GRAPH_MANAGER_EVENT, LAMBDA_PIPE, Class, - ExpectedOutputMappingTestAssert, - ObjectInputData, + DEPRECATED__ExpectedOutputMappingTestAssert, + DEPRECATED__ObjectInputData, ObjectInputType, IdentifiedConnection, EngineRuntime, @@ -81,7 +81,7 @@ import { reportGraphAnalytics, } from '@finos/legend-graph'; import { DEFAULT_TAB_SIZE } from '@finos/legend-application'; -import { flatData_setData } from '../../../../graph-modifier/STO_FlatData_GraphModifierHelper.js'; +import { flatData_setData } from '../../../../../graph-modifier/STO_FlatData_GraphModifierHelper.js'; import { expectedOutputMappingTestAssert_setExpectedOutput, mappingTest_setAssert, @@ -89,19 +89,19 @@ import { objectInputData_setData, runtime_addIdentifiedConnection, runtime_addMapping, -} from '../../../../graph-modifier/DSL_Mapping_GraphModifierHelper.js'; +} from '../../../../../graph-modifier/DSL_Mapping_GraphModifierHelper.js'; import { localH2DatasourceSpecification_setTestDataSetupCsv, localH2DatasourceSpecification_setTestDataSetupSqls, relationalInputData_setData, -} from '../../../../graph-modifier/STO_Relational_GraphModifierHelper.js'; +} from '../../../../../graph-modifier/STO_Relational_GraphModifierHelper.js'; import { LambdaEditorState, QueryBuilderTelemetryHelper, QUERY_BUILDER_EVENT, ExecutionPlanState, } from '@finos/legend-query-builder'; -import { MappingEditorTabState } from './MappingTabManagerState.js'; +import { MappingEditorTabState } from '../MappingTabManagerState.js'; export enum TEST_RESULT { NONE = 'NONE', // test has not run yet @@ -194,12 +194,12 @@ abstract class MappingTestInputDataState { readonly uuid = uuid(); editorStore: EditorStore; mapping: Mapping; - inputData: InputData; + inputData: DEPRECATED__InputData; constructor( editorStore: EditorStore, mapping: Mapping, - inputData: InputData, + inputData: DEPRECATED__InputData, ) { this.editorStore = editorStore; this.mapping = mapping; @@ -210,7 +210,7 @@ abstract class MappingTestInputDataState { } export class MappingTestObjectInputDataState extends MappingTestInputDataState { - declare inputData: ObjectInputData; + declare inputData: DEPRECATED__ObjectInputData; /** * @workaround https://github.com/finos/legend-studio/issues/68 */ @@ -219,7 +219,7 @@ export class MappingTestObjectInputDataState extends MappingTestInputDataState { constructor( editorStore: EditorStore, mapping: Mapping, - inputData: ObjectInputData, + inputData: DEPRECATED__ObjectInputData, ) { super(editorStore, mapping, inputData); @@ -353,21 +353,21 @@ export class MappingTestRelationalInputDataState extends MappingTestInputDataSta abstract class MappingTestAssertionState { readonly uuid = uuid(); - assert: MappingTestAssert; + assert: DEPRECATED__MappingTestAssert; - constructor(assert: MappingTestAssert) { + constructor(assert: DEPRECATED__MappingTestAssert) { this.assert = assert; } } export class MappingTestExpectedOutputAssertionState extends MappingTestAssertionState { - declare assert: ExpectedOutputMappingTestAssert; + declare assert: DEPRECATED__ExpectedOutputMappingTestAssert; /** * @workaround https://github.com/finos/legend-studio/issues/68 */ expectedResult: string; - constructor(assert: ExpectedOutputMappingTestAssert) { + constructor(assert: DEPRECATED__ExpectedOutputMappingTestAssert) { super(assert); makeObservable(this, { @@ -399,6 +399,7 @@ export enum MAPPING_TEST_EDITOR_TAB_TYPE { SETUP = 'Test Setup', RESULT = 'Test Result', } + /** * TODO: Remove once migration from `MappingTest_Legacy` to `MappingTest` is complete * @deprecated @@ -513,7 +514,7 @@ export class DEPRECATED__MappingTestState extends MappingEditorTabState { 'Mapping test input data must contain at least one item', ); const inputData = this.test.inputData[0]; - if (inputData instanceof ObjectInputData) { + if (inputData instanceof DEPRECATED__ObjectInputData) { return new MappingTestObjectInputDataState( this.editorStore, this.mappingEditorState.mapping, @@ -540,7 +541,7 @@ export class DEPRECATED__MappingTestState extends MappingEditorTabState { buildAssertionState(): MappingTestAssertionState { const testAssertion = this.test.assert; - if (testAssertion instanceof ExpectedOutputMappingTestAssert) { + if (testAssertion instanceof DEPRECATED__ExpectedOutputMappingTestAssert) { return new MappingTestExpectedOutputAssertionState(testAssertion); } throw new UnsupportedOperationError( @@ -584,7 +585,7 @@ export class DEPRECATED__MappingTestState extends MappingEditorTabState { const newInputDataState = new MappingTestObjectInputDataState( this.editorStore, this.mappingEditorState.mapping, - new ObjectInputData( + new DEPRECATED__ObjectInputData( PackageableElementExplicitReference.create(source ?? stub_Class()), ObjectInputType.JSON, tryToMinifyJSONString('{}'), diff --git a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/legacy/MappingTestMigrationState.ts b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/legacy/MappingTestMigrationState.ts new file mode 100644 index 0000000000..b03c0feae6 --- /dev/null +++ b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/mapping/legacy/MappingTestMigrationState.ts @@ -0,0 +1,383 @@ +/** + * 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 DEPRECATED__MappingTest, + type DEPRECATED__InputData, + type Mapping, + FlatDataInputData, + MappingTestSuite, + MappingTest, + EqualToJson, + StoreTestData, + DEPRECATED__ObjectInputData, + PackageableElementExplicitReference, + ModelStore, + ModelStoreData, + ModelEmbeddedData, + ExternalFormatData, + ObjectInputType, + DEPRECATED__ExpectedOutputMappingTestAssert, + RelationalInputData, +} from '@finos/legend-graph'; +import type { EditorStore } from '../../../../EditorStore.js'; +import type { MappingEditorState } from '../MappingEditorState.js'; +import { + ContentType, + guaranteeNonNullable, + uniq, + type GeneratorFn, + guaranteeType, + ActionState, + assertErrorThrown, +} from '@finos/legend-shared'; +import { action, computed, flow, makeObservable, observable } from 'mobx'; +import { + mapping_addDEPRECATEDTest, + mapping_addTestSuite, + mapping_deleteTest, + mapping_deleteTestSuite, +} from '../../../../../graph-modifier/DSL_Mapping_GraphModifierHelper.js'; + +enum UNSUPPORTED_REASON { + FLAT_DATA = 'FLAT_DATA', + MULTI_INPUT_DATA = 'MULTI_INPUT_DATA', + RELATIONAL = 'RELATIONAL', +} + +class SupportedMigrationTestState { + test: DEPRECATED__MappingTest; + inputData: DEPRECATED__InputData; + + constructor(test: DEPRECATED__MappingTest, inputData: DEPRECATED__InputData) { + this.test = test; + this.inputData = inputData; + } +} +export enum MIGRATE_PHASE { + OVERVIEW = 'OVERVIEW', + CONFIRM = 'CONFIRM', +} +export class MappingTestConfirmationState { + editorStore: EditorStore; + migrationState: MappingTestMigrationState; + before: string | undefined; + after: string | undefined; + // diffs + suitesToAdd: MappingTestSuite[]; + testsToDelete: DEPRECATED__MappingTest[]; + migrated = false; + calculatingDiffs = ActionState.create(); + + constructor(migrationState: MappingTestMigrationState) { + makeObservable(this, { + init: flow, + migrate: action, + reverse: action, + before: observable, + after: observable, + suitesToAdd: observable, + testsToDelete: observable, + }); + this.migrationState = migrationState; + this.editorStore = migrationState.editorStore; + this.suitesToAdd = migrationState.migrate(); + this.testsToDelete = this.migrationState.migrateableTests; + this.init(); + } + + *init(): GeneratorFn { + try { + this.calculatingDiffs.inProgress(); + // before + const beforeEntity = + this.migrationState.editorStore.graphManagerState.graphManager.elementToEntity( + this.migrationState.mapping, + ); + this.migrate(); + // after + const after = + this.migrationState.editorStore.graphManagerState.graphManager.elementToEntity( + this.migrationState.mapping, + ); + const beforeP = + this.editorStore.graphManagerState.graphManager.entitiesToPureCode([ + beforeEntity, + ]); + const afterP = + this.editorStore.graphManagerState.graphManager.entitiesToPureCode([ + after, + ]); + const [beforeGrammar, afterGrammar] = (yield Promise.all([ + beforeP, + afterP, + ])) as [string, string]; + + // done + this.before = beforeGrammar; + this.after = afterGrammar; + this.reverse(); + } catch (error) { + assertErrorThrown(error); + this.editorStore.applicationStore.notificationService.notifyError( + `Error calcuating mapping diffs: ${error.message}`, + ); + } finally { + this.calculatingDiffs.complete(); + } + } + + migrate(): void { + // add + this.suitesToAdd.forEach((suite) => + mapping_addTestSuite( + this.migrationState.mapping, + suite, + this.editorStore.changeDetectionState.observerContext, + ), + ); + // delete + this.testsToDelete.forEach((test) => + mapping_deleteTest(this.migrationState.mapping, test), + ); + this.migrated = true; + } + + reverse(): void { + // add + this.suitesToAdd.forEach((suite) => + mapping_deleteTestSuite(this.migrationState.mapping, suite), + ); + // delete + this.testsToDelete.forEach((test) => + mapping_addDEPRECATEDTest( + this.migrationState.mapping, + test, + this.editorStore.changeDetectionState.observerContext, + ), + ); + this.migrated = false; + } +} + +export class MappingTestMigrationState { + readonly editorStore: EditorStore; + readonly mappingEditorState: MappingEditorState; + unsupported: Map; + queryToSuiteMap: Map; + suitesAdded: MappingTestSuite[] | undefined; + + // phases + steps: [MIGRATE_PHASE, MIGRATE_PHASE] = [ + MIGRATE_PHASE.OVERVIEW, + MIGRATE_PHASE.CONFIRM, + ]; + currentStep: MIGRATE_PHASE = MIGRATE_PHASE.OVERVIEW; + confirmationState: MappingTestConfirmationState | undefined; + + constructor( + editorStore: EditorStore, + mappingEditorState: MappingEditorState, + unsupported: Map, + supported: Map, + ) { + makeObservable(this, { + suitesAdded: observable, + currentStep: observable, + confirmationState: observable, + handleNext: action, + activeStep: computed, + disableBack: computed, + disableNext: computed, + handleBack: action, + }); + this.editorStore = editorStore; + this.mappingEditorState = mappingEditorState; + this.unsupported = unsupported; + this.queryToSuiteMap = supported; + } + + get activeStep(): number { + return this.steps.findIndex((e) => e === this.currentStep); + } + + get disableBack(): boolean { + if (this.currentStep === MIGRATE_PHASE.OVERVIEW) { + return true; + } + return false; + } + + get disableNext(): boolean { + return !this.migrateableTests.length; + } + + get nextText(): string { + if (this.currentStep === MIGRATE_PHASE.CONFIRM) { + return 'Confirm'; + } + + return 'Next'; + } + + handleBack(): void { + if (this.currentStep === MIGRATE_PHASE.CONFIRM) { + this.confirmationState?.reverse(); + this.confirmationState = undefined; + this.currentStep = MIGRATE_PHASE.OVERVIEW; + } + } + + handleNext(): void { + if (this.currentStep === MIGRATE_PHASE.OVERVIEW) { + this.confirmationState = new MappingTestConfirmationState(this); + this.currentStep = MIGRATE_PHASE.CONFIRM; + } else { + this.confirmationState?.migrate(); + this.mappingEditorState.closeMigrationTool(); + this.mappingEditorState.DEPRECATED_mappingTestStates = + this.mappingEditorState.buildLegacyTestsStates(); + } + } + + static build( + editorStore: EditorStore, + mappingEditorState: MappingEditorState, + ): MappingTestMigrationState { + const unsupportedTests = new Map< + UNSUPPORTED_REASON, + DEPRECATED__MappingTest[] + >(); + const supported = new Map(); + mappingEditorState.mapping.test.forEach((test) => { + let testUnsupportedReason: UNSUPPORTED_REASON | undefined = undefined; + const nullableInputData = test.inputData[0]; + if (test.inputData.length !== 1 || !nullableInputData) { + testUnsupportedReason = UNSUPPORTED_REASON.MULTI_INPUT_DATA; + } else if (nullableInputData instanceof FlatDataInputData) { + testUnsupportedReason = UNSUPPORTED_REASON.FLAT_DATA; + } else if (nullableInputData instanceof RelationalInputData) { + testUnsupportedReason = UNSUPPORTED_REASON.FLAT_DATA; + } + if (testUnsupportedReason) { + const unsupportedTest = + unsupportedTests.get(testUnsupportedReason) ?? []; + unsupportedTest.push(test); + unsupportedTests.set(testUnsupportedReason, unsupportedTest); + return; + } + const inputData = guaranteeNonNullable(nullableInputData); + const testQuery = test.query; + // we will use hash code to see if the same query exists + const suites = supported.get(testQuery.hashCode) ?? []; + suites.push(new SupportedMigrationTestState(test, inputData)); + supported.set(testQuery.hashCode, suites); + }); + + return new MappingTestMigrationState( + editorStore, + mappingEditorState, + unsupportedTests, + supported, + ); + } + + migrate(): MappingTestSuite[] { + const suites: MappingTestSuite[] = []; + this.suiteValuesToMigrate.forEach((testsWithQuery) => { + if (!testsWithQuery.length) { + return; + } + const test = guaranteeNonNullable(testsWithQuery[0]); + const query = test.test.query; + const suite = new MappingTestSuite(); + suite.func = query; + testsWithQuery.forEach((legacyTestState) => { + const legacyTest = legacyTestState.test; + const legacyAssertion = guaranteeType( + legacyTest.assert, + DEPRECATED__ExpectedOutputMappingTestAssert, + ); + const mappingTest = new MappingTest(); + const assertion = new EqualToJson(); + mappingTest.id = legacyTest.name; + assertion.id = legacyTest.name; + mappingTest.assertions = [assertion]; + assertion.parentTest = mappingTest; + assertion.expected = new ExternalFormatData(); + assertion.expected.contentType = ContentType.APPLICATION_JSON; + assertion.expected.data = legacyAssertion.expectedOutput; + const inputData = legacyTestState.inputData; + const storeTestData = new StoreTestData(); + if (inputData instanceof DEPRECATED__ObjectInputData) { + storeTestData.store = PackageableElementExplicitReference.create( + ModelStore.INSTANCE, + ); + const modelStoreData = new ModelStoreData(); + const modelEmbeddedData = new ModelEmbeddedData(); + modelEmbeddedData.model = PackageableElementExplicitReference.create( + inputData.sourceClass.value, + ); + const externalFormatData = new ExternalFormatData(); + if (inputData.inputType === ObjectInputType.XML) { + externalFormatData.contentType = ContentType.APPLICATION_XML; + } else { + externalFormatData.contentType = ContentType.APPLICATION_JSON; + } + externalFormatData.data = inputData.data; + modelEmbeddedData.data = externalFormatData; + modelStoreData.modelData = [modelEmbeddedData]; + storeTestData.data = modelStoreData; + } else if (inputData instanceof RelationalInputData) { + storeTestData.store = PackageableElementExplicitReference.create( + inputData.database.value, + ); + } + mappingTest.storeTestData = [storeTestData]; + mappingTest.__parent = suite; + suite.tests.push(mappingTest); + }); + // calculate ID + let id = 'suite'; + if (suite.tests.length === 1) { + id = `${guaranteeNonNullable(suite.tests[0]).id}_${id}`; + } else { + id = `suite_${suites.length}`; + } + suite.id = id; + suites.push(suite); + }); + return suites; + } + + get suiteValuesToMigrate(): SupportedMigrationTestState[][] { + return Array.from(this.queryToSuiteMap.values()); + } + + get unSupportedTestsToMigrate(): DEPRECATED__MappingTest[] { + return uniq(Array.from(this.unsupported.values()).flat()); + } + + get mapping(): Mapping { + return this.mappingEditorState.mapping; + } + + get migrateableTests(): DEPRECATED__MappingTest[] { + return this.mapping.test.filter( + (t) => !this.unSupportedTestsToMigrate.includes(t), + ); + } +} diff --git a/packages/legend-application-studio/src/stores/graph-modifier/DSL_Mapping_GraphModifierHelper.ts b/packages/legend-application-studio/src/stores/graph-modifier/DSL_Mapping_GraphModifierHelper.ts index ff24d9496d..92943f3b68 100644 --- a/packages/legend-application-studio/src/stores/graph-modifier/DSL_Mapping_GraphModifierHelper.ts +++ b/packages/legend-application-studio/src/stores/graph-modifier/DSL_Mapping_GraphModifierHelper.ts @@ -18,15 +18,15 @@ import { type Enum, type EnumerationMapping, type EnumValueMapping, - type InputData, + type DEPRECATED__InputData, type InstanceSetImplementation, type Mapping, type DEPRECATED__MappingTest, - type MappingTestAssert, + type DEPRECATED__MappingTestAssert, type PropertyMapping, type RawLambda, type SetImplementation, - type ExpectedOutputMappingTestAssert, + type DEPRECATED__ExpectedOutputMappingTestAssert, type OperationSetImplementation, type OperationType, type SetImplementationContainer, @@ -44,7 +44,7 @@ import { type Class, type PureInstanceSetImplementation, type PurePropertyMapping, - type ObjectInputData, + type DEPRECATED__ObjectInputData, type ObserverContext, type Type, PackageableElementExplicitReference, @@ -331,7 +331,7 @@ export const mappingTest_setName = action( export const mappingTest_setInputData = action( ( test: DEPRECATED__MappingTest, - value: InputData[], + value: DEPRECATED__InputData[], observeContext: ObserverContext, ): void => { test.inputData = value.map((i) => observe_InputData(i, observeContext)); @@ -347,14 +347,14 @@ export const DEPRECATED_mappingTest_setQuery = action( export const mappingTest_setAssert = action( ( test: DEPRECATED__MappingTest, - value: MappingTestAssert, + value: DEPRECATED__MappingTestAssert, observerContext: ObserverContext, ): void => { test.assert = observe_MappingTestAssert(value, observerContext); }, ); export const expectedOutputMappingTestAssert_setExpectedOutput = action( - (e: ExpectedOutputMappingTestAssert, val: string): void => { + (e: DEPRECATED__ExpectedOutputMappingTestAssert, val: string): void => { e.expectedOutput = val; }, ); @@ -471,7 +471,7 @@ export const setImpl_nominateRoot = action( // --------------------------------------------- M2M ------------------------------------- export const objectInputData_setData = action( - (o: ObjectInputData, val: string): void => { + (o: DEPRECATED__ObjectInputData, val: string): void => { o.data = val; }, ); diff --git a/packages/legend-application-studio/style/components/editor/_mapping-editor.scss b/packages/legend-application-studio/style/components/editor/_mapping-editor.scss index 6fdc7ff37b..f4bca9d9e9 100644 --- a/packages/legend-application-studio/style/components/editor/_mapping-editor.scss +++ b/packages/legend-application-studio/style/components/editor/_mapping-editor.scss @@ -348,3 +348,8 @@ } } } + +.mapping-migration-tool { + padding: 1rem; + height: calc(100% - 4rem); +} diff --git a/packages/legend-graph/src/index.ts b/packages/legend-graph/src/index.ts index 6eb33755e8..3af2e95431 100644 --- a/packages/legend-graph/src/index.ts +++ b/packages/legend-graph/src/index.ts @@ -502,10 +502,10 @@ export { SetImplementationExplicitReference } from './graph/metamodel/pure/packa export * from './graph/metamodel/pure/packageableElements/mapping/EnumerationMappingReference.js'; export { DEPRECATED__MappingTest, - DEPRECATED__MappingTestAssert as MappingTestAssert, - DEPRECATED__ExpectedOutputMappingTestAssert as ExpectedOutputMappingTestAssert, - DEPRECATED__InputData as InputData, - DEPRECATED__ObjectInputData as ObjectInputData, + DEPRECATED__MappingTestAssert, + DEPRECATED__ExpectedOutputMappingTestAssert, + DEPRECATED__InputData, + DEPRECATED__ObjectInputData, ObjectInputType, } from './graph/metamodel/pure/packageableElements/mapping/DEPRECATED__MappingTest.js'; export * from './graph/metamodel/pure/packageableElements/mapping/MappingTest.js';