@finos/legend-studio
Version:
858 lines • 52.1 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 { 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