Skip to content

Commit c70be34

Browse files
Merge pull request #969 from KCSAbeywickrama/bi-dm-join-clause
[BI Data Mapper] Add support for the join clause
2 parents ede2d99 + 880cbd0 commit c70be34

File tree

36 files changed

+526
-239
lines changed

36 files changed

+526
-239
lines changed

workspaces/ballerina/ballerina-core/src/interfaces/data-mapper.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ export enum IntermediateClauseType {
5858
WHERE = "where",
5959
FROM = "from",
6060
ORDER_BY = "order by",
61-
LIMIT = "limit"
61+
LIMIT = "limit",
62+
JOIN = "join",
6263
}
6364

6465
export enum ResultClauseType {
@@ -93,7 +94,6 @@ export interface IOType {
9394
members?: IOType[];
9495
defaultValue?: unknown;
9596
optional?: boolean;
96-
focusedMemberId?: string;
9797
isFocused?: boolean;
9898
isRecursive?: boolean;
9999
isDeepNested?: boolean;
@@ -125,6 +125,7 @@ export interface ExpandedDMModel {
125125
query?: Query;
126126
mapping_fields?: Record<string, any>;
127127
triggerRefresh?: boolean;
128+
focusInputRootMap?: Record<string, string>;
128129
}
129130

130131
export interface DMModel {
@@ -138,6 +139,8 @@ export interface DMModel {
138139
focusInputs?: Record<string, IOTypeField>;
139140
mapping_fields?: Record<string, any>;
140141
triggerRefresh?: boolean;
142+
traversingRoot?: string;
143+
focusInputRootMap?: Record<string, string>;
141144
}
142145

143146
export interface ModelState {
@@ -205,6 +208,9 @@ export interface IntermediateClauseProps {
205208
type?: string;
206209
expression: string;
207210
order?: "ascending" | "descending";
211+
lhsExpression?: string;
212+
rhsExpression?: string;
213+
isOuter?: boolean;
208214
}
209215

210216
export interface IntermediateClause {

workspaces/ballerina/ballerina-extension/src/rpc-managers/data-mapper/utils.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,8 @@ export function expandDMModel(
467467
query: model.query,
468468
source: "",
469469
rootViewId,
470-
triggerRefresh: model.triggerRefresh
470+
triggerRefresh: model.triggerRefresh,
471+
focusInputRootMap: model.focusInputRootMap
471472
};
472473
}
473474

@@ -485,13 +486,18 @@ function processInputRoots(model: DMModel): IOType[] {
485486
inputs.push(input);
486487
}
487488
}
489+
490+
model.focusInputRootMap = {};
488491
const preProcessedModel: DMModel = {
489492
...model,
490493
inputs,
491494
focusInputs
492495
};
493496

494-
return inputs.map(input => processIORoot(input, preProcessedModel));
497+
return inputs.map(input => {
498+
preProcessedModel.traversingRoot = input.name;
499+
return processIORoot(input, preProcessedModel);
500+
});
495501
}
496502

497503
/**
@@ -599,6 +605,10 @@ function processArray(
599605
parentId = member.name;
600606
fieldId = member.name;
601607
isFocused = true;
608+
609+
if (model.traversingRoot){
610+
model.focusInputRootMap[parentId] = model.traversingRoot;
611+
}
602612
}
603613
}
604614

workspaces/ballerina/ballerina-visualizer/src/views/DataMapper/DataMapperView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,7 @@ export function DataMapperView(props: DataMapperProps) {
714714
};
715715

716716
const getModelSignature = (model: DMModel | ExpandedDMModel): ModelSignature => ({
717-
inputs: model.inputs.map(i => i.name),
717+
inputs: [...model.inputs.map(i => i.name), ...(model.query?.inputs || [])],
718718
output: model.output.name,
719719
subMappings: model.subMappings?.map(s => (s as IORoot | IOType).name) || [],
720720
refs: 'refs' in model ? JSON.stringify(model.refs) : ''

workspaces/ballerina/data-mapper/src/components/DataMapper/DataMapperEditor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export function DataMapperEditor(props: DataMapperEditorProps) {
150150
hasInputsOutputsChanged = false
151151
} = modelState;
152152

153-
const initialView = [{
153+
const initialView: View[] = [{
154154
label: model.output.name,
155155
targetField: name
156156
}];
@@ -228,10 +228,10 @@ export function DataMapperEditor(props: DataMapperEditorProps) {
228228
const context = new DataMapperContext(
229229
model,
230230
views,
231+
hasInputsOutputsChanged,
231232
addView,
232233
applyModifications,
233234
addArrayElement,
234-
hasInputsOutputsChanged,
235235
convertToQuery,
236236
deleteMapping,
237237
deleteSubMapping,

workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClauseEditor.tsx

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import React from "react";
2020
import { EditorContainer } from "./styles";
2121
import { Divider, Dropdown, OptionProps, Typography } from "@wso2/ui-toolkit";
2222
import { DMFormProps, DMFormField, DMFormFieldValues, IntermediateClauseType, IntermediateClause, IntermediateClauseProps } from "@wso2/ballerina-core";
23+
import { useDMQueryClausesPanelStore } from "../../../../store/store";
2324

2425
export interface ClauseEditorProps {
2526
clause?: IntermediateClause;
@@ -32,24 +33,26 @@ export interface ClauseEditorProps {
3233

3334
export function ClauseEditor(props: ClauseEditorProps) {
3435
const { clause, onSubmitText, isSaving, onSubmit, onCancel, generateForm } = props;
35-
const { type: _clauseType, properties: clauseProps } = clause ?? {};
36+
const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore.getState();
37+
const { type: _clauseType, properties: clauseProps } = clause ?? clauseToAdd ?? {};
3638

3739
const [clauseType, setClauseType] = React.useState<string>(_clauseType ?? IntermediateClauseType.WHERE);
3840
const clauseTypeItems: OptionProps[] = [
3941
{ content: "condition", value: IntermediateClauseType.WHERE },
4042
{ content: "local variable", value: IntermediateClauseType.LET },
4143
{ content: "sort by", value: IntermediateClauseType.ORDER_BY },
4244
{ content: "limit", value: IntermediateClauseType.LIMIT },
43-
{ content: "from", value: IntermediateClauseType.FROM }
45+
{ content: "from", value: IntermediateClauseType.FROM },
46+
{ content: "join", value: IntermediateClauseType.JOIN },
4447
]
4548

4649
const nameField: DMFormField = {
4750
key: "name",
48-
label: "Name",
51+
label: clauseType === IntermediateClauseType.JOIN ? "Item Alias" : "Name",
4952
type: "IDENTIFIER",
5053
optional: false,
5154
editable: true,
52-
documentation: "Enter the name of the tool.",
55+
documentation: clauseType === IntermediateClauseType.JOIN ? "Represents each record in the joined collection" : "Enter a name for the variable",
5356
value: clauseProps?.name ?? "",
5457
valueTypeConstraint: "Global",
5558
enabled: true,
@@ -61,19 +64,19 @@ export function ClauseEditor(props: ClauseEditorProps) {
6164
type: "TYPE",
6265
optional: false,
6366
editable: true,
64-
documentation: "Enter the type of the clause.",
67+
documentation: "Enter the type of the clause",
6568
value: clauseProps?.type ?? "",
6669
valueTypeConstraint: "Global",
6770
enabled: true,
6871
}
6972

7073
const expressionField: DMFormField = {
7174
key: "expression",
72-
label: "Expression",
75+
label: clauseType === IntermediateClauseType.JOIN ? "Join With Collection" : "Expression",
7376
type: "EXPRESSION",
7477
optional: false,
7578
editable: true,
76-
documentation: "Enter the expression of the clause.",
79+
documentation: clauseType === IntermediateClauseType.JOIN ? "Collection to be joined" : "Enter the expression of the clause",
7780
value: clauseProps?.expression ?? "",
7881
valueTypeConstraint: "Global",
7982
enabled: true,
@@ -85,18 +88,53 @@ export function ClauseEditor(props: ClauseEditorProps) {
8588
type: "ENUM",
8689
optional: false,
8790
editable: true,
88-
documentation: "Enter the order.",
91+
documentation: "Enter the order",
8992
value: clauseProps?.order ?? "",
9093
valueTypeConstraint: "Global",
9194
enabled: true,
9295
items: ["ascending", "descending"]
9396
}
9497

98+
const lhsExpressionField: DMFormField = {
99+
key: "lhsExpression",
100+
label: "LHS Expression",
101+
type: "EXPRESSION",
102+
optional: false,
103+
editable: true,
104+
documentation: "Enter the LHS expression of join-on condition",
105+
value: clauseProps?.lhsExpression ?? "",
106+
valueTypeConstraint: "Global",
107+
enabled: true,
108+
}
109+
110+
const rhsExpressionField: DMFormField = {
111+
key: "rhsExpression",
112+
label: "RHS Expression",
113+
type: "EXPRESSION",
114+
optional: false,
115+
editable: true,
116+
documentation: "Enter the RHS expression of join-on condition",
117+
value: clauseProps?.rhsExpression ?? "",
118+
valueTypeConstraint: "Global",
119+
enabled: true,
120+
}
121+
95122
const handleSubmit = (data: DMFormFieldValues) => {
96-
onSubmit({
123+
setClauseToAdd(undefined);
124+
const clause: IntermediateClause = {
97125
type: clauseType as IntermediateClauseType,
98126
properties: data as IntermediateClauseProps
99-
});
127+
};
128+
if (clauseType === IntermediateClauseType.JOIN) {
129+
clause.properties.type = "var";
130+
clause.properties.isOuter = false;
131+
}
132+
onSubmit(clause);
133+
}
134+
135+
const handleCancel = () => {
136+
setClauseToAdd(undefined);
137+
onCancel();
100138
}
101139

102140
// function with select case to gen fields based on clause type
@@ -107,6 +145,8 @@ export function ClauseEditor(props: ClauseEditorProps) {
107145
return [nameField, typeField, expressionField];
108146
case IntermediateClauseType.ORDER_BY:
109147
return [expressionField, orderField];
148+
case IntermediateClauseType.JOIN:
149+
return [expressionField, nameField, lhsExpressionField, rhsExpressionField];
110150
default:
111151
return [expressionField];
112152
}
@@ -119,7 +159,7 @@ export function ClauseEditor(props: ClauseEditorProps) {
119159
cancelText: "Cancel",
120160
nestedForm: true,
121161
onSubmit: handleSubmit,
122-
onCancel,
162+
onCancel: handleCancel,
123163
isSaving
124164
}
125165

workspaces/ballerina/data-mapper/src/components/DataMapper/SidePanel/QueryClauses/ClausesPanel.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* under the License.
1717
*/
1818

19-
import React from "react";
19+
import React, { useEffect } from "react";
2020

2121
import { Button, Codicon, SidePanel, SidePanelBody, SidePanelTitleContainer, ThemeColors } from "@wso2/ui-toolkit";
2222
import { useDMQueryClausesPanelStore } from "../../../../store/store";
@@ -35,6 +35,7 @@ export interface ClausesPanelProps {
3535

3636
export function ClausesPanel(props: ClausesPanelProps) {
3737
const { isQueryClausesPanelOpen, setIsQueryClausesPanelOpen } = useDMQueryClausesPanelStore();
38+
const { clauseToAdd, setClauseToAdd } = useDMQueryClausesPanelStore.getState();
3839
const { query, targetField, addClauses, deleteClause, generateForm } = props;
3940

4041
const [adding, setAdding] = React.useState<number>();
@@ -67,11 +68,20 @@ export function ClausesPanel(props: ClausesPanelProps) {
6768
setEditing(undefined);
6869
}
6970

71+
useEffect(() => {
72+
if (clauseToAdd) {
73+
setAdding(clauses.length - 1);
74+
}
75+
return () => {
76+
setClauseToAdd(undefined);
77+
}
78+
}, [clauseToAdd, clauses.length, setClauseToAdd, setAdding]);
79+
7080
return (
7181
<SidePanel
7282
isOpen={isQueryClausesPanelOpen}
7383
alignment="right"
74-
width={312}
84+
width={400}
7585
overlay={false}
7686
sx={{
7787
fontFamily: "GilmerRegular",

workspaces/ballerina/data-mapper/src/components/DataMapper/Views/DataMapperView.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*/
1818
export interface View {
1919
label: string;
20-
sourceField?: string;
20+
sourceFields?: string[];
2121
targetField: string;
2222
subMappingInfo?: SubMappingInfo;
2323
}

workspaces/ballerina/data-mapper/src/components/Diagram/Actions/IONodesScrollCanvasAction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
isOutputNode
2828
} from "./utils";
2929
import { IO_NODE_DEFAULT_WIDTH, VISUALIZER_PADDING, defaultModelOptions } from "../utils/constants";
30-
import { LinkConnectorNode, QueryExprConnectorNode } from "../Node";
30+
import { ClauseConnectorNode, LinkConnectorNode, QueryExprConnectorNode } from "../Node";
3131

3232
export interface PanAndZoomCanvasActionOptions {
3333
inverseZoom?: boolean;
@@ -161,7 +161,7 @@ function repositionIntermediateNodes(outputNode: NodeModel) {
161161
if (link instanceof DataMapperLinkModel) {
162162
const sourceNode = link.getSourcePort().getNode();
163163
const targetPortPosition = link.getTargetPort().getPosition();
164-
if (sourceNode instanceof LinkConnectorNode || sourceNode instanceof QueryExprConnectorNode) {
164+
if (sourceNode instanceof LinkConnectorNode || sourceNode instanceof QueryExprConnectorNode || sourceNode instanceof ClauseConnectorNode) {
165165
sourceNode.setPosition(sourceNode.getX(), targetPortPosition.y - 4.5);
166166
}
167167
}

workspaces/ballerina/data-mapper/src/components/Diagram/Actions/utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import {
2525
QueryExprConnectorNode,
2626
LinkConnectorNode,
2727
QueryOutputNode,
28-
SubMappingNode
28+
SubMappingNode,
29+
ClauseConnectorNode
2930
} from "../Node";
3031
import { IO_NODE_DEFAULT_WIDTH } from "../utils/constants";
3132
import { DataMapperLinkModel } from "../Link";
@@ -44,7 +45,8 @@ export const OUTPUT_NODES = [
4445

4546
export const INTERMEDIATE_NODES = [
4647
LinkConnectorNode,
47-
QueryExprConnectorNode
48+
QueryExprConnectorNode,
49+
ClauseConnectorNode
4850
];
4951

5052
export const MIN_VISIBLE_HEIGHT = 68;

workspaces/ballerina/data-mapper/src/components/Diagram/Diagram.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { DataMapperCanvasContainerWidget } from './Canvas/DataMapperCanvasContai
3636
import { DataMapperCanvasWidget } from './Canvas/DataMapperCanvasWidget';
3737
import { DefaultState as LinkState } from './LinkState/DefaultState';
3838
import { DataMapperNodeModel } from './Node/commons/DataMapperNode';
39-
import { LinkConnectorNode, QueryExprConnectorNode } from './Node';
39+
import { ClauseConnectorNode, LinkConnectorNode, QueryExprConnectorNode } from './Node';
4040
import { OverlayLayerFactory } from './OverlayLayer/OverlayLayerFactory';
4141
import { OverriddenLinkLayerFactory } from './OverriddenLinkLayer/LinkLayerFactory';
4242
import { useDiagramModel, useFocusLinkedNodes, useRepositionedNodes } from './hooks';
@@ -87,6 +87,7 @@ function initDiagramEngine() {
8787
engine.getNodeFactories().registerFactory(new Nodes.QueryOutputNodeFactory());
8888
engine.getNodeFactories().registerFactory(new Nodes.LinkConnectorNodeFactory());
8989
engine.getNodeFactories().registerFactory(new Nodes.QueryExprConnectorNodeFactory());
90+
engine.getNodeFactories().registerFactory(new Nodes.ClauseConnectorNodeFactory());
9091
engine.getNodeFactories().registerFactory(new Nodes.DataImportNodeFactory());
9192
engine.getNodeFactories().registerFactory(new Nodes.EmptyInputsNodeFactory());
9293

@@ -157,10 +158,10 @@ function DataMapperDiagram(props: DataMapperDiagramProps): React.ReactElement {
157158
if (!isFetching && engine.getModel()) {
158159
const modelNodes = engine.getModel().getNodes();
159160
const nodesToUpdate = modelNodes.filter(node =>
160-
node instanceof LinkConnectorNode || node instanceof QueryExprConnectorNode
161+
node instanceof LinkConnectorNode || node instanceof QueryExprConnectorNode || node instanceof ClauseConnectorNode
161162
);
162163

163-
nodesToUpdate.forEach((node: LinkConnectorNode | QueryExprConnectorNode) => {
164+
nodesToUpdate.forEach((node: LinkConnectorNode | QueryExprConnectorNode | ClauseConnectorNode) => {
164165
const targetPortPosition = node.targetMappedPort?.getPosition();
165166
if (targetPortPosition) {
166167
node.setPosition(targetPortPosition.x - 155, targetPortPosition.y - 6.5);

0 commit comments

Comments
 (0)