diff --git a/src/components/MapView.vue b/src/components/MapView.vue index 9cbf88d88c469d86e68808e55164ac1b95bf654a..604d2506f93804fe3d83a6596f11994b9c12a0e9 100644 --- a/src/components/MapView.vue +++ b/src/components/MapView.vue @@ -1,6 +1,6 @@ <script setup lang="ts"> -import {ConceptMapping, Mappings, MitMDataType} from "@/services/api" +import {ConceptMapping, Mappings} from "@/services/api" import {useSelectionStore} from "@/services/selectionStore"; import {computed, reactive, ref, watch} from "vue"; import TableSelector from "@/components/helpers/TableSelector.vue"; @@ -13,7 +13,13 @@ import {ConceptMappingEntry, useMappingStore} from "@/services/mappingStore"; import InfoDialog from "@/components/helpers/InfoDialog.vue"; import {anyTIDtoTID, harmonizeToObj, rules} from "@/services/utils"; import RelationsColumnMapper from "@/components/subcomponents/map/RelationsColumnMapper.vue"; -import {ForeignRelationMappings, WithinTableMappings} from "@/services/mappingUtils" +import { + colMapFromNames, + ForeignRelationMappings, + suggestCol, + TypedColumnSelection, + WithinTableMappings +} from "@/services/mappingUtils" const selection = useSelectionStore() const mappings = useMappingStore() @@ -31,7 +37,7 @@ const kindCol = ref<string>() const idCols = ref({} as WithinTableMappings) const inlineCols = ref({} as WithinTableMappings) const foreignRelationMappings = ref({} as ForeignRelationMappings) -const attributeCols = ref({} as { [key: string]: { selected: boolean, declaredType: MitMDataType } }) +const attributeCols = ref({} as TypedColumnSelection) const {columnList} = useColumnsOfTableWithProbe(selectedTable) @@ -72,18 +78,7 @@ const isValid = computed(() => { && (Object.keys(cr.to_one).length == 0 || formvalidity.fkRelationCols) }) -function colMapFromNames(columns?: ExtendedColumnListItem[], nameColumnMapping?: { [key: string]: string }): { - [key: string]: ExtendedColumnListItem -} { - if (!nameColumnMapping) return {} - else return Object.fromEntries(Object.entries(nameColumnMapping).map(([k, v]) => [k, columns?.find(item => item.name.toLowerCase() === v.toLowerCase())]).filter(([k, v]) => !!v)) -} - -function suggestCol(columns?: ExtendedColumnListItem[], name?: string, mitmType?: MitMDataType) { - return columns?.find(item => item.name.toLowerCase() === name?.toLowerCase() && item.inferredType === mitmType) ?? null -} - -function suggestTypeCol(columns?: ExtendedColumnListItem[]) { +function suggestTypeCol(columns?: ExtendedColumnListItem[]): ExtendedColumnListItem | null { const tc = !!recreatedMapping.value ? recreatedMapping.value.type_col : currentConceptProperties.value?.typing_concept return suggestCol(columns, tc, "text") } @@ -115,7 +110,7 @@ function suggestInlineColMap(columns?: ExtendedColumnListItem[]) { } function suggestFKColMaps(columns?: ExtendedColumnListItem[]): { - [fkRelName: string]: { [nameToMap: string]: ExtendedColumnListItem } + [fkRelName: string]: { [nameToMap: string]: ExtendedColumnListItem | null } } { if (!!recreatedMapping.value?.foreign_relations) return Object.fromEntries(Object.entries(recreatedMapping.value.foreign_relations).map(([fkRelName, fkRel]) => [fkRelName, colMapFromNames(columns, harmonizeToObj(fkRel.fk_columns))])) const mitmDef = selectedMitMDef.value @@ -144,7 +139,7 @@ function suggestForeignRelationsMappings(columns?: ExtendedColumnListItem[]): Fo }])) } -function suggestAttributeColMap(columns?: ExtendedColumnListItem[], preselect?) { +function suggestAttributeColMap(columns?: ExtendedColumnListItem[], preselect?): TypedColumnSelection { if (!!recreatedMapping.value?.attributes) { const attributes = recreatedMapping.value.attributes const attributeDTypes = recreatedMapping.value.attribute_dtypes @@ -164,17 +159,18 @@ watch([columnList, currentConceptRelations], () => { console.log('Updating concept mapping suggestions.' + (recreatedMapping.value ? ' (recreating existing mapping)' : '')) const columns = columnList.value - typeCol.value = suggestTypeCol(columns)?.name - kindCol.value = suggestKindCol(columns)?.name + typeCol.value = suggestTypeCol(columns)?.name ?? null + kindCol.value = suggestKindCol(columns)?.name ?? null idCols.value = suggestIdColMap(columns) inlineCols.value = suggestInlineColMap(columns) foreignRelationMappings.value = suggestForeignRelationsMappings(columns) - const wasPreviouslySuggested = c => (c === typeCol.value || c === kindCol.value || Object.values(idCols.value).some(item => c === item.name) || Object.values(inlineCols.value).some(item => c === item.name) || Object.values(foreignRelationMappings.value).some(fkMapping => Object.values(fkMapping.fk_columns).some(item => c === item.name))) + const wasPreviouslySuggested = c => (c === typeCol.value || c === kindCol.value || Object.values(idCols.value).some(item => !!item && c === item.name) || Object.values(inlineCols.value).some(item => !!item && c === item.name) || Object.values(foreignRelationMappings.value).some(fkMapping => Object.values(fkMapping.fk_columns).some(item => !!item && c === item.name))) attributeCols.value = suggestAttributeColMap(columns, colItem => !wasPreviouslySuggested(colItem.name)) }, {flush: "post"}) async function onSubmit() { + if(!isValid.value) return loading.value = true const identity_columns = {} @@ -219,7 +215,7 @@ async function onSubmit() { if (!!kindCol.value) { mapping["kind_col"] = kindCol.value } - console.log("Current MitM Def:\n", selectedMitM.value) + console.log("Current MitM Def:\n", selectedMitMDef.value) const res = await mappings.saveMapping(mapping) loading.value = false @@ -297,7 +293,7 @@ const expanded = ref([0]) :readonly="!!recreatedMapping"></InlineColumnMapper> </template> - <template v-if="!!currentConceptRelations && Object.keys(currentConceptRelations.to_one).length > 0"> + <template v-if="!!currentConceptRelations && Object.keys(foreignRelationMappings).length > 0"> <RelationsColumnMapper class="ma-2 pa-2" title="Foreign Relations" :mitm-def="selectedMitMDef" :base-table="selectedTable" diff --git a/src/components/helpers/TimeRangeInput.vue b/src/components/helpers/TimeRangeInput.vue index 72717846a428f5d3794c6ba4321dcc25f623e3e4..412807538b618dce0a2447a0eb27a8a8957a1a30 100644 --- a/src/components/helpers/TimeRangeInput.vue +++ b/src/components/helpers/TimeRangeInput.vue @@ -11,22 +11,28 @@ const dateRange = ref() const startTime = ref("00:00:00") const endTime = ref("00:00:00") -watch([dateRange, startTime, endTime], ([dr, st, et]) => { - if (!!dr && dr.length >= 1) { - const ds = DateTime.fromJSDate(dr[0].getDate(), {zone: props.timezone}) - const de = DateTime.fromJSDate(dr.slice(-1)[0].getDate(), {zone: props.timezone}) - if (!!st) { - const ts = DateTime.fromISO(st) - startDateTime.value = ds.set({hour: ts.hour, minute: ts.minute, second: ts.second}) - } - if (!!et) { - const te = DateTime.fromISO(et) - endDateTime.value = de.set({hour: te.hour, minute: te.minute, second: te.second}) +function time(s: string): { hour: number, minute: number, second: number } { + const [hour, minute, second] = s.split(":").map(v => Number(v)) + return {hour, minute, second} +} + +function pain(d: Date, t: string, tz: Zone): DateTime | null { + if (!!d && !!t) + return DateTime.fromObject({ + year: d.getFullYear(), + month: 1 + d.getMonth(), // lol + day: d.getDate(), + }, {zone: tz}).set(time(t)) + else return null +} - } else { - startDateTime.value = null - endDateTime.value = null - } +watch([dateRange, startTime, endTime, props.timezone], () => { + const dr = dateRange.value + const st = startTime.value + const et = endTime.value + if (!!dr && dr.length >= 1) { + startDateTime.value = pain(dr[0], st, props.timezone) + endDateTime.value = pain(dr.slice(-1)[0], et, props.timezone) } }) diff --git a/src/components/subcomponents/map/AttributeSelector.vue b/src/components/subcomponents/map/AttributeSelector.vue index 94fa5eded597d3db8c45af521f52b1d81540e4ca..4d10e5519aae41f3df2fb23a02e0d77e25fad752 100644 --- a/src/components/subcomponents/map/AttributeSelector.vue +++ b/src/components/subcomponents/map/AttributeSelector.vue @@ -1,6 +1,7 @@ <script setup lang="ts"> import {ExtendedColumnListItem, useMitMTypes} from "@/services/convenienceStore"; import {MitMDataType} from "@/services/api"; +import {TypedColumnSelection} from "@/services/mappingUtils"; const props = withDefaults(defineProps<{ columnList: ExtendedColumnListItem[], @@ -10,9 +11,7 @@ const props = withDefaults(defineProps<{ readonly: false }) -const attributeDeclarations = defineModel<{ - [key: string]: { selected: boolean, declaredType: MitMDataType } -}>({required: true}) +const attributeDeclarations = defineModel<TypedColumnSelection>({required: true}) const {mitmTypes} = useMitMTypes() @@ -20,7 +19,7 @@ const {mitmTypes} = useMitMTypes() <template> <v-card variant="flat" title="Attributes"> - <v-table density="compact"> + <v-table density="compact" max-height="400"> <thead> <tr> <th></th> @@ -35,7 +34,7 @@ const {mitmTypes} = useMitMTypes() <template v-if="item.name in attributeDeclarations"> <tr> <td> - <v-checkbox class="align-self-center" v-model="attributeDeclarations[item.name].selected" + <v-checkbox v-model="attributeDeclarations[item.name].selected" :disabled="props.readonly"></v-checkbox> </td> <td>{{ item.name }}</td> diff --git a/src/components/subcomponents/map/InlineColumnMapper.vue b/src/components/subcomponents/map/InlineColumnMapper.vue index 9edd37a334b3cdc83ea8e0ce18e9eb743b759016..785c4839cb68afd2f4f63c19f9b7faeea6ef429a 100644 --- a/src/components/subcomponents/map/InlineColumnMapper.vue +++ b/src/components/subcomponents/map/InlineColumnMapper.vue @@ -1,20 +1,21 @@ <script setup lang="ts"> import {ExtendedColumnListItem} from "@/services/convenienceStore"; -import {MitMDataType, MitMDefinition} from "@/services/api"; -import {reactive, ref, shallowReactive, watch, watchEffect} from "vue"; +import {MitMDefinition} from "@/services/api"; +import {getRequiredType, WithinTableMappings} from "@/services/mappingUtils"; +import {ref, watch, watchEffect} from "vue"; const props = defineProps<{ mitmDef: MitMDefinition toMap: { - [name: string]: string; + [name: string]: string }, columnList: ExtendedColumnListItem[], title: string, readonly?: boolean }>() -const mappedColumns = defineModel<{ [key: string]: ExtendedColumnListItem }>('mappedColumns', {required: true}) +const mappedColumns = defineModel<WithinTableMappings>('mappedColumns', {required: true}) const isValid = defineModel('isValid', {type: Boolean, default: false, required: false}) const overrides = ref([]) @@ -25,13 +26,9 @@ watch(props.columnList, () => { watchEffect(() => { const toSkip = overrides.value const filtered = Object.entries(mappedColumns.value).filter(([name, mc]) => !toSkip.some(n => n === name)) - isValid.value = filtered.every(([name, mc]) => getRequiredType(props.toMap[name]) === mc.inferredType) + isValid.value = !!props.mitmDef && filtered.every(([name, mc]) => !!mc && getRequiredType(props.mitmDef, props.toMap[name]) === mc.inferredType) }) -function getRequiredType(concept: string): MitMDataType { - return props.mitmDef.weak_concepts[concept] -} - </script> <template> @@ -54,18 +51,21 @@ function getRequiredType(concept: string): MitMDataType { <td>{{ name }}</td> <td>{{ props.toMap[name].toUpperCase() }}</td> <td> - <v-select :items="props.columnList" v-model="mappedColumns[name]" return-object variant="plain" :disabled="props.readonly"></v-select> + <v-select :items="props.columnList" v-model="mappedColumns[name]" return-object variant="plain" + :disabled="props.readonly"></v-select> </td> <td> - {{mappedColumns[name]?.sqlType}} + {{ mappedColumns[name]?.sqlType }} </td> <td> - <v-chip :color="getRequiredType(props.toMap[name]) === mappedColumns[name]?.inferredType ? 'green' : 'red'"> + <v-chip :color="getRequiredType(props.mitmDef, props.toMap[name]) === mappedColumns[name]?.inferredType ? 'green' : 'red'"> {{ mappedColumns[name]?.inferredType }} </v-chip> </td> - <td>{{ getRequiredType(props.toMap[name]) }}</td> - <td><v-checkbox-btn v-model="overrides" :value="name" :disabled="props.readonly"></v-checkbox-btn></td> + <td>{{ getRequiredType(props.mitmDef, props.toMap[name]) }}</td> + <td> + <v-checkbox-btn v-model="overrides" :value="name" :disabled="props.readonly"></v-checkbox-btn> + </td> </tr> </template> </tbody> diff --git a/src/components/subcomponents/map/PresetMenu.vue b/src/components/subcomponents/map/PresetMenu.vue index a1d0f7ed9089c92ecfb3d45e997eaabe98a67fc7..8f02972898bde6b3064d389b1b596785ddb2440a 100644 --- a/src/components/subcomponents/map/PresetMenu.vue +++ b/src/components/subcomponents/map/PresetMenu.vue @@ -64,8 +64,8 @@ async function fileDroppedIn(files) { </v-list-item> <v-list-subheader>Recreate Preset</v-list-subheader> <v-list-item> - <v-file-input variant="plain" hint="Drop a preset definition file here" - @update:model-value="fileDroppedIn" label="Upload preset definition"></v-file-input> + <v-file-input variant="plain" tooltip="Drop a preset definition file here" + @update:model-value="fileDroppedIn" label="Upload Preset Definition"></v-file-input> </v-list-item> <v-list-item v-for="item in presets" :title="item.props.title" @click="() => loadPreset(item.preset)"> </v-list-item> diff --git a/src/components/subcomponents/map/RelationsColumnMapper.vue b/src/components/subcomponents/map/RelationsColumnMapper.vue index 34ac1bece065d4f8db0c3c78244f85836b839f22..530ef544fbad78a67ed3971fc01f605a1f60375b 100644 --- a/src/components/subcomponents/map/RelationsColumnMapper.vue +++ b/src/components/subcomponents/map/RelationsColumnMapper.vue @@ -1,14 +1,14 @@ <script setup lang="ts"> import {ExtendedColumnListItem} from "@/services/convenienceStore"; -import {Mappings, MitMDataType, MitMDefinition, TableIdentifier, TableMetaInfoResponse} from "@/services/api"; -import {Ref, ref, watch, watchEffect} from "vue"; +import {Mappings, MitMDefinition, TableIdentifier, TableMetaInfoResponse} from "@/services/api"; +import {reactive, ref, Ref, unref, watchEffect} from "vue"; import {useMappingStore} from "@/services/mappingStore"; import {storeToRefs} from "pinia"; import TableSelector from "@/components/helpers/TableSelector.vue"; import {useMainStore} from "@/services/mainStore"; -import {anyTIDtoTID, rules} from "@/services/utils" -import {ForeignRelationMappings} from "@/services/mappingUtils" +import {isSameTID, rules} from "@/services/utils" +import {ForeignRelationMappings, getRequiredType} from "@/services/mappingUtils" const store = useMainStore() @@ -25,8 +25,8 @@ const props = defineProps<{ const foreignRelations: Ref<ForeignRelationMappings> = defineModel<ForeignRelationMappings>('foreignRelations', {required: true}) const isValid = defineModel('isValid', {type: Boolean, default: false, required: false}) -const fkValidity = ref({requiredMappingExistence: {}, fkExistence: {}}) -const overrides = ref<{ [fkRelName: string]: Ref<[]> }>({}) +const fkValidity = reactive({requiredMappingExistence: {}, fkExistence: {}}) +const overrides = ref<{ [fkRelName: string]: string[] }>({}) const fkRelationTargets = ref<{ [fkRelName: string]: { @@ -39,31 +39,29 @@ const fkRelationTargets = ref<{ }>({}) watchEffect(() => { - // const frs = foreignRelations.value - let temp = {} - if (!!props.toMap && !!props.mitmDef ) // && !!frs && Object.keys(frs).every(fkRelName => fkRelName in props.toMap) - temp = Object.fromEntries(Object.keys(foreignRelations.value).map(fkRelName => { - const fkRelInfo = props.toMap[fkRelName] + if (!!props.toMap && !!props.mitmDef) + fkRelationTargets.value = Object.fromEntries(Object.entries(props.toMap).map(([fkRelName, fkRelInfo]) => { const targetIdentity = props.mitmDef.concept_relations[fkRelInfo.target_concept].identity - const targetConcepts = Object.values(fkRelInfo.fk_relations).map(targetColName => targetIdentity[targetColName]) - const targetTypes = targetConcepts.map(getRequiredType) + const targetConcepts = Object.fromEntries(Object.entries(fkRelInfo.fk_relations).map(([nameToMap, targetColName]) => [nameToMap, targetIdentity[targetColName]])) + const targetTypes = Object.fromEntries(Object.entries(targetConcepts).map(([nameToMap, concept]) => [nameToMap, getRequiredType(props.mitmDef, concept)])) const namesToMap = Object.keys(fkRelInfo.fk_relations) return [fkRelName, { targetConcept: fkRelInfo.target_concept, fkLength: namesToMap.length, namesToMap, targetConcepts, - targetTypes + targetTypes, }] })) - fkRelationTargets.value = temp - overrides.value = !!props.toMap ? Object.fromEntries(Object.keys(props.toMap).map(fkRelName => [fkRelName, ref([])])) : {} + else + fkRelationTargets.value = {} +}) + +watchEffect(() => { + const fkRelTargets = fkRelationTargets.value + overrides.value = !!fkRelTargets ? Object.fromEntries(Object.keys(fkRelTargets).map(fkRelName => [fkRelName, []])) : {} }) -//watch(props.columnList, () => { -// overrides.value = [] -//}) -// watchEffect(() => { const toSkip = overrides.value @@ -71,136 +69,126 @@ watchEffect(() => { const fkRelTargets = fkRelationTargets.value if (!toSkip || !frs || !fkRelTargets) isValid.value = false else { - isValid.value = Object.entries(frs).map(([fkRelName, fkMapping]) => Object.entries(fkMapping.fk_columns) - .filter(([nameToMap]) => !toSkip[fkRelName] || !toSkip[fkRelName].some(n => n === nameToMap)) - .every(([nameToMap, selectedCol]) => fkRelTargets[fkRelName].targetTypes[nameToMap] === selectedCol.inferredType)).every(v => v) + isValid.value = Object.entries(fkRelTargets).every(([fkRelName, fkRelTarget]) => + fkRelName in frs + && Object.entries(frs[fkRelName].fk_columns) + .every(([nameToMap, selectedCol]) => !!selectedCol && (fkRelTarget.targetTypes[nameToMap] === selectedCol.inferredType || (fkRelName in toSkip && toSkip[fkRelName].some(n => n === nameToMap))))) + && Object.values(fkValidity.requiredMappingExistence).every(v => v) } -}) +}, {flush: "post"}) -watchEffect( () => { +watchEffect(() => { // check for existence of required concept mapping of selected table, and check for regular FK relation const frs = foreignRelations.value - console.log('validity 0') + const selectedTables = !!frs ? Object.fromEntries(Object.entries(frs).map(([fkRelName, fkMapping]) => [fkRelName, unref(fkMapping.referred_table)])) : {} const fkRelTargets = fkRelationTargets.value - const baseTable = props.baseTable + const baseTable = unref(props.baseTable) const tm: TableMetaInfoResponse = store.getTableMeta(baseTable) - const referredTables = !!frs ? Object.values(frs).map(fkMapping => fkMapping.referred_table) : [] - console.log('validity 1') - console.log(frs) - console.log(fkRelTargets) - console.log(baseTable) - console.log(referredTables) - console.log(tm?.foreign_key_constraints) - let temp = {requiredMappingExistence: {}, fkExistence: {}} if (!!frs && !!baseTable && !!tm && !!fkRelTargets) { - //baseTable = anyTIDtoTID(baseTable) - const requiredMappingExistence = Object.entries(frs).map(([fkRelName, fkMapping]) => - currentMappings.value.some(cme => cme.mapping.concept === fkRelTargets[fkRelName].targetConcept && cme.mapping.base_table === fkMapping.referred_table.value) - ) - const fkExistence = referredTables.map(referredTable => - baseTable.source === referredTable.source - && tm.foreign_key_constraints?.some(fkc => - { - const tt = anyTIDtoTID(fkc.target_table, baseTable.source) - return tt.schema === referredTable.schema && tt.name === referredTable.name - })) - temp = {requiredMappingExistence, fkExistence} + const toConsider = Object.entries(fkRelTargets).map(([fkRelName]) => [fkRelName, selectedTables[fkRelName]]) + fkValidity.fkExistence = Object.fromEntries(toConsider.map(([fkRelName, selectedTable]) => + [fkRelName, + !!selectedTable + && baseTable.source === selectedTable.source + && tm.foreign_key_constraints?.some(fkc => isSameTID(fkc.target_table, baseTable, baseTable.source))])) + + fkValidity.requiredMappingExistence = Object.fromEntries(toConsider.map(([fkRelName, selectedTable]) => + [fkRelName, + !!selectedTable + && currentMappings.value.some(cme => + cme.mapping.concept === fkRelTargets[fkRelName].targetConcept + && isSameTID(cme.mapping.base_table, selectedTable))] + )) + } else { + fkValidity.fkExistence = {} + fkValidity.requiredMappingExistence = {} } - console.log("validity 2", temp) - fkValidity.value = temp -}) - - -function getRequiredType(concept: string): MitMDataType { - return props.mitmDef.weak_concepts[concept] -} +}, {flush: "post"}) </script> <template> <v-card :title="props.title" variant="flat" :border="isValid ? null : 'lg error'"> <v-list> - <template v-for="(fkRelTargets, fkRelName, i) in fkRelationTargets" v-if="!!fkRelationTargets" :key="fkRelName"> - <v-list-item> - <v-card variant="flat" :title="`Relation: ${fkRelName}`"> - <v-table density="compact" class="my-2"> - <thead> - <tr> - <th>Target Concept</th> - <th>Target Table</th> - <th>Foreign Key Relationship</th> - <th>Mapping Existence</th> - </tr> - </thead> - <tbody> + <v-list-item v-for="(fkRelTarget, fkRelName) in fkRelationTargets" v-if="!!fkRelationTargets" :key="fkRelName"> + <v-card variant="flat" :title="`Relation: ${fkRelName}`" v-if="fkRelName in foreignRelations"> + <v-table density="compact" class="my-2"> + <thead> + <tr> + <th>Target Concept</th> + <th>Target Table</th> + <th>Foreign Key Relationship</th> + <th>Mapping Existence</th> + </tr> + </thead> + <tbody> + <tr> + <td>{{ fkRelTarget.targetConcept.toUpperCase() }}</td> + <td> + <TableSelector v-model:selected-table="foreignRelations[fkRelName].referred_table" + source-d-b="either" :rules="[rules.required]" + :disabled="props.readonly" hide-details class="my-2"></TableSelector> + </td> + <td> + <v-chip :color="fkValidity.fkExistence[fkRelName] ? 'green' : 'orange'"> + {{ fkValidity.fkExistence[fkRelName] ? 'FK is declared' : 'there is no FK to table' }} + </v-chip> + </td> + <td> + <v-chip :color="fkValidity.requiredMappingExistence[fkRelName] ? 'green' : 'red'"> + {{ + fkValidity.requiredMappingExistence[fkRelName] ? `table is mapped to target concept` : `table is not mapped to target concept` + }} + </v-chip> + </td> + </tr> + </tbody> + </v-table> + <v-table density="compact" class="my-2"> + <thead> + <tr> + <th>Target Column</th> + <th>Target Concept</th> + <th>Selected Column</th> + <th>Actual SQL Type</th> + <th>Inferred Type</th> + <th>Required Type</th> + <th>Ignore Type Check?</th> + </tr> + </thead> + <tbody> + <template v-for="(nameToMap, i) in fkRelTarget.namesToMap" :key="nameToMap"> <tr> - <td>{{ fkRelTargets.targetConcept.toUpperCase() }}</td> + <td>{{ nameToMap }}</td> + <td>{{ fkRelTarget.targetConcepts[nameToMap] }}</td> <td> - <TableSelector :selected-table="foreignRelations[fkRelName].referred_table" - source-d-b="either" :rules="[rules.required]" - :disabled="props.readonly" hide-details class="my-2"></TableSelector> + <v-select :items="props.columnList" v-model="foreignRelations[fkRelName].fk_columns[nameToMap]" + return-object variant="plain" + :rules="[rules.required]" + :disabled="props.readonly"> + </v-select> </td> <td> - <v-chip :color="fkValidity.fkExistence[fkRelName] ? 'green' : 'orange'"> - {{ fkValidity.fkExistence[fkRelName] ? 'FK is declared in DB' : 'No FK to table exists' }} - </v-chip> + {{ foreignRelations[fkRelName]?.fk_columns[nameToMap]?.sqlType }} </td> <td> - <v-chip :color="fkValidity.requiredMappingExistence[fkRelName] ? 'green' : 'red'"> - {{ - fkValidity.requiredMappingExistence[fkRelName] ? 'Target table is mapped as required' : 'Target table is not mapped to declared concept' - }} + <v-chip + :color="fkRelTarget.targetTypes[nameToMap] === foreignRelations[fkRelName]?.fk_columns[nameToMap]?.inferredType ? 'green' : 'red'"> + {{ foreignRelations[fkRelName]?.fk_columns[nameToMap]?.inferredType }} </v-chip> </td> + <td>{{ fkRelTarget.targetTypes[nameToMap] }}</td> + <td> + <v-checkbox-btn v-model="overrides[fkRelName]" :value="nameToMap" + :disabled="props.readonly"></v-checkbox-btn> + </td> </tr> - </tbody> - </v-table> - <v-table density="compact" class="my-2"> - <thead> - <tr> - <th>Target Column</th> - <th>Target Concept</th> - <th>Selected Column</th> - <th>Actual SQL Type</th> - <th>Inferred Type</th> - <th>Required Type</th> - <th>Ignore Type Check?</th> - </tr> - </thead> - <tbody> - <template v-for="(nameToMap, i) in fkRelTargets.namesToMap" v-if="fkRelName in foreignRelations" - :key="nameToMap"> - <tr> - <td>{{ nameToMap }}</td> - <td>{{ fkRelTargets.targetConcepts[i] }}</td> - <td> - <v-select :items="props.columnList" v-model="foreignRelations[fkRelName].fk_columns[nameToMap]" - return-object variant="plain" - :rules="[rules.required]" - :disabled="props.readonly"> - </v-select> - </td> - <td> - {{ foreignRelations[fkRelName]?.fk_columns[nameToMap]?.sqlType }} - </td> - <td> - <v-chip - :color="fkRelTargets.targetTypes[i] === foreignRelations[fkRelName]?.fk_columns[nameToMap]?.inferredType ? 'green' : 'red'"> - {{ foreignRelations[fkRelName]?.fk_columns[nameToMap]?.inferredType }} - </v-chip> - </td> - <td>{{ fkRelTargets.targetTypes[i] }}</td> - <td> - <v-checkbox-btn v-model="overrides[fkRelName]" :value="nameToMap" :disabled="props.readonly"></v-checkbox-btn> - </td> - </tr> - </template> - </tbody> - </v-table> - </v-card> - </v-list-item> - </template> + </template> + </tbody> + </v-table> + </v-card> + </v-list-item> </v-list> </v-card> diff --git a/src/components/subcomponents/transform/AdvancedRaw.vue b/src/components/subcomponents/transform/AdvancedRaw.vue index d47dcaeb0de8648daa578cf3236f0c76068a4e10..4d8feeba89c5bd841eb7f77b7e409639d789d538 100644 --- a/src/components/subcomponents/transform/AdvancedRaw.vue +++ b/src/components/subcomponents/transform/AdvancedRaw.vue @@ -1,26 +1,41 @@ <script setup lang="ts"> -import {ref} from "vue"; -import {Transforms, CompiledVirtualView} from "@/services/api"; +import {computed, ref, watchEffect} from "vue"; +import {Transforms} from "@/services/api"; import {jsonToPrettyStr} from "@/services/utils"; -const isValid = defineModel("isValid", {type: Boolean, default: false, required: false}) +const isValid = defineModel({type: Boolean, default: false, required: false}) const placeHolderTypedQuery: Transforms.TypedRawQuery = { columns: ["A", "B"], - column_dtypes: ["text", "numeric"], + column_dtypes: ["TEXT", "INTEGER"], dialect: "sqlite", compiled_sql: "SELECT 'Hi' AS A, 1 AS B FROM someSchema.someTable" } -const text = ref(jsonToPrettyStr(placeHolderTypedQuery)) +const text = ref<string>(jsonToPrettyStr(placeHolderTypedQuery)) +const typedQuery = computed<Transforms.TypedRawQuery | null>(() => { + try { + const tq = JSON.parse(text.value) + if (Object.keys(placeHolderTypedQuery).every(k => k in tq)) + return Object.fromEntries(Object.entries(tq).filter(([k]) => k in placeHolderTypedQuery)) + } catch (e) { + } + return null +}) + +watchEffect(() => { + isValid.value = !!typedQuery.value +}) -async function fileDroppedIn(files) { - if (files?.length > 0) { - const f = files[0] - const uploadedDefinition = JSON.parse(f) - if (!!uploadedDefinition) { +async function fileDroppedIn(file: File) { + if (!!file) { + try { + const content = await file.text() + const uploadedDefinition = JSON.parse(content) text.value = jsonToPrettyStr(uploadedDefinition) + } catch (e) { + console.log('Uploaded Typed Query was not valid JSON') } } } @@ -30,7 +45,10 @@ function resetAll() { } function createBase(): Transforms.Base { - return {operation: "raw", typed_query: text.value} as Transforms.RawCompiled + return { + operation: "raw", + typed_query: typedQuery.value + } as Transforms.RawCompiled } const createTransforms = () => [] as Transforms.Transform[] @@ -44,13 +62,19 @@ defineExpose({resetAll, createTransforms, createBase}) <v-row> <v-col> <v-file-input variant="outlined" prepend-inner-icon="mdi-code-json" - label="Upload Compiled View Definition" hide-details + label="Upload Typed Query Definition" hide-details @update:model-value="fileDroppedIn"></v-file-input> </v-col> </v-row> + <v-row justify="start" align="center"> + <v-col cols="auto"><strong>Definition</strong></v-col> + <v-col cols="auto"> + <v-chip :color="isValid ? 'green' : 'red'">{{ isValid ? "valid" : "invalid" }}</v-chip> + </v-col> + </v-row> <v-row> <v-col> - <v-textarea :text="text" hint="Enter Raw Typed Query"></v-textarea> + <v-textarea v-model="text" hint="Enter Typed Query Definition"></v-textarea> </v-col> </v-row> </v-container> diff --git a/src/components/subcomponents/transform/CreateVirtualView.vue b/src/components/subcomponents/transform/CreateVirtualView.vue index 98c709c44604c9977f2f2dbbbe36d6dd7b4fa3c4..51328bcca2c0e08447fb2590acc738ffc8de9cc4 100644 --- a/src/components/subcomponents/transform/CreateVirtualView.vue +++ b/src/components/subcomponents/transform/CreateVirtualView.vue @@ -1,6 +1,6 @@ <script setup lang="ts"> -import {computed, reactive, Ref, ref, watch} from "vue"; +import {computed, reactive, Ref, ref, unref, watch} from "vue"; import {TableIdentifier, Transforms, useAPI, VirtualViewCreation} from "@/services/api"; import {useMainStore} from "@/services/mainStore"; import {useSelectionStore} from "@/services/selectionStore"; @@ -48,11 +48,11 @@ const widgetComponents: { title: string, value: string, kind: WidgetKind }[] = [ const widgetInstances: { [key: string]: { ref: any, isValid: Ref<boolean>, kind: WidgetKind, cls: any } } = { - "editColumns": {cls: EditColumns, ref: ref(), isValid: ref<boolean>(false), kind: "transform"}, - "extractJson": {cls: ExtractJson, ref: ref(), isValid: ref<boolean>(false), kind: "transform"}, - "tableFilter": {cls: TableFilter, ref: ref(), isValid: ref<boolean>(false), kind: "transform"}, - "simpleJoin": {cls: SimpleJoin, ref: ref(), isValid: ref<boolean>(false), kind: "base"}, - "advancedRaw": {cls: AdvancedRaw, ref: ref(), isValid: ref<boolean>(false), kind: "advanced"}, + "editColumns": {cls: EditColumns, ref: ref(), isValid: ref(false), kind: "transform"}, + "extractJson": {cls: ExtractJson, ref: ref(), isValid: ref(false), kind: "transform"}, + "tableFilter": {cls: TableFilter, ref: ref(), isValid: ref(false), kind: "transform"}, + "simpleJoin": {cls: SimpleJoin, ref: ref(), isValid: ref(false), kind: "base"}, + "advancedRaw": {cls: AdvancedRaw, ref: ref(), isValid: ref(false), kind: "advanced"}, } const store = useMainStore() @@ -63,7 +63,7 @@ const {selectedTable} = storeToRefs(selection) const dialog = reactive({content: "", show: false, success: null, title: "Virtual View Creation"}) const loading = ref(false) -const mode = ref("transform") +const mode = ref<WidgetKind>("transform") const removeBaseTable = ref(false) const permitOverride = ref(true) const viewIdentifier = reactive<TableIdentifier>({source: "virtual", schema: "virtual", name: ""}) @@ -81,11 +81,11 @@ const widgetInstance = computed(() => widgetInstances[tab.value]) const widgetComponent = computed(() => widgetInstance.value.cls) -const isValid = computed(() => (mode.value !== "transform" || !!selectedTable.value) && !!viewIdentifier.schema && !!viewIdentifier.name && uniqueName()) +const isValid = computed(() => (mode.value !== "transform" || !!selectedTable.value) && !!viewIdentifier.schema && !!viewIdentifier.name && !!uniqueName()) const disabled = computed(() => { const selfValid = isValid.value - const widgetValid = widgetInstance.value?.isValid.value + const widgetValid = unref(widgetInstance.value?.isValid) return !selfValid || !widgetValid }) @@ -198,7 +198,7 @@ function resetWidget() { </v-tabs> <v-tabs-window v-model="tab" class="fill-height"> <v-tabs-window-item v-for="(item, key) in widgetInstances" :value="key" :key="key"> - <component class="mx-auto" :is="item.cls" v-model="item.isValid.value" :ref="el => item.ref = el" + <component class="mx-auto" :is="item.cls" @update:model-value="v => item.isValid.value = v" :ref="el => item.ref = el" :table="selectedTable"></component> </v-tabs-window-item> </v-tabs-window> diff --git a/src/components/subcomponents/transform/TimeFilter.vue b/src/components/subcomponents/transform/TimeFilter.vue index 7cabe8e1b954059867fbd16ac4b651601035ca9c..031e7685f1ccbc874c68eeff845dfc0bc5dad05c 100644 --- a/src/components/subcomponents/transform/TimeFilter.vue +++ b/src/components/subcomponents/transform/TimeFilter.vue @@ -32,12 +32,16 @@ const dateTimeCol = ref() const comparator = ref("after") const timeZone = ref(IANAZone.create("Europe/Berlin")) const dateTimePoint = ref<DateTime>(null) -const dateTimeRange = reactive({startDateTime: null as DateTime, endDateTime: null as DateTime}) +const startDateTime = ref<DateTime>(null) +const endDateTime = ref<DateTime>(null) watchEffect(() => { const dtCol = dateTimeCol.value const comp = comparator.value - isValid.value = !!dtCol && (!!dateTimePoint.value || (comp === "between" && !!dateTimeRange.startDateTime && !!dateTimeRange.endDateTime)) + const dtp = dateTimePoint.value + const sdt = startDateTime.value + const edt = endDateTime.value + isValid.value = !!dtCol && ((comp !== "between" && !!dtp) || (comp === "between" && !!sdt && !!edt)) }) function dtToCond(col: string, dt: DateTime, operator: Transforms.SimpleSQLOperator): Transforms.SimpleWhere { @@ -48,8 +52,7 @@ function createWheres(): Transforms.SimpleWhere[] { const dtCol = dateTimeCol.value const comp = comparator.value if (comp === "between") { - console.log(dateTimeRange) - return [dtToCond(dtCol, dateTimeRange.startDateTime, ">="), dtToCond(dtCol, dateTimeRange.endDateTime, "<=")] + return [dtToCond(dtCol, startDateTime.value, ">="), dtToCond(dtCol, endDateTime.value, "<=")] } else { return [dtToCond(dtCol, dateTimePoint.value, comp === "before" ? "<" : ">")] } @@ -81,12 +84,12 @@ defineExpose({ <v-row no-gutters class="d-flex flex-nowrap" justify="start"> <template v-if="comparator === 'between'"> <v-col class="ma-2" cols="12"> - <TimeRangeInput v-model:start-date-time="dateTimeRange.startDateTime" v-model:end-date-time="dateTimeRange.endDateTime" :timezone="timeZone.obj"></TimeRangeInput> + <TimeRangeInput v-model:start-date-time="startDateTime" v-model:end-date-time="endDateTime" :timezone="timeZone.value"></TimeRangeInput> </v-col> </template> <template v-else> <v-col class="ma-2" cols="12"> - <TimePointInput v-model="dateTimePoint" :timezone="timeZone.obj"></TimePointInput> + <TimePointInput v-model="dateTimePoint" :timezone="timeZone.value"></TimePointInput> </v-col> </template> </v-row> diff --git a/src/services/mappingUtils.ts b/src/services/mappingUtils.ts index f7a1d60153df55efd1c3c4c94b812df8b09b2696..e92435014a450d05f08b99339b54af353dd62480 100644 --- a/src/services/mappingUtils.ts +++ b/src/services/mappingUtils.ts @@ -1,11 +1,27 @@ import {ExtendedColumnListItem} from "@/services/convenienceStore"; -import {Ref, ref} from "vue"; -import {TableIdentifier} from "@/services/api"; +import {Ref} from "vue"; +import {MitMDataType, MitMDefinition, TableIdentifier} from "@/services/api"; export type WithinTableMappings = { [key: string]: ExtendedColumnListItem } export type ForeignRelationMappings = { [fkRelName: string]: { - fk_columns: { [nameToMap: string]: ExtendedColumnListItem }, - referred_table: Ref<TableIdentifier> + fk_columns: { [nameToMap: string]: ExtendedColumnListItem | null }, + referred_table: Ref<TableIdentifier | null> } } +export type TypedColumnSelection = { [key: string]: { selected: boolean, declaredType: MitMDataType | null } } + +export function colMapFromNames(columns?: ExtendedColumnListItem[], nameColumnMapping?: { [key: string]: string }): { + [key: string]: ExtendedColumnListItem +} { + if (!nameColumnMapping) return {} + else return Object.fromEntries(Object.entries(nameColumnMapping).map(([k, v]) => [k, columns?.find(item => item.name.toLowerCase() === v.toLowerCase())]).filter(([k, v]) => !!v)) +} + +export function suggestCol(columns?: ExtendedColumnListItem[], name?: string, mitmType?: MitMDataType): ExtendedColumnListItem | null { + return columns?.find(item => item.name.toLowerCase() === name?.toLowerCase() && item.inferredType === mitmType) ?? null +} + +export function getRequiredType(mitmDef: MitMDefinition, concept: string): MitMDataType | null { + return mitmDef?.weak_concepts[concept] ?? null +} diff --git a/src/services/preset-definitions/synthetic-preset.json b/src/services/preset-definitions/synthetic-preset.json index a1ec54e271b0bf0511563842242c8b6849666a9c..abb9860ad274792527788aa869b6d40c497448c6 100644 --- a/src/services/preset-definitions/synthetic-preset.json +++ b/src/services/preset-definitions/synthetic-preset.json @@ -381,6 +381,20 @@ "segment_index", "object" ], + "foreign_relations": { + "fk": { + "referred_table": [ + "virtual", + "main", + "segments" + ], + "fk_columns": [ + "concept", + "segment_index", + "object" + ] + } + }, "attributes": [ "target_geometry", "extrusion_temp", @@ -410,6 +424,20 @@ "segment_index", "object" ], + "foreign_relations": { + "fk": { + "referred_table": [ + "virtual", + "main", + "segments" + ], + "fk_columns": [ + "concept", + "segment_index", + "object" + ] + } + }, "attributes": [ "workpiece_quality" ], diff --git a/src/services/utils.ts b/src/services/utils.ts index 7e0b5d6357e0302a30c158b7b6066294e8216da3..76b33521e436b5f57d3c5e650aaa2aaf85cee088 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -9,13 +9,19 @@ import {useExportStore} from "@/services/exportStore"; export function anyTIDtoTID(arg, source?): TableIdentifier | null { if (Array.isArray(arg)) return arg.length > 2 ? {source: arg[0], schema: arg[1], name: arg[2]} : { - source: source ? source : "original", + source: !!source ? source : "original", schema: arg[0], name: arg[1] } else return arg } +export function isSameTID(arg1, arg2, source?) : boolean { + const tid1 = anyTIDtoTID(arg1, source) + const tid2 = anyTIDtoTID(arg2, source) + return !!tid1 && !!tid2 && tid1.source === tid2.source && tid1.schema === tid2.schema && tid1.name === tid2.name +} + export function tableIdToStr(id: TableIdentifier) { return `${id.source}:${id.schema}.${id.name}` }