UNPKG

@finos/legend-studio

Version:
858 lines 52.1 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 { observable, action, computed, flow, makeObservable, flowResult, } from 'mobx'; import { InstanceSetImplementationState, MappingElementState, } from './MappingElementState.js'; import { PureInstanceSetImplementationState } from './PureInstanceSetImplementationState.js'; import { ElementEditorState } from '../../../editor-state/element-editor-state/ElementEditorState.js'; import { MAPPING_TEST_EDITOR_TAB_TYPE, MappingTestState, TEST_RESULT, } from './MappingTestState.js'; import { createMockDataForMappingElementSource } from '../../../shared/MockDataUtil.js'; import { assertErrorThrown, LogEvent, deleteEntry, generateEnumerableNameFromToken, IllegalStateError, isNonNullable, assertNonNullable, guaranteeNonNullable, guaranteeType, UnsupportedOperationError, assertTrue, addUniqueEntry, filterByType, } from '@finos/legend-shared'; import { MappingExecutionState } from './MappingExecutionState.js'; import { RootFlatDataInstanceSetImplementationState } from './FlatDataInstanceSetImplementationState.js'; import { UnsupportedInstanceSetImplementationState } from './UnsupportedInstanceSetImplementationState.js'; import { RootRelationalInstanceSetImplementationState } from './relational/RelationalInstanceSetImplementationState.js'; import { getAllClassMappings, GRAPH_MANAGER_EVENT, PRIMITIVE_TYPE, fromElementPathToMappingElementId, extractSourceInformationCoordinates, getAllEnumerationMappings, Class, Enumeration, Mapping, EnumerationMapping, SetImplementation, PureInstanceSetImplementation, MappingTest, ExpectedOutputMappingTestAssert, ObjectInputData, ObjectInputType, FlatDataInstanceSetImplementation, InstanceSetImplementation, EmbeddedFlatDataPropertyMapping, FlatDataInputData, RootFlatDataRecordType, PackageableElementExplicitReference, RootFlatDataRecordTypeExplicitReference, RootRelationalInstanceSetImplementation, EmbeddedRelationalInstanceSetImplementation, AggregationAwareSetImplementation, TableAlias, RelationalInputData, RelationalInputType, OperationSetImplementation, OperationType, AssociationImplementation, InferableMappingElementIdExplicitValue, InferableMappingElementRootExplicitValue, stub_Class, findPropertyMapping, } from '@finos/legend-graph'; import { LambdaEditorState } from '@finos/legend-application'; import { flatData_setSourceRootRecordType } from '../../../graphModifier/StoreFlatData_GraphModifierHelper.js'; import { pureInstanceSetImpl_setSrcClass, mapping_addClassMapping, mapping_addEnumerationMapping, mapping_addTest, mapping_deleteAssociationMapping, mapping_deleteClassMapping, mapping_deleteEnumerationMapping, mapping_deleteTest, setImpl_updateRootOnCreate, setImpl_updateRootOnDelete, } from '../../../graphModifier/DSLMapping_GraphModifierHelper.js'; import { BASIC_SET_IMPLEMENTATION_TYPE } from '../../../shared/ModelUtil.js'; import { rootRelationalSetImp_setMainTableAlias } from '../../../graphModifier/StoreRelational_GraphModifierHelper.js'; export const generateMappingTestName = (mapping) => { const generatedName = generateEnumerableNameFromToken(mapping.tests.map((test) => test.name), 'test'); assertTrue(!mapping.tests.find((test) => test.name === generatedName), `Can't auto-generate test name for value '${generatedName}'`); return generatedName; }; export var MAPPING_ELEMENT_SOURCE_ID_LABEL; (function (MAPPING_ELEMENT_SOURCE_ID_LABEL) { MAPPING_ELEMENT_SOURCE_ID_LABEL["ENUMERATION_MAPPING"] = "enumerationMapping"; MAPPING_ELEMENT_SOURCE_ID_LABEL["OPERATION_CLASS_MAPPING"] = "operationClassMapping"; MAPPING_ELEMENT_SOURCE_ID_LABEL["PURE_INSTANCE_CLASS_MAPPING"] = "pureInstanceClassMapping"; MAPPING_ELEMENT_SOURCE_ID_LABEL["FLAT_DATA_CLASS_MAPPING"] = "flatDataClassMapping"; MAPPING_ELEMENT_SOURCE_ID_LABEL["RELATIONAL_CLASS_MAPPING"] = "relationalClassMapping"; MAPPING_ELEMENT_SOURCE_ID_LABEL["AGGREGATION_AWARE_CLASS_MAPPING"] = "aggregationAwareClassMapping"; })(MAPPING_ELEMENT_SOURCE_ID_LABEL = MAPPING_ELEMENT_SOURCE_ID_LABEL || (MAPPING_ELEMENT_SOURCE_ID_LABEL = {})); export var MAPPING_ELEMENT_TYPE; (function (MAPPING_ELEMENT_TYPE) { MAPPING_ELEMENT_TYPE["CLASS"] = "CLASS"; MAPPING_ELEMENT_TYPE["ENUMERATION"] = "ENUMERATION"; MAPPING_ELEMENT_TYPE["ASSOCIATION"] = "ASSOCIATION"; })(MAPPING_ELEMENT_TYPE = MAPPING_ELEMENT_TYPE || (MAPPING_ELEMENT_TYPE = {})); export const getMappingElementTarget = (mappingElement) => { if (mappingElement instanceof EnumerationMapping) { return mappingElement.enumeration.value; } else if (mappingElement instanceof AssociationImplementation) { return mappingElement.association.value; } else if (mappingElement instanceof EmbeddedFlatDataPropertyMapping) { return mappingElement.class.value; } else if (mappingElement instanceof EmbeddedRelationalInstanceSetImplementation) { return mappingElement.class.value; } else if (mappingElement instanceof SetImplementation) { return mappingElement.class.value; } throw new UnsupportedOperationError(`Can't derive target of mapping element`, mappingElement); }; export const getMappingElementLabel = (mappingElement, editorStore) => { if (mappingElement instanceof EnumerationMapping) { return { value: `${fromElementPathToMappingElementId(mappingElement.enumeration.value.path) === mappingElement.id.value ? mappingElement.enumeration.value.name : `${mappingElement.enumeration.value.name} [${mappingElement.id.value}]`}`, root: false, tooltip: mappingElement.enumeration.value.path, }; } else if (mappingElement instanceof AssociationImplementation) { return { value: `${fromElementPathToMappingElementId(mappingElement.association.value.path) === mappingElement.id.value ? mappingElement.association.value.name : `${mappingElement.association.value.name} [${mappingElement.id.value}]`}`, root: false, tooltip: mappingElement.association.value.path, }; } else if (mappingElement instanceof SetImplementation) { if (mappingElement instanceof EmbeddedFlatDataPropertyMapping) { return { value: `${mappingElement.class.value.name} [${mappingElement.property.value.name}]`, root: mappingElement.root.value, tooltip: mappingElement.class.value.path, }; } const extraSetImplementationMappingElementLabelInfoBuilders = editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraSetImplementationMappingElementLabelInfoBuilders?.() ?? []); for (const labelInfoBuilder of extraSetImplementationMappingElementLabelInfoBuilders) { const labelInfo = labelInfoBuilder(mappingElement); if (labelInfo) { return labelInfo; } } return { value: `${fromElementPathToMappingElementId(mappingElement.class.value.path) === mappingElement.id.value ? mappingElement.root.value ? mappingElement.class.value.name : `${mappingElement.class.value.name} [default]` : `${mappingElement.class.value.name} [${mappingElement.id.value}]`}`, root: mappingElement.root.value, tooltip: mappingElement.class.value.path, }; } throw new UnsupportedOperationError(`Can't build label info for mapping element`, mappingElement); }; export const getMappingElementSource = (mappingElement, plugins) => { if (mappingElement instanceof OperationSetImplementation) { // NOTE: we don't need to resolve operation union because at the end of the day, it uses other class mappings // in the mapping, so if we use this method on all class mappings of a mapping, we don't miss anything return undefined; } else if (mappingElement instanceof EnumerationMapping) { return mappingElement.sourceType?.value; } else if (mappingElement instanceof AssociationImplementation) { throw new UnsupportedOperationError(); } else if (mappingElement instanceof PureInstanceSetImplementation) { return mappingElement.srcClass?.value; } else if (mappingElement instanceof FlatDataInstanceSetImplementation) { return mappingElement.sourceRootRecordType.value; } else if (mappingElement instanceof EmbeddedFlatDataPropertyMapping) { return getMappingElementSource(guaranteeType(mappingElement.rootInstanceSetImplementation, FlatDataInstanceSetImplementation), plugins); } else if (mappingElement instanceof RootRelationalInstanceSetImplementation) { return mappingElement.mainTableAlias; } else if (mappingElement instanceof EmbeddedRelationalInstanceSetImplementation) { return mappingElement.rootInstanceSetImplementation.mainTableAlias; } else if (mappingElement instanceof AggregationAwareSetImplementation) { return getMappingElementSource(mappingElement.mainSetImplementation, plugins); } const extraMappingElementSourceExtractors = plugins.flatMap((plugin) => plugin.getExtraMappingElementSourceExtractors?.() ?? []); for (const extractor of extraMappingElementSourceExtractors) { const mappingElementSource = extractor(mappingElement); if (mappingElementSource) { return mappingElementSource; } } throw new UnsupportedOperationError(`Can't extract source of mapping element: no compatible extractor available from plugins`, mappingElement); }; export const getMappingElementType = (mappingElement) => { if (mappingElement instanceof EnumerationMapping) { return MAPPING_ELEMENT_TYPE.ENUMERATION; } else if (mappingElement instanceof AssociationImplementation) { return MAPPING_ELEMENT_TYPE.ASSOCIATION; } else if (mappingElement instanceof EmbeddedFlatDataPropertyMapping) { return MAPPING_ELEMENT_TYPE.CLASS; } else if (mappingElement instanceof SetImplementation) { return MAPPING_ELEMENT_TYPE.CLASS; } throw new UnsupportedOperationError(`Can't classify mapping element`, mappingElement); }; export const createClassMapping = (mapping, id, _class, setImpType, editorStore) => { let setImp; // NOTE: by default when we create a new instance set implementation, we will create PURE instance set implementation // we don't let users choose the various instance set implementation type as that require proper source // e.g. flat data class mapping requires stubbing the source switch (setImpType) { case BASIC_SET_IMPLEMENTATION_TYPE.OPERATION: setImp = new OperationSetImplementation(InferableMappingElementIdExplicitValue.create(id, _class.path), mapping, PackageableElementExplicitReference.create(_class), InferableMappingElementRootExplicitValue.create(false), OperationType.STORE_UNION); break; case BASIC_SET_IMPLEMENTATION_TYPE.INSTANCE: setImp = new PureInstanceSetImplementation(InferableMappingElementIdExplicitValue.create(id, _class.path), mapping, PackageableElementExplicitReference.create(_class), InferableMappingElementRootExplicitValue.create(false), undefined); break; default: return undefined; } setImpl_updateRootOnCreate(setImp); mapping_addClassMapping(mapping, setImp, editorStore.changeDetectionState.observerContext); return setImp; }; export const createEnumerationMapping = (mapping, id, enumeration, sourceType) => { const enumMapping = new EnumerationMapping(InferableMappingElementIdExplicitValue.create(id, enumeration.path), PackageableElementExplicitReference.create(enumeration), mapping, PackageableElementExplicitReference.create(sourceType)); mapping_addEnumerationMapping(mapping, enumMapping); return enumMapping; }; export const getEmbeddedSetImplementations = (setImpl) => { const embeddedPropertyMappings = setImpl.propertyMappings.filter( // NOTE: we use this convenient flag to check if something is embedded mapping or not // however, in reality, we can check for presence of `propertyMappings`, or more overkill // do an extension mechanism to figure this out, for example, do an extension mechanism // to check if an instance set implementation is embedded or not (pm) => pm._isEmbedded); return embeddedPropertyMappings .flatMap(getEmbeddedSetImplementations) .concat(embeddedPropertyMappings); }; // We only care to get `own` class mapping as embedded set implementations can only be within the // current class mapping i.e current mapping. const getMappingEmbeddedSetImplementations = (mapping) => mapping.classMappings .filter(filterByType(InstanceSetImplementation)) .map(getEmbeddedSetImplementations) .flat(); const getMappingElementByTypeAndId = (mapping, mappingElementType, mappingElementId) => { // NOTE: ID must be unique across all mapping elements of the same type switch (mappingElementType) { case MAPPING_ELEMENT_TYPE.CLASS: return (getAllClassMappings(mapping).find((classMapping) => classMapping.id.value === mappingElementId) ?? getMappingEmbeddedSetImplementations(mapping) .filter(filterByType(EmbeddedFlatDataPropertyMapping)) .find((me) => me.id.value === mappingElementId)); case MAPPING_ELEMENT_TYPE.ASSOCIATION: return mapping.associationMappings.find((associationMapping) => associationMapping.id.value === mappingElementId); case MAPPING_ELEMENT_TYPE.ENUMERATION: return getAllEnumerationMappings(mapping).find((enumerationMapping) => enumerationMapping.id.value === mappingElementId); default: return undefined; } }; // TODO?: We need to consider whther to keep this method or not, because in the future we might // need to treat class mappings, enumeration mappings, and association mappings fairly differently // TODO: account for mapping includes? export const getAllMappingElements = (mapping) => [ ...mapping.classMappings, ...mapping.associationMappings, ...mapping.enumerationMappings, ]; const constructMappingElementNodeData = (mappingElement, editorStore) => ({ id: `${mappingElement.id.value}`, mappingElement: mappingElement, label: getMappingElementLabel(mappingElement, editorStore).value, }); const getMappingElementTreeNodeData = (mappingElement, editorStore) => { const nodeData = constructMappingElementNodeData(mappingElement, editorStore); if (mappingElement instanceof FlatDataInstanceSetImplementation || mappingElement instanceof EmbeddedFlatDataPropertyMapping) { const embedded = mappingElement.propertyMappings.filter(filterByType(EmbeddedFlatDataPropertyMapping)); nodeData.childrenIds = embedded.map((e) => `${nodeData.id}.${e.property.value.name}`); } return nodeData; }; const getMappingIdentitySortString = (me, type) => `${type.name}-${type.path}-${me.id.value}`; const getMappingElementTreeData = (mapping, editorStore) => { const rootIds = []; const nodes = new Map(); const rootMappingElements = getAllMappingElements(mapping).sort((a, b) => getMappingIdentitySortString(a, getMappingElementTarget(a)).localeCompare(getMappingIdentitySortString(b, getMappingElementTarget(b)))); rootMappingElements.forEach((mappingElement) => { const mappingElementTreeNodeData = getMappingElementTreeNodeData(mappingElement, editorStore); addUniqueEntry(rootIds, mappingElementTreeNodeData.id); nodes.set(mappingElementTreeNodeData.id, mappingElementTreeNodeData); }); return { rootIds, nodes }; }; const reprocessMappingElement = (mappingElement, treeNodes, openNodes, editorStore) => { const nodeData = constructMappingElementNodeData(mappingElement, editorStore); if (mappingElement instanceof FlatDataInstanceSetImplementation || mappingElement instanceof EmbeddedFlatDataPropertyMapping) { const embedded = mappingElement.propertyMappings.filter(filterByType(EmbeddedFlatDataPropertyMapping)); nodeData.childrenIds = embedded.map((e) => `${nodeData.id}.${e.property.value.name}`); if (openNodes.includes(mappingElement.id.value)) { nodeData.isOpen = true; embedded.forEach((e) => reprocessMappingElement(e, treeNodes, openNodes, editorStore)); } } treeNodes.set(nodeData.id, nodeData); return nodeData; }; const reprocessMappingElementNodes = (mapping, openNodes, editorStore) => { const rootIds = []; const nodes = new Map(); const rootMappingElements = getAllMappingElements(mapping).sort((a, b) => getMappingIdentitySortString(a, getMappingElementTarget(a)).localeCompare(getMappingIdentitySortString(b, getMappingElementTarget(b)))); rootMappingElements.forEach((mappingElement) => { const mappingElementTreeNodeData = reprocessMappingElement(mappingElement, nodes, openNodes, editorStore); addUniqueEntry(rootIds, mappingElementTreeNodeData.id); }); return { rootIds, nodes }; }; export class MappingEditorState extends ElementEditorState { currentTabState; openedTabStates = []; mappingExplorerTreeData; newMappingElementSpec; mappingTestStates = []; isRunningAllTests = false; allTestRunTime = 0; constructor(editorStore, element) { super(editorStore, element); makeObservable(this, { currentTabState: observable, openedTabStates: observable, mappingTestStates: observable, newMappingElementSpec: observable, isRunningAllTests: observable, allTestRunTime: observable, mappingExplorerTreeData: observable.ref, mapping: computed, testSuiteResult: computed, hasCompilationError: computed, setNewMappingElementSpec: action, setMappingExplorerTreeNodeData: action, openMappingElement: action, closeAllTabs: action, createMappingElement: action, reprocessMappingExplorerTree: action, mappingElementsWithSimilarTarget: computed, reprocess: action, openTab: flow, closeTab: flow, closeAllOtherTabs: flow, openTest: flow, buildExecution: flow, addTest: flow, deleteTest: flow, createNewTest: flow, runTests: flow, changeClassMappingSourceDriver: flow, closeMappingElementTabState: flow, deleteMappingElement: flow, }); this.editorStore = editorStore; this.mappingTestStates = this.mapping.tests.map((test) => new MappingTestState(editorStore, test, this)); this.mappingExplorerTreeData = getMappingElementTreeData(this.mapping, editorStore); } get mapping() { return guaranteeType(this.element, Mapping, 'Element inside mapping editor state must be a mapping'); } /** * 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. */ get mappingElementsWithSimilarTarget() { if (this.currentTabState instanceof MappingElementState) { const mappingElement = this.currentTabState.mappingElement; switch (getMappingElementType(mappingElement)) { case MAPPING_ELEMENT_TYPE.CLASS: return this.mapping.classMappings.filter((cm) => cm.class.value === mappingElement.class.value); case MAPPING_ELEMENT_TYPE.ENUMERATION: return this.mapping.enumerationMappings.filter((em) => em.enumeration.value === mappingElement.enumeration.value); case MAPPING_ELEMENT_TYPE.ASSOCIATION: // NOTE: we might not even support Association Mapping default: return []; } } return []; } setNewMappingElementSpec(spec) { this.newMappingElementSpec = spec; } // -------------------------------------- Tabs --------------------------------------- *openTab(tabState) { if (tabState !== this.currentTabState) { if (tabState instanceof MappingTestState) { yield flowResult(this.openTest(tabState.test)); } else if (tabState instanceof MappingElementState) { this.openMappingElement(tabState.mappingElement, false); } else if (tabState instanceof MappingExecutionState) { this.currentTabState = tabState; } } } *closeTab(tabState) { const tabIndex = this.openedTabStates.findIndex((ts) => ts === tabState); assertTrue(tabIndex !== -1, `Mapping editor tab should be currently opened`); this.openedTabStates.splice(tabIndex, 1); // if current tab is closed, we need further processing if (this.currentTabState === tabState) { if (this.openedTabStates.length) { const openIndex = tabIndex - 1; const tabStateToOpen = openIndex >= 0 ? this.openedTabStates[openIndex] : this.openedTabStates.length ? this.openedTabStates[0] : undefined; if (tabStateToOpen) { yield flowResult(this.openTab(tabStateToOpen)); } else { this.currentTabState = undefined; } } else { this.currentTabState = undefined; } } } *closeAllOtherTabs(tabState) { assertNonNullable(this.openedTabStates.find((ts) => ts === tabState), `Mapping editor tab should be currently opened`); this.openedTabStates = [tabState]; yield flowResult(this.openTab(tabState)); } closeAllTabs() { this.currentTabState = undefined; this.openedTabStates = []; } // -------------------------------------- Explorer Tree --------------------------------------- setMappingExplorerTreeNodeData(data) { this.mappingExplorerTreeData = data; } onMappingExplorerTreeNodeExpand = (node) => { const mappingElement = node.mappingElement; const treeData = this.mappingExplorerTreeData; if (node.childrenIds?.length) { node.isOpen = !node.isOpen; if (mappingElement instanceof FlatDataInstanceSetImplementation || mappingElement instanceof EmbeddedFlatDataPropertyMapping) { mappingElement.propertyMappings .filter(filterByType(EmbeddedFlatDataPropertyMapping)) .forEach((embeddedPM) => { const embeddedPropertyNode = getMappingElementTreeNodeData(embeddedPM, this.editorStore); treeData.nodes.set(embeddedPropertyNode.id, embeddedPropertyNode); }); } } this.setMappingExplorerTreeNodeData({ ...treeData }); }; onMappingExplorerTreeNodeSelect = (node) => { this.onMappingExplorerTreeNodeExpand(node); this.openMappingElement(node.mappingElement, false); }; getMappingExplorerTreeChildNodes = (node) => { if (!node.childrenIds) { return []; } const childrenNodes = node.childrenIds .map((id) => this.mappingExplorerTreeData.nodes.get(id)) .filter(isNonNullable) .sort((a, b) => a.label.localeCompare(b.label)); return childrenNodes; }; reprocessMappingExplorerTree(openNodeFoCurrentTab = false) { const openedTreeNodeIds = Array.from(this.mappingExplorerTreeData.nodes.values()) .filter((node) => node.isOpen) .map((node) => node.id); this.setMappingExplorerTreeNodeData(reprocessMappingElementNodes(this.mapping, openedTreeNodeIds, this.editorStore)); if (openNodeFoCurrentTab) { // TODO: we should follow the example of project explorer where we maintain the currentlySelectedNode // instead of adaptively show the `selectedNode` based on current tab state. This is bad // this.setMappingElementTreeNodeData(openNode(openElement, this.mappingElementsTreeData)); // const openNode = (element: EmbeddedFlatDataPropertyMapping, treeData: TreeData<MappingElementTreeNodeData>): MappingElementTreeNodeData => { // if (element instanceof EmbeddedFlatDataPropertyMapping) { // let currentElement: InstanceSetImplementation | undefined = element; // while (currentElement instanceof EmbeddedFlatDataPropertyMapping) { // const node: MappingElementTreeNodeData = treeData.nodes.get(currentElement.id) ?? addNode(currentElement, treeData); // node.isOpen = true; // currentElement = currentElement.owner as InstanceSetImplementation; // } // // create children if not created // element.propertyMappings.filter((me: AbstractFlatDataPropertyMapping): me is EmbeddedFlatDataPropertyMapping => me instanceof EmbeddedFlatDataPropertyMapping) // .forEach(el => treeData.nodes.get(el.id) ?? addNode(el, treeData)); // } // return treeData; // const addNode = (element: EmbeddedFlatDataPropertyMapping, treeData: TreeData<MappingElementTreeNodeData>): MappingElementTreeNodeData => { // const newNode = getMappingElementTreeNodeData(element); // treeData.nodes.set(newNode.id, newNode); // if (element.owner instanceof FlatDataInstanceSetImplementation || element.owner instanceof EmbeddedFlatDataPropertyMapping) { // const baseNode = treeData.nodes.get(element.owner.id); // if (baseNode) { // baseNode.isOpen = true; // } // } else { // const parentNode = treeData.nodes.get(element.owner.id); // if (parentNode) { // parentNode.childrenIds = parentNode.childrenIds ? Array.from((new Set(parentNode.childrenIds)).add(newNode.id)) : [newNode.id]; // } // } // return newNode; // }; } } // -------------------------------------- Mapping Element --------------------------------------- openMappingElement(mappingElement, openInAdjacentTab) { if (mappingElement instanceof AssociationImplementation) { this.editorStore.applicationStore.notifyUnsupportedFeature('Association mapping editor'); return; } // Open mapping element from included mapping in another mapping editor tab if (mappingElement._PARENT !== this.element) { this.editorStore.openElement(mappingElement._PARENT); } const currentMappingEditorState = this.editorStore.getCurrentEditorState(MappingEditorState); // If the next mapping element to be opened is not opened yet, we will find the right place to put it in the tab bar if (!currentMappingEditorState.openedTabStates.find((tabState) => tabState instanceof MappingElementState && tabState.mappingElement === mappingElement)) { const newMappingElementState = guaranteeNonNullable(currentMappingEditorState.createMappingElementState(mappingElement)); if (openInAdjacentTab) { const currentMappingElementIndex = this.openedTabStates.findIndex((tabState) => tabState === this.currentTabState); if (currentMappingElementIndex !== -1) { currentMappingEditorState.openedTabStates.splice(currentMappingElementIndex + 1, 0, newMappingElementState); } else { throw new IllegalStateError(`Can't find current mapping editor tab`); } } else { currentMappingEditorState.openedTabStates.push(newMappingElementState); } } // Set current mapping element, i.e. switch to new tab currentMappingEditorState.currentTabState = currentMappingEditorState.openedTabStates.find((tabState) => tabState instanceof MappingElementState && tabState.mappingElement === mappingElement); currentMappingEditorState.reprocessMappingExplorerTree(true); } *changeClassMappingSourceDriver(setImplementation, newSource) { const currentSource = getMappingElementSource(setImplementation, this.editorStore.pluginManager.getApplicationPlugins()); if (currentSource !== newSource) { // first, we check if the current class mapping is compatible with the new source // if it is, we don't need to create a new class mapping, // if it is not, we would need to create a new class mapping that is compatible with the new source // and as a result, we will reset all the property mappings // // TODO?: we might need to think of how we would handle embedded class mapping let sourceUpdated = false; if (setImplementation instanceof PureInstanceSetImplementation) { if (newSource instanceof Class || newSource === undefined) { pureInstanceSetImpl_setSrcClass(setImplementation, newSource ? PackageableElementExplicitReference.create(newSource) : undefined); sourceUpdated = true; } } else if (setImplementation instanceof FlatDataInstanceSetImplementation) { if (newSource instanceof RootFlatDataRecordType && !getEmbeddedSetImplementations(setImplementation).length) { flatData_setSourceRootRecordType(setImplementation, newSource); sourceUpdated = true; } } else if (setImplementation instanceof RootRelationalInstanceSetImplementation) { if (newSource instanceof TableAlias && !getEmbeddedSetImplementations(setImplementation).length) { rootRelationalSetImp_setMainTableAlias(setImplementation, newSource); sourceUpdated = true; } } else { const extraInstanceSetImplementationSourceUpdaters = this.editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraInstanceSetImplementationSourceUpdaters?.() ?? []); for (const updater of extraInstanceSetImplementationSourceUpdaters) { sourceUpdated = updater(setImplementation, newSource); if (sourceUpdated) { break; } } } // here we require a change of set implementation as the source type does not match the what the current class mapping supports if (!sourceUpdated) { let newSetImp; if (newSource instanceof Class || newSource === undefined) { newSetImp = new PureInstanceSetImplementation(setImplementation.id, this.mapping, setImplementation.class, setImplementation.root, newSource ? PackageableElementExplicitReference.create(newSource) : undefined); } else if (newSource instanceof RootFlatDataRecordType) { newSetImp = new FlatDataInstanceSetImplementation(setImplementation.id, this.mapping, PackageableElementExplicitReference.create(setImplementation.class.value), setImplementation.root, RootFlatDataRecordTypeExplicitReference.create(newSource)); } else if (newSource instanceof TableAlias) { const newRootRelationalInstanceSetImplementation = new RootRelationalInstanceSetImplementation(setImplementation.id, this.mapping, setImplementation.class, setImplementation.root); newRootRelationalInstanceSetImplementation.mainTableAlias = newSource; newSetImp = newRootRelationalInstanceSetImplementation; } else { throw new UnsupportedOperationError(`Can't use the specified class mapping source`, newSource); } // replace the instance set implementation in mapping const idx = guaranteeNonNullable(this.mapping.classMappings.findIndex((classMapping) => classMapping === setImplementation), `Can't find class mapping with ID '${setImplementation.id.value}' in mapping '${this.mapping.path}'`); this.mapping.classMappings[idx] = newSetImp; // replace the instance set implementation in opened tab state const setImplStateIdx = guaranteeNonNullable(this.openedTabStates.findIndex((tabState) => tabState instanceof MappingElementState && tabState.mappingElement === setImplementation), `Can't find any mapping state for class mapping with ID '${setImplementation.id.value}'`); const newMappingElementState = guaranteeNonNullable(this.createMappingElementState(newSetImp)); this.openedTabStates[setImplStateIdx] = newMappingElementState; this.currentTabState = newMappingElementState; // close all children yield flowResult(this.closeMappingElementTabState(setImplementation)); this.reprocessMappingExplorerTree(true); } } } *closeMappingElementTabState(mappingElement) { let mappingElementsToClose = [mappingElement]; if (this.editorStore.graphManagerState.isInstanceSetImplementation(mappingElement)) { const embeddedChildren = getEmbeddedSetImplementations(mappingElement); mappingElementsToClose = mappingElementsToClose.concat(embeddedChildren); } const matchMappingElementState = (tabState) => tabState instanceof MappingElementState && mappingElementsToClose.includes(tabState.mappingElement); if (this.currentTabState && matchMappingElementState(this.currentTabState)) { yield flowResult(this.closeTab(this.currentTabState)); } this.openedTabStates = this.openedTabStates.filter((tabState) => !matchMappingElementState(tabState)); } *deleteMappingElement(mappingElement) { if (mappingElement instanceof EnumerationMapping) { mapping_deleteEnumerationMapping(this.mapping, mappingElement); } else if (mappingElement instanceof AssociationImplementation) { mapping_deleteAssociationMapping(this.mapping, mappingElement); } else if (mappingElement instanceof EmbeddedFlatDataPropertyMapping) { deleteEntry(mappingElement._OWNER.propertyMappings, mappingElement); } else if (mappingElement instanceof EmbeddedRelationalInstanceSetImplementation) { deleteEntry(mappingElement._OWNER.propertyMappings, mappingElement); } else if (mappingElement instanceof SetImplementation) { mapping_deleteClassMapping(this.mapping, mappingElement); } if (mappingElement instanceof SetImplementation) { setImpl_updateRootOnDelete(mappingElement); } yield flowResult(this.closeMappingElementTabState(mappingElement)); this.reprocessMappingExplorerTree(); } /** * This will determine if we need to show the new mapping element modal or not */ createMappingElement(spec) { if (spec.target) { const suggestedId = fromElementPathToMappingElementId(spec.target.path); const mappingIds = getAllMappingElements(this.mapping).map((mElement) => mElement.id.value); const showId = mappingIds.includes(suggestedId); const showClasMappingType = spec.target instanceof Class; const showNewMappingModal = [ showId, spec.showTarget, showClasMappingType, ].some(Boolean); if (showNewMappingModal) { this.setNewMappingElementSpec(spec); } else { let newMappingElement = undefined; if (spec.target instanceof Enumeration) { // We default to a source type of String when creating a new enumeration mapping newMappingElement = createEnumerationMapping(this.mapping, suggestedId, spec.target, this.editorStore.graphManagerState.graph.getPrimitiveType(PRIMITIVE_TYPE.STRING)); } // NOTE: we don't support association now, nor do we support this for class // since class requires a step to choose the class mapping type if (newMappingElement) { this.openMappingElement(newMappingElement, true); } if (spec.postSubmitAction) { spec.postSubmitAction(newMappingElement); } } } else { this.setNewMappingElementSpec(spec); } } createMappingElementState(mappingElement) { if (!mappingElement) { return undefined; } if (mappingElement instanceof PureInstanceSetImplementation) { return new PureInstanceSetImplementationState(this.editorStore, mappingElement); } else if (mappingElement instanceof FlatDataInstanceSetImplementation) { return new RootFlatDataInstanceSetImplementationState(this.editorStore, mappingElement); } else if (mappingElement instanceof EmbeddedFlatDataPropertyMapping) { throw new UnsupportedOperationError(`Can't create mapping element state for emebdded property mapping`); } else if (mappingElement instanceof RootRelationalInstanceSetImplementation) { return new RootRelationalInstanceSetImplementationState(this.editorStore, mappingElement); } else if (mappingElement instanceof EmbeddedRelationalInstanceSetImplementation || mappingElement instanceof AggregationAwareSetImplementation) { return new UnsupportedInstanceSetImplementationState(this.editorStore, mappingElement); } const extraMappingElementStateCreators = this.editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraMappingElementStateCreators?.() ?? []); for (const elementStateCreator of extraMappingElementStateCreators) { const mappingElementState = elementStateCreator(mappingElement, this.editorStore); if (mappingElementState) { return mappingElementState; } } return new MappingElementState(this.editorStore, mappingElement); } // -------------------------------------- Compilation --------------------------------------- reprocess(newElement, editorStore) { const mappingEditorState = new MappingEditorState(editorStore, newElement); // process tabs mappingEditorState.openedTabStates = this.openedTabStates .map((tabState) => { if (tabState instanceof MappingElementState) { const mappingElement = getMappingElementByTypeAndId(mappingEditorState.mapping, getMappingElementType(tabState.mappingElement), tabState.mappingElement.id.value); return this.createMappingElementState(mappingElement); } else if (tabState instanceof MappingTestState) { return mappingEditorState.mappingTestStates.find((testState) => testState.test.name === tabState.test.name); } else if (tabState instanceof MappingExecutionState) { // TODO?: re-consider if we would want to reprocess mapping execution tabs or not return undefined; } // TODO?: re-consider if we would want to reprocess mapping execution tabs or not return undefined; }) .filter(isNonNullable); // process currently opened tab if (this.currentTabState instanceof MappingElementState) { const currentlyOpenedMappingElement = getMappingElementByTypeAndId(mappingEditorState.mapping, getMappingElementType(this.currentTabState.mappingElement), this.currentTabState.mappingElement.id.value); mappingEditorState.currentTabState = this.openedTabStates.find((tabState) => tabState instanceof MappingElementState && tabState.mappingElement === currentlyOpenedMappingElement); } else if (this.currentTabState instanceof MappingTestState) { const currentlyOpenedMappingTest = mappingEditorState.mappingTestStates.find((testState) => this.currentTabState instanceof MappingTestState && testState.test.name === this.currentTabState.test.name)?.test; mappingEditorState.currentTabState = this.openedTabStates.find((tabState) => tabState instanceof MappingTestState && tabState.test === currentlyOpenedMappingTest); } else { // TODO?: re-consider if we would want to reprocess mapping execution tab or not mappingEditorState.currentTabState = undefined; } return mappingEditorState; } revealCompilationError(compilationError) { let revealed = false; try { if (compilationError.sourceInformation) { const errorCoordinates = extractSourceInformationCoordinates(compilationError.sourceInformation); if (errorCoordinates) { const sourceId = compilationError.sourceInformation.sourceId; assertTrue(errorCoordinates.length >= 5); const [, mappingElementType, mappingElementId, propertyName, targetPropertyId,] = errorCoordinates; const newMappingElement = getMappingElementByTypeAndId(this.mapping, guaranteeNonNullable(mappingElementType, `Can't reveal compilation error: mapping type is missing`), guaranteeNonNullable(mappingElementId, `Can't reveal compilation error: mapping ID is missing`)); // TODO: take care of operation mapping using systematic coordinates // See https://github.com/finos/legend-studio/issues/1168 if (newMappingElement instanceof InstanceSetImplementation) { const propertyMapping = findPropertyMapping(newMappingElement, guaranteeNonNullable(propertyName, `Can't reveal compilation error: mapping property name is missing`), targetPropertyId); if (propertyMapping) { if (!(this.currentTabState instanceof MappingElementState) || newMappingElement !== this.currentTabState.mappingElement) { this.openMappingElement(newMappingElement, false); } if ( // TODO: take care of operation mapping using systematic coordinates // See https://github.com/finos/legend-studio/issues/1168 this.currentTabState instanceof InstanceSetImplementationState) { const propertyMappingState = this.currentTabState.propertyMappingStates .filter(filterByType(LambdaEditorState)) .find((state) => state.lambdaId === sourceId); if (propertyMappingState) { propertyMappingState.setCompilationError(compilationError); revealed = true; } } } } } } } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.warn(LogEvent.create(GRAPH_MANAGER_EVENT.COMPILATION_FAILURE), `Can't locate error, redirecting to text mode`, error); } return revealed; } get hasCompilationError() { return this.openedTabStates .filter(filterByType(InstanceSetImplementationState)) .some((tabState) => tabState.propertyMappingStates.some((pmState) => Boolean(pmState.compilationError))); } clearCompilationError() { this.openedTabStates .filter(filterByType(InstanceSetImplementationState)) .forEach((tabState) => { tabState.propertyMappingStates.forEach((pmState) => pmState.setCompilationError(undefined)); }); } // -------------------------------------- Execution --------------------------------------- *buildExecution(setImpl) { const executionTabStates = this.openedTabStates.filter(filterByType(MappingExecutionState)); const executionStateName = generateEnumerableNameFromToken(executionTabStates.map((tabState) => tabState.name), 'execution'); assertTrue(!executionTabStates.find((tabState) => tabState.name === executionStateName), `Can't auto-generate execution name for value '${executionStateName}'`); const executionState = new MappingExecutionState(this.editorStore, this, executionStateName); yield flowResult(executionState.buildQueryWithClassMapping(setImpl)); addUniqueEntry(this.openedTabStates, executionState); this.currentTabState = executionState; } // -------------------------------------- Test --------------------------------------- *openTest(test, openTab) { const isOpened = Boolean(this.openedTabStates.find((tabState) => tabState instanceof MappingTestState && tabState.test === test)); const testState = this.mappingTestStates.find((mappingTestState) => mappingTestState.test === test); assertNonNullable(testState, `Mapping test state must already been created for test '${test.name}'`); if (!this.openedTabStates.find((tabState) => tabState instanceof MappingTestState && tabState.test === test)) { addUniqueEntry(this.openedTabStates, testState); } this.currentTabState = this.openedTabStates.find((tabState) => tabState instanceof MappingTestState && tabState.test === test); yield flowResult(testState.onTestStateOpen(openTab ?? // This is for user's convenience. // If the test is already opened, respect is currently opened tab // otherwise, if the test has a result, switch to show the result tab (!isOpened && testState.result !== TEST_RESULT.NONE ? MAPPING_TEST_EDITOR_TAB_TYPE.RESULT : undefined))); } get testSuiteResult() { const numberOfTestPassed = this.mappingTestStates.filter((testState) => testState.result === TEST_RESULT.PASSED).length; const numberOfTestFailed = this.mappingTestStates.filter((testState) => testState.result === TEST_RESULT.FAILED || testState.result === TEST_RESULT.ERROR).length; return numberOfTestFailed ? TEST_RESULT.FAILED : numberOfTestPassed ? TEST_RESULT.PASSED : TEST_RESULT.NONE; } *runTests() { const startTime = Date.now(); this.isRunningAllTests = true; this.mappingTestStates.forEach((testState) => testState.resetTestRunStatus()); yield Promise.all(this.mappingTestStates.map((testState) => { // run non-skip tests, and reset all skipped tests if (!testState.isSkipped) { return testState.runTest(); } testState.resetTestRunStatus(); return undefined; })); this.isRunningAllTests = false; this.allTestRunTime = Date.now() - startTime; } *addTest(test) { this.mappingTestStates.push(new MappingTestState(this.editorStore, test, this)); mapping_addTest(this.mapping, test, this.editorStore.changeDetectionState.observerContext); yield flowResult(this.openTest(test)); } *deleteTest(test) { const matchMappingTestState = (tabState) => tabState instanceof MappingTestState && tabState.test === test; mapping_deleteTest(this.mapping, test); if (this.currentTabState && matchMappingTestState(this.currentTabState)) { yield flowResult(this.closeTab(this.currentTabState)); } this.openedTabStates = this.openedTabStates.filter((tabState) => !matchMappingTestState(tabState)); this.mappingTestStates = this.mappingTestStates.filter((tabState