Skip to content

Commit 6b5510c

Browse files
authored
Feature/introduce data mapping (#746)
* implement data mapping functionality * WIP AI integration for data mapping * add data mapping to toolbar * get data mapping working also for complex use case * apply formatting changes * lot of progress on data mapping and now also supporting alternative method: JSONata * WIP error handling for LLM results * minor mapping improvements * rename simple schema mapping to STML (Schema Transformation Mapping Language) * fix bugs and make improvements for data mapping dialog: always show editor, also apply mapping if written by human and not generated * remove duplication * fix file name * fix test * apply formatting changes --------- Co-authored-by: Logende <[email protected]>
1 parent 8239402 commit 6b5510c

26 files changed

Lines changed: 2275 additions & 7 deletions

meta_configurator/package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

meta_configurator/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"json-schema-merge-allof": "^0.8.1",
4949
"json-schema-resolver": "^2.0.0",
5050
"json-schema-to-typescript": "^13.0.1",
51+
"jsonata": "^2.0.6",
5152
"lodash": "^4.17.21",
5253
"node-fetch": "^3.3.1",
5354
"pinia": "^2.0.36",
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
<script setup lang="ts">
2+
import {ref, computed, watch, type Ref, onMounted, nextTick} from 'vue';
3+
import Dialog from 'primevue/dialog';
4+
import InputText from 'primevue/inputtext';
5+
import Button from 'primevue/button';
6+
import Select from 'primevue/select';
7+
import Divider from 'primevue/divider';
8+
import Message from 'primevue/message';
9+
import ApiKey from '@/components/panels/ai-prompts/ApiKey.vue';
10+
import {SessionMode} from '@/store/sessionMode';
11+
import {getDataForMode} from '@/data/useDataLink';
12+
import {DataMappingServiceStml} from '@/data-mapping/stml/dataMappingServiceStml';
13+
import {DataMappingServiceJsonata} from '@/data-mapping/jsonata/dataMappingServiceJsonata';
14+
import type {DataMappingService} from '@/data-mapping/dataMappingService';
15+
import type {Editor} from 'brace';
16+
import * as ace from 'brace';
17+
import {setupAceProperties} from '@/components/panels/shared-components/aceUtils';
18+
import {useSettings} from '@/settings/useSettings';
19+
import {useDebounceFn} from '@vueuse/core';
20+
import ProgressSpinner from 'primevue/progressspinner';
21+
22+
const showDialog = ref(false);
23+
const editor_id = 'data-mapping-' + Math.random();
24+
const editorInitialized: Ref<boolean> = ref(false);
25+
const editor: Ref<Editor | null> = ref(null);
26+
const input = ref({});
27+
const result = ref('');
28+
const resultIsValid = ref(false);
29+
const statusMessage = ref('');
30+
const errorMessage = ref('');
31+
const userComments = ref('');
32+
const isLoadingMapping = ref(false);
33+
34+
const settings = useSettings();
35+
36+
const mappingServiceTypes = ['Advanced (JSONata)', 'SimpleTransformationMappingLanguage (STML)'];
37+
38+
const mappingServiceWarnings = [
39+
'The JSONata mapping service is very expressive flexible, but may generate invalid mappings for complex inputs, which have to manually be corrected.',
40+
'The STML mapping service usually generates valid mappings, but it can perform only simple source to target path mappings and value transformations. WARNING: It supports executing arbitrary JavaScript functions as transformations, which may lead to security issues if the input is not properly sanitized.',
41+
];
42+
43+
const selectedMappingServiceType: Ref<string> = ref(mappingServiceTypes[0]);
44+
45+
const mappingService: Ref<DataMappingService> = computed(() => {
46+
if (selectedMappingServiceType.value === 'SimpleTransformationMappingLanguage (STML)') {
47+
return new DataMappingServiceStml();
48+
}
49+
if (selectedMappingServiceType.value === 'Advanced (JSONata)') {
50+
return new DataMappingServiceJsonata();
51+
}
52+
// Add other mapping service types here
53+
throw new Error('Invalid mapping service type');
54+
});
55+
56+
const mappingServiceWarning: Ref<string> = computed(() => {
57+
const index = mappingServiceTypes.indexOf(selectedMappingServiceType.value);
58+
return mappingServiceWarnings[index] || '';
59+
});
60+
61+
onMounted(() => {
62+
// when a new result is generated: replace the editor content with it
63+
watch(
64+
() => result.value,
65+
newValue => {
66+
if (newValue.length > 0) {
67+
editor.value = ace.edit(editor_id);
68+
editor.value?.setValue(newValue, -1);
69+
}
70+
}
71+
);
72+
});
73+
74+
watch(showDialog, async visible => {
75+
// when the dialog turns visible, initialize the editor
76+
if (visible) {
77+
await nextTick(); // Wait until dialog content is rendered
78+
initializeEditor();
79+
80+
if (result.value.length > 0) {
81+
editor.value?.setValue(result.value, -1);
82+
}
83+
}
84+
});
85+
86+
function openDialog() {
87+
// when the dialog is opened, reset old values and load the current input data into the component, sanitize it
88+
resetDialog();
89+
input.value = getDataForMode(SessionMode.DataEditor).data.value;
90+
input.value = mappingService.value.sanitizeInputDocument(input.value);
91+
showDialog.value = true;
92+
}
93+
94+
function hideDialog() {
95+
showDialog.value = false;
96+
}
97+
98+
function resetDialog() {
99+
statusMessage.value = '';
100+
errorMessage.value = '';
101+
userComments.value = '';
102+
input.value = {};
103+
result.value = '';
104+
resultIsValid.value = false;
105+
}
106+
107+
function initializeEditor() {
108+
const container = document.getElementById(editor_id);
109+
110+
if (!container) {
111+
console.log('Unable to initialize editor because element is not found.');
112+
return;
113+
}
114+
115+
// Destroy any existing editor if present
116+
if (editor.value) {
117+
editor.value.destroy();
118+
editor.value.container.innerHTML = ''; // Clean up old editor DOM
119+
editor.value = null;
120+
editorInitialized.value = false;
121+
}
122+
123+
editor.value = ace.edit(editor_id);
124+
setupAceProperties(editor.value, settings.value);
125+
126+
editorInitialized.value = true;
127+
128+
editor.value.on(
129+
'change',
130+
useDebounceFn(() => {
131+
const editorContent = editor.value?.getValue();
132+
if (editorContent) {
133+
validateConfig(editorContent, input.value);
134+
}
135+
}, 100)
136+
);
137+
}
138+
139+
function validateConfig(config: string, input: any) {
140+
const validationResult = mappingService.value.validateMappingConfig(config, input);
141+
if (!validationResult.success) {
142+
errorMessage.value = validationResult.message;
143+
statusMessage.value = '';
144+
resultIsValid.value = false;
145+
} else {
146+
errorMessage.value = '';
147+
resultIsValid.value = true;
148+
}
149+
}
150+
151+
function generateMappingSuggestion() {
152+
isLoadingMapping.value = true;
153+
const targetSchema = getDataForMode(SessionMode.SchemaEditor).data.value;
154+
mappingService.value
155+
.generateMappingSuggestion(input.value, targetSchema, userComments.value)
156+
.then(res => {
157+
result.value = res.config;
158+
if (res.success) {
159+
statusMessage.value = res.message;
160+
errorMessage.value = '';
161+
} else {
162+
statusMessage.value = '';
163+
errorMessage.value = res.message;
164+
}
165+
isLoadingMapping.value = false;
166+
validateConfig(res.config, input.value);
167+
});
168+
}
169+
170+
function performMapping() {
171+
const config = editor.value?.getValue();
172+
if (!config) {
173+
errorMessage.value = 'No mapping configuration available.';
174+
statusMessage.value = '';
175+
return;
176+
}
177+
178+
mappingService.value.performDataMapping(input.value, config).then(res => {
179+
if (res.success) {
180+
statusMessage.value = res.message;
181+
errorMessage.value = '';
182+
// write the result data to the data editor
183+
getDataForMode(SessionMode.DataEditor).setData(res.resultData);
184+
hideDialog();
185+
} else {
186+
statusMessage.value = '';
187+
errorMessage.value = res.message;
188+
}
189+
});
190+
}
191+
192+
defineExpose({show: openDialog, close: hideDialog});
193+
</script>
194+
195+
<template>
196+
<Dialog
197+
v-model:visible="showDialog"
198+
header="Convert Data to Target Schema"
199+
:modal="true"
200+
:style="{width: '50vw'}">
201+
<div class="space-y-4">
202+
<ApiKey />
203+
204+
<Message severity="warn" v-if="mappingServiceWarning.length">
205+
<span v-html="mappingServiceWarning"></span>
206+
</Message>
207+
208+
<p class="text-sm text-gray-700">
209+
This tool converts the data from the <strong>Data Editor</strong> to match the schema
210+
defined in the <strong>Schema Editor</strong>. You can optionally provide extra instructions
211+
below to guide the mapping.
212+
</p>
213+
214+
<div>
215+
<label for="userComments" class="block font-semibold mb-1">Additional Mapping Hints</label>
216+
<InputText
217+
id="userComments"
218+
v-model="userComments"
219+
class="w-full"
220+
placeholder="e.g., rename fields, format dates..." />
221+
</div>
222+
223+
<div class="flex items-center gap-2">
224+
<label class="font-semibold">Mapping Method</label>
225+
<Select
226+
v-model="selectedMappingServiceType"
227+
:options="mappingServiceTypes"
228+
class="flex-1" />
229+
</div>
230+
231+
<Button
232+
label="Generate Suggestion"
233+
icon="pi pi-wand"
234+
@click="generateMappingSuggestion"
235+
class="w-full" />
236+
<ProgressSpinner v-if="isLoadingMapping" />
237+
238+
<div class="mt-6">
239+
<Divider />
240+
<label :for="editor_id" class="block font-semibold mb-2">Mapping Configuration</label>
241+
<div class="border rounded h-72 overflow-hidden">
242+
<div :id="editor_id" class="h-full w-full" />
243+
</div>
244+
<Button
245+
v-if="resultIsValid"
246+
label="Perform Mapping"
247+
icon="pi pi-play"
248+
class="mt-4 w-full"
249+
@click="performMapping" />
250+
</div>
251+
252+
<Message severity="info" v-if="statusMessage.length">{{ statusMessage }}</Message>
253+
<Message severity="error" v-if="errorMessage.length">
254+
<span v-html="errorMessage"></span>
255+
</Message>
256+
</div>
257+
</Dialog>
258+
</template>
259+
260+
<style scoped>
261+
label {
262+
font-size: 0.9rem;
263+
}
264+
</style>

meta_configurator/src/components/panels/ai-prompts/aiPromptUtils.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
export function fixAndParseGeneratedJson(json: string): any {
2-
if (json.startsWith('```json\n') && json.endsWith('```')) {
3-
json = json.substring(8, json.length - 3);
4-
}
2+
json = fixGeneratedExpression(json);
53

64
try {
75
return JSON.parse(json);
@@ -16,6 +14,23 @@ export function fixAndParseGeneratedJson(json: string): any {
1614
}
1715
}
1816

17+
export function fixGeneratedExpression(
18+
json: string,
19+
expressionTypes: string | string[] = 'json'
20+
): string {
21+
for (const expressionType of Array.isArray(expressionTypes)
22+
? expressionTypes
23+
: [expressionTypes]) {
24+
if (json.toLowerCase().startsWith(`\`\`\`${expressionType}`) && json.endsWith('```')) {
25+
json = json.substring(3 + expressionType.length, json.length - 3);
26+
}
27+
}
28+
if (json.startsWith('```') && json.endsWith('```')) {
29+
json = json.substring(3, json.length - 3);
30+
}
31+
return json;
32+
}
33+
1934
function hasMoreOpeningBrackets(input: string): boolean {
2035
const openingCount = (input.match(/\{/g) || []).length;
2136
const closingCount = (input.match(/\}/g) || []).length;

meta_configurator/src/components/toolbar/Toolbar.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import CodeGenerationDialog from '@/components/dialogs/code-generation/CodeGener
1515
import FetchedSchemasSelectionDialog from '@/components/dialogs/FetchedSchemasSelectionDialog.vue';
1616
import UrlInputDialog from '@/components/dialogs/UrlInputDialog.vue';
1717
import TopToolbar from '@/components/toolbar/TopToolbar.vue';
18+
import DataMappingDialog from '@/components/dialogs/data-mapping/DataMappingDialog.vue';
1819
1920
const props = defineProps<{
2021
currentMode: SessionMode;
@@ -60,6 +61,10 @@ function showSnapshotDialog() {
6061
snapshotDialog.value?.show();
6162
}
6263
64+
function showDataMappingDialog() {
65+
dataMappingDialog.value?.show();
66+
}
67+
6368
async function showSchemaStoreDialog(): Promise<void> {
6469
try {
6570
// Wait for the fetch to complete
@@ -90,6 +95,7 @@ const snapshotDialog = ref();
9095
const codeGenerationDialog = ref();
9196
const fetchedSchemasSelectionDialog = ref();
9297
const urlInputDialog = ref();
98+
const dataMappingDialog = ref();
9399
94100
defineExpose({
95101
showInitialSchemaDialog,
@@ -111,6 +117,8 @@ defineExpose({
111117

112118
<UrlInputDialog ref="urlInputDialog" />
113119

120+
<DataMappingDialog ref="dataMappingDialog" />
121+
114122
<AboutDialog
115123
:visible="showAboutDialog"
116124
@update:visible="newValue => (showAboutDialog = newValue)" />
@@ -124,6 +132,7 @@ defineExpose({
124132
@show-schemastore-dialog="() => showSchemaStoreDialog()"
125133
@show-import-csv-dialog="() => showCsvImportDialog()"
126134
@show-snapshot-dialog="() => showSnapshotDialog()"
135+
@show-data-mapping-dialog="() => showDataMappingDialog()"
127136
@mode-selected="newMode => emit('mode-selected', newMode)" />
128137
</template>
129138

0 commit comments

Comments
 (0)