import { Brackets, DataSource, ObjectLiteral, SelectQueryBuilder } from "typeorm"; import { dataSource } from "../data-source"; import { ConditionStructure, DynamicQueryStructure } from "../type/dynamicQueries"; import { TableMeta } from "../type/tableMeta"; export default abstract class DynamicQueryBuilder { public static allowedTables: Array = [ "award", "communication", "communicationType", "executivePosition", "membershipStatus", "qualification", "member", "memberAwards", "memberExecutivePositions", "memberQualifications", "membership", ]; public static getTableMeta(tableName: string): TableMeta { let { name, columns, relations } = dataSource.getMetadata(tableName); const uniqueColumns = columns.map((c) => ({ column: c.propertyName, type: c.type })); return { tableName: name, columns: [ ...uniqueColumns, ...relations .filter((r) => !uniqueColumns.some((c) => r.propertyName == c.column)) .map((r) => ({ column: r.propertyName, type: r.inverseEntityMetadata?.columns.find((col) => col.propertyName === r.inverseSidePropertyPath)?.type, })), ], relations: relations.map((r) => ({ column: r.propertyName, relationType: r.relationType, referencedTableName: r.inverseEntityMetadata?.tableName, })), }; } public static getAllTableMeta(): Array { return this.allowedTables.map((table) => this.getTableMeta(table)); } 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 // use NotBrackets/Brackets for nested conditions // use joins by requesting table schema and setting correct column }