From 238a35da9f5dd2e52abf3aa8d8302f161e37d3c7 Mon Sep 17 00:00:00 2001 From: Julian Krauser <jkrauser209@gmail.com> Date: Wed, 16 Apr 2025 16:11:10 +0200 Subject: [PATCH] extend query builder by custom join --- src/components/queryBuilder/BuilderHost.vue | 2 +- src/components/queryBuilder/Join.vue | 9 +- src/components/queryBuilder/JoinTable.vue | 115 ++++++++++++++------ src/components/queryBuilder/Table.vue | 6 +- src/types/dynamicQueries.ts | 4 +- src/views/admin/club/query/Builder.vue | 7 +- 6 files changed, 97 insertions(+), 46 deletions(-) diff --git a/src/components/queryBuilder/BuilderHost.vue b/src/components/queryBuilder/BuilderHost.vue index 035ca81..b1d0c67 100644 --- a/src/components/queryBuilder/BuilderHost.vue +++ b/src/components/queryBuilder/BuilderHost.vue @@ -71,7 +71,7 @@ </div> </div> </div> - <div class="p-2 h-60 md:h-60 w-full overflow-y-auto"> + <div class="p-2 h-44 md:h-60 w-full overflow-y-auto"> <textarea v-if="typeof value == 'string'" v-model="value" placeholder="SQL Query" class="h-full w-full" /> <Table v-else v-model="value" enableOrder /> </div> diff --git a/src/components/queryBuilder/Join.vue b/src/components/queryBuilder/Join.vue index 85eb23a..e92c37a 100644 --- a/src/components/queryBuilder/Join.vue +++ b/src/components/queryBuilder/Join.vue @@ -1,6 +1,6 @@ <template> <div class="flex flex-row gap-2"> - <p class="w-14 min-w-14 pt-2">JOIN</p> + <p class="w-14 min-w-14 pt-2">I_JOIN</p> <div class="flex flex-row flex-wrap gap-2 items-center w-full"> <div class="flex flex-row flex-wrap gap-2 items-center justify-end w-full"> <JoinTable @@ -22,7 +22,7 @@ <script setup lang="ts"> import { defineComponent, type PropType } from "vue"; import { mapActions, mapState } from "pinia"; -import { type DynamicQueryStructure } from "@/types/dynamicQueries"; +import { type DynamicQueryStructure, type JoinStructure } from "@/types/dynamicQueries"; import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder"; import { PlusIcon } from "@heroicons/vue/24/outline"; import JoinTable from "./JoinTable.vue"; @@ -37,7 +37,7 @@ export default defineComponent({ default: "", }, modelValue: { - type: Array as PropType<Array<DynamicQueryStructure & { foreignColumn: string }>>, + type: Array as PropType<Array<DynamicQueryStructure & JoinStructure>>, default: [], }, alreadyJoined: { @@ -52,7 +52,7 @@ export default defineComponent({ get() { return this.modelValue; }, - set(val: Array<DynamicQueryStructure & { foreignColumn: string }>) { + set(val: Array<DynamicQueryStructure & JoinStructure>) { this.$emit("update:model-value", val); }, }, @@ -66,6 +66,7 @@ export default defineComponent({ where: [], join: [], orderBy: [], + type: "defined", foreignColumn: "", }); }, diff --git a/src/components/queryBuilder/JoinTable.vue b/src/components/queryBuilder/JoinTable.vue index f8fac05..453ae0a 100644 --- a/src/components/queryBuilder/JoinTable.vue +++ b/src/components/queryBuilder/JoinTable.vue @@ -2,28 +2,51 @@ <div class="flex flex-row gap-2 w-full"> <div class="flex flex-row gap-2 w-full"> <div class="flex flex-col gap-2 w-full"> - <select v-model="foreignColumn" class="w-full"> - <option value="" disabled>Relation auswählen</option> - <option - v-for="relation in activeTable?.relations" - :value="relation.column" - :disabled=" - alreadyJoined.includes(joinTableName(relation.referencedTableName)) && - joinTableName(relation.referencedTableName) != value.table - " + <div class="flex flex-row gap-2 w-full"> + <div + class="h-fit p-1 border border-gray-400 hover:bg-gray-200 rounded-md" + title="Join Modus wechseln" + @click="swapJoinType(value.type)" > - {{ relation.column }} -> {{ joinTableName(relation.referencedTableName) }} - <span - v-if=" + <ArrowsUpDownIcon class="text-gray-500 h-6 w-6 cursor-pointer" /> + </div> + + <select v-if="value.type == 'defined'" v-model="context" class="w-full"> + <option value="" disabled>Relation auswählen</option> + <option + v-for="relation in activeTable?.relations" + :value="relation.column" + :disabled=" alreadyJoined.includes(joinTableName(relation.referencedTableName)) && joinTableName(relation.referencedTableName) != value.table " > - (Join auf dieser Ebene besteht schon) - </span> - </option> - </select> - <Table v-model="value" disable-table-select /> + {{ relation.column }} -> {{ joinTableName(relation.referencedTableName) }} + <span + v-if=" + alreadyJoined.includes(joinTableName(relation.referencedTableName)) && + joinTableName(relation.referencedTableName) != value.table + " + > + (Join auf dieser Ebene besteht schon) + </span> + </option> + </select> + <div v-else class="flex flex-col w-full"> + <select v-model="joinTable"> + <option value="" disabled>Tabelle auswählen</option> + <option + v-for="table in tableMetas" + :value="table.tableName" + :disabled="alreadyJoined.includes(table.tableName) && table.tableName != value.table" + > + {{ table.tableName }} + </option> + </select> + <input v-model="context" type="text" placeholder="Join Condition tabA.col = tabB.col" /> + </div> + </div> + <Table v-model="value" disable-table-select :show-table-select="false" /> </div> <div class="h-fit p-1 border border-gray-400 hover:bg-gray-200 rounded-md" @click="$emit('remove')"> <TrashIcon class="text-gray-500 h-6 w-6 cursor-pointer" /> @@ -35,10 +58,10 @@ <script setup lang="ts"> import { defineComponent, type PropType } from "vue"; import { mapActions, mapState } from "pinia"; -import { type DynamicQueryStructure } from "@/types/dynamicQueries"; +import { type DynamicQueryStructure, type JoinStructure } from "@/types/dynamicQueries"; import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder"; import Table from "./Table.vue"; -import { TrashIcon } from "@heroicons/vue/24/outline"; +import { ArrowsUpDownIcon, TrashIcon } from "@heroicons/vue/24/outline"; import { joinTableName } from "@/helpers/queryFormatter"; import { v4 as uuid } from "uuid"; </script> @@ -51,11 +74,7 @@ export default defineComponent({ default: "", }, modelValue: { - type: Object as PropType< - DynamicQueryStructure & { - foreignColumn: string; - } - >, + type: Object as PropType<DynamicQueryStructure & JoinStructure>, required: true, }, alreadyJoined: { @@ -73,24 +92,43 @@ export default defineComponent({ get() { return this.modelValue; }, - set( - val: DynamicQueryStructure & { - foreignColumn: string; - } - ) { + set(val: DynamicQueryStructure & JoinStructure) { this.$emit("update:model-value", val); }, }, - foreignColumn: { + context: { get() { - return this.modelValue.foreignColumn; + if (this.modelValue.type == "defined") { + return this.modelValue.foreignColumn ?? ""; + } else { + return this.modelValue.condition ?? ""; + } + }, + set(val: string) { + console.log(val, this.modelValue.type); + if (this.modelValue.type == "defined") { + let relTable = this.activeTable?.relations.find((r) => r.column == val); + this.$emit("update:model-value", { + ...this.modelValue, + foreignColumn: val, + table: joinTableName(relTable?.referencedTableName ?? ""), + }); + } else { + this.$emit("update:model-value", { + ...this.modelValue, + condition: val, + }); + } + }, + }, + joinTable: { + get(): string { + return this.modelValue.table; }, set(val: string) { - let relTable = this.activeTable?.relations.find((r) => r.column == val); this.$emit("update:model-value", { ...this.modelValue, - foreignColumn: val, - table: joinTableName(relTable?.referencedTableName ?? ""), + table: val, }); }, }, @@ -100,5 +138,14 @@ export default defineComponent({ this.value.id = uuid(); } }, + methods: { + swapJoinType(type: string) { + if (type == "defined") { + this.value.type = "custom"; + } else { + this.value.type = "defined"; + } + }, + }, }); </script> diff --git a/src/components/queryBuilder/Table.vue b/src/components/queryBuilder/Table.vue index 7f7b6ea..a1697dc 100644 --- a/src/components/queryBuilder/Table.vue +++ b/src/components/queryBuilder/Table.vue @@ -1,6 +1,6 @@ <template> <div class="flex flex-col gap-2 w-full"> - <TableSelect v-model="table" :disableTableSelect="disableTableSelect" /> + <TableSelect v-if="showTableSelect" v-model="table" :disableTableSelect="disableTableSelect" /> <ColumnSelect v-if="table != ''" v-model="columnSelect" :table="table" /> <Where v-if="table != ''" v-model="where" :table="table" /> <Join v-if="table != ''" v-model="modelValue.join" :table="table" :alreadyJoined="alreadyJoined" /> @@ -34,6 +34,10 @@ export default defineComponent({ type: Boolean, default: false, }, + showTableSelect: { + type: Boolean, + default: true, + }, }, emits: ["update:model-value"], computed: { diff --git a/src/types/dynamicQueries.ts b/src/types/dynamicQueries.ts index adf4190..161f590 100644 --- a/src/types/dynamicQueries.ts +++ b/src/types/dynamicQueries.ts @@ -3,7 +3,7 @@ export interface DynamicQueryStructure { select: string[] | "*"; table: string; where?: Array<ConditionStructure>; - join?: Array<DynamicQueryStructure & { foreignColumn: string }>; + join?: Array<DynamicQueryStructure & JoinStructure>; orderBy?: Array<OrderByStructure>; // only at top level } @@ -48,6 +48,8 @@ export type WhereOperation = | "timespanEq"; // Date before x years (YYYY-01-01 <bis> YYYY-12-31) // TODO: age between | age equals | age greater | age smaller +export type JoinStructure = { foreignColumn: string; type: "defined" } | { condition: string; type: "custom" }; + export type OrderByStructure = { id: string; depth: number; diff --git a/src/views/admin/club/query/Builder.vue b/src/views/admin/club/query/Builder.vue index 29025fc..08b0b0d 100644 --- a/src/views/admin/club/query/Builder.vue +++ b/src/views/admin/club/query/Builder.vue @@ -70,14 +70,11 @@ import { useQueryStoreStore } from "@/stores/admin/configuration/queryStore"; <script lang="ts"> export default defineComponent({ computed: { - ...mapState(useQueryBuilderStore, ["loading", "loadingData", "tableMetas", "data", "totalLength", "queryError"]), + ...mapState(useQueryBuilderStore, ["loading", "loadingData", "data", "totalLength", "queryError"]), ...mapWritableState(useQueryBuilderStore, ["query"]), }, - mounted() { - this.fetchTableMetas(); - }, methods: { - ...mapActions(useQueryBuilderStore, ["fetchTableMetas", "sendQuery", "clearResults", "exportData"]), + ...mapActions(useQueryBuilderStore, ["sendQuery", "clearResults", "exportData"]), ...mapActions(useQueryStoreStore, ["triggerSave"]), }, });