From 63a0a60b128dd1c9e82ace7ec44f8f3932053184 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Fri, 13 Dec 2024 16:24:33 +0100 Subject: [PATCH] builder concept --- src/helpers/dynamicQueryBuilder.ts | 203 ++++++++++++++++++++++++++++- src/type/dynamicQueries.ts | 25 +++- 2 files changed, 217 insertions(+), 11 deletions(-) diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts index 59eadc6..7bbdd25 100644 --- a/src/helpers/dynamicQueryBuilder.ts +++ b/src/helpers/dynamicQueryBuilder.ts @@ -1,5 +1,6 @@ +import { Brackets, DataSource, ObjectLiteral, SelectQueryBuilder } from "typeorm"; import { dataSource } from "../data-source"; -import { DynamicQueryStructure } from "../type/dynamicQueries"; +import { ConditionStructure, DynamicQueryStructure } from "../type/dynamicQueries"; import { TableMeta } from "../type/tableMeta"; export default abstract class DynamicQueryBuilder { @@ -45,11 +46,201 @@ export default abstract class DynamicQueryBuilder { return this.allowedTables.map((table) => this.getTableMeta(table)); } - public static buildQuery(query: DynamicQueryStructure, offset: number = 0, count: number = 25) { - // execute: - // .offset(offset) - // .limit(count) - // .getManyAndCount() + public static buildQuery( + queryObj: DynamicQueryStructure, + offset: number = 0, + count: number = 25 + ): SelectQueryBuilder { + let query = dataSource.getRepository(queryObj.table).createQueryBuilder(queryObj.table + "_0"); + query = this.buildDynamicQuery(query, queryObj); + query = query.offset(offset); + query = query.limit(count); + + return query; + + dataSource + .getRepository("member") + .createQueryBuilder("member_0") + .select("member_0.firstname") + .addSelect("member_0.lastname") + .where("member_0.mail LIKE '%@gmail.com'") + .andWhere( + new Brackets((qb) => { + qb.where("user.firstName LIKE '%J'").orWhere("user.lastName LIKE '%K'"); + }) + ) + .leftJoinAndSelect("member_0.sendNewsletter", "communication_0") + .addSelect("communication_0.*") + .orderBy("member_0.firstname", "ASC") + .addOrderBy("member_0.lastname", "ASC"); + } + + public static buildDynamicQuery( + queryBuilder: SelectQueryBuilder, + queryObject: DynamicQueryStructure, + depth: number = 0 + ): SelectQueryBuilder { + const alias = queryObject.table + "_" + depth; + + // Handle SELECT + if (queryObject.select == "*") { + queryBuilder.select(`${alias}.*`); + } else { + queryBuilder.select(queryObject.select.map((col) => `${alias}.${col}`)); + } + + // Handle WHERE + if (queryObject.where) { + this.applyWhere(queryBuilder, queryObject.where, alias); + } + + // Handle JOINS + if (queryObject.join) { + queryObject.join.forEach((join) => { + const joinAlias = join.table; + const joinType = "leftJoin"; // Default join type + const joinCondition = `${alias}.${join.foreignColumn} = ${joinAlias}.${join.foreignColumn}`; + + queryBuilder[joinType](`${join.table}`, joinAlias, joinCondition); + + // Recursively handle sub-joins and their conditions + this.buildDynamicQuery(queryBuilder, join, depth + 1); + }); + } + + // Handle ORDER BY + if (queryObject.orderBy) { + queryObject.orderBy.forEach((order) => { + queryBuilder.addOrderBy(`${alias}.${order.column}`, order.order); + }); + } + + return queryBuilder; + } + + // Helper: Apply WHERE conditions + public static applyWhere( + queryBuilder: SelectQueryBuilder, + conditions: Array, + alias: string + ): void { + conditions.forEach((condition, index) => { + if (condition.structureType === "condition") { + const whereClause = this.buildConditionClause(condition, alias); + const whereMethod = condition.concat === "_" ? "where" : "andWhere"; + + if (condition.concat === "OR") { + queryBuilder.orWhere(whereClause.query, whereClause.parameters); + } else { + queryBuilder[whereMethod](whereClause.query, whereClause.parameters); + } + } else if (condition.structureType === "nested") { + const nestedQuery = this.conditionsToQuery(condition.condition, alias); + const whereMethod = condition.concat === "OR" ? "orWhere" : "andWhere"; + + queryBuilder[whereMethod](`(${nestedQuery.query})`, nestedQuery.parameters); + } + }); + } + + // Helper: Build a single condition clause + public static buildConditionClause( + condition: ConditionStructure, + alias: string + ): { query: string; parameters: Record } { + if (condition.structureType == "nested") return; + const parameterKey = `${alias}_${condition.column}_${Math.random().toString(36).substring(2)}`; + let query = `${alias}.${condition.column}`; + let parameters: Record = {}; + + switch (condition.operation) { + case "eq": + query += ` = :${parameterKey}`; + parameters[parameterKey] = condition.value; + break; + case "neq": + query += ` != :${parameterKey}`; + parameters[parameterKey] = condition.value; + break; + case "lt": + query += ` < :${parameterKey}`; + parameters[parameterKey] = condition.value; + break; + case "lte": + query += ` <= :${parameterKey}`; + parameters[parameterKey] = condition.value; + break; + case "gt": + query += ` > :${parameterKey}`; + parameters[parameterKey] = condition.value; + break; + case "gte": + query += ` >= :${parameterKey}`; + parameters[parameterKey] = condition.value; + break; + case "in": + query += ` IN (:...${parameterKey})`; + parameters[parameterKey] = condition.value; + break; + case "notIn": + query += ` NOT IN (:...${parameterKey})`; + parameters[parameterKey] = condition.value; + break; + case "between": + query += ` BETWEEN :${parameterKey}_start AND :${parameterKey}_end`; + parameters[`${parameterKey}_start`] = (condition.value as { start: any }).start; + parameters[`${parameterKey}_end`] = (condition.value as { end: any }).end; + break; + case "null": + query += ` IS NULL`; + break; + case "notNull": + query += ` IS NOT NULL`; + break; + case "contains": + query += ` LIKE :${parameterKey}`; + parameters[parameterKey] = `%${condition.value}%`; + break; + case "notContains": + query += ` NOT LIKE :${parameterKey}`; + parameters[parameterKey] = `%${condition.value}%`; + break; + case "startsWith": + query += ` LIKE :${parameterKey}`; + parameters[parameterKey] = `${condition.value}%`; + break; + case "endsWith": + query += ` LIKE :${parameterKey}`; + parameters[parameterKey] = `%${condition.value}`; + break; + default: + throw new Error(`Unsupported operation: ${condition.operation}`); + } + + return { query, parameters }; + } + + // Helper: Convert nested conditions to a query + public static conditionsToQuery( + conditions: Array, + alias: string + ): { query: string; parameters: Record } { + let queryParts: string[] = []; + let parameters: Record = {}; + + conditions.forEach((condition, index) => { + if (condition.structureType === "condition") { + const clause = this.buildConditionClause(condition, alias); + queryParts.push(clause.query); + parameters = { ...parameters, ...clause.parameters }; + } else if (condition.structureType === "nested") { + const nested = this.conditionsToQuery(condition.condition, alias); + queryParts.push(`(${nested.query})`); + parameters = { ...parameters, ...nested.parameters }; + } + }); + + return { query: queryParts.join(" AND "), parameters }; } // use switch... for compare functions diff --git a/src/type/dynamicQueries.ts b/src/type/dynamicQueries.ts index 629454f..92f953c 100644 --- a/src/type/dynamicQueries.ts +++ b/src/type/dynamicQueries.ts @@ -3,16 +3,18 @@ export interface DynamicQueryStructure { table: string; where?: Array; join?: Array; - orderBy?: { [key: string]: "ASC" | "DESC" }; + orderBy?: Array; } export type ConditionStructure = ( | { + structureType: "condition"; column: string; operation: WhereOperation; value: ConditionValue; } | { + structureType: "nested"; invert?: boolean; condition: Array; } @@ -43,6 +45,13 @@ export type WhereOperation = | "startsWith" // Starts with | "endsWith"; // Ends with +export type OrderByStructure = { + column: string; + order: OrderByType; +}; + +export type OrderByType = "ASC" | "DESC"; + const exampleQuery: DynamicQueryStructure = { select: ["firstname", "lastname"], table: "member", @@ -103,8 +112,14 @@ const exampleQuery: DynamicQueryStructure = { ], }, ], - orderBy: { - firstname: "ASC", - lastname: "ASC", - }, + orderBy: [ + { + column: "firstname", + order: "ASC", + }, + { + column: "lastname", + order: "ASC", + }, + ], };