From 717de68f4e45bf728162040ecceb24a70d7a4ff2 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Tue, 26 Nov 2024 15:00:21 +0100 Subject: [PATCH 01/15] dynamic Query typing --- src/type/dynamicQueries.ts | 60 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/type/dynamicQueries.ts diff --git a/src/type/dynamicQueries.ts b/src/type/dynamicQueries.ts new file mode 100644 index 0000000..70d4158 --- /dev/null +++ b/src/type/dynamicQueries.ts @@ -0,0 +1,60 @@ +export interface DynamicQueryStructure { + select: string[] | "*"; + table: string; + where?: Partial; + join?: Array; + orderBy?: { [key: string]: "ASC" | "DESC" }; +} + +export type ConditionStructure = { + [opt in WhereOptions]: Partial | { [column: string]: Partial }; +}; + +export type ConditionOperation = { + [op in WhereOperation]: FieldType | Array | { start: FieldType; end: FieldType }; +}; + +export type FieldType = number | string | Date | boolean; + +export type WhereOptions = "OR" | "AND"; + +export type WhereOperation = + | "eq" // Equal + | "neq" // Not equal + | "lt" // Less than + | "lte" // Less than or equal to + | "gt" // Greater than + | "gte" // Greater than or equal to + | "in" // Included in an array + | "notIn" // Not included in an array + | "contains" // Contains + | "notContains" // Does not contain + | "null" // Is null + | "notNull" // Is not null + | "between" // Is between + | "startsWith" // Starts with + | "endsWith"; // Ends with + +const exampleQuery: DynamicQueryStructure = { + select: ["firstname", "lastname"], + table: "member", + where: { + AND: { + mail: { eq: 1 }, + OR: { + firstname: { eq: "hi" }, + lastname: { eq: "ho" }, + }, + }, + }, + join: [ + { + select: "*", + table: "adress", + }, + ], + orderBy: { + firstname: "ASC", + lastname: "ASC", + }, +}; From 9944fb931a6361ce4af90243194a97274d6c6394 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Wed, 27 Nov 2024 15:01:31 +0100 Subject: [PATCH 02/15] get table definition --- src/helpers/dynamicQueryBuilder.ts | 34 ++++++++++++++++++++++++++++++ src/type/dynamicQueries.ts | 11 +++++----- 2 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 src/helpers/dynamicQueryBuilder.ts diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts new file mode 100644 index 0000000..ebd9e7a --- /dev/null +++ b/src/helpers/dynamicQueryBuilder.ts @@ -0,0 +1,34 @@ +import { dataSource } from "../data-source"; +import { DynamicQueryStructure } from "../type/dynamicQueries"; + +export default abstract class DynamicQueryBuilder { + public static buildQuery(query: DynamicQueryStructure) {} + + // use switch... for compare functions + // use NotBrackets/Brackets for nested conditions + // use joins by requesting table schema and setting correct column + + public static getTableMeta(tableName: string) { + 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, + })), + }; + } +} diff --git a/src/type/dynamicQueries.ts b/src/type/dynamicQueries.ts index 70d4158..a8c78a9 100644 --- a/src/type/dynamicQueries.ts +++ b/src/type/dynamicQueries.ts @@ -2,7 +2,7 @@ export interface DynamicQueryStructure { select: string[] | "*"; table: string; where?: Partial; - join?: Array; + join?: Array; orderBy?: { [key: string]: "ASC" | "DESC" }; } @@ -40,17 +40,18 @@ const exampleQuery: DynamicQueryStructure = { table: "member", where: { AND: { - mail: { eq: 1 }, + mail: { endsWith: "@gmail.com" }, OR: { - firstname: { eq: "hi" }, - lastname: { eq: "ho" }, + firstname: { startsWith: "J" }, + lastname: { endsWith: "K" }, }, }, }, join: [ { select: "*", - table: "adress", + table: "communication", + foreignColumn: "sendNewsletter", }, ], orderBy: { From f05adbd430d1fbfe54c6216a4c648a38346ffe8b Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Thu, 28 Nov 2024 14:16:44 +0100 Subject: [PATCH 03/15] query build update --- src/type/dynamicQueries.ts | 55 ++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/src/type/dynamicQueries.ts b/src/type/dynamicQueries.ts index a8c78a9..25abe12 100644 --- a/src/type/dynamicQueries.ts +++ b/src/type/dynamicQueries.ts @@ -1,22 +1,27 @@ export interface DynamicQueryStructure { select: string[] | "*"; table: string; - where?: Partial; + where?: Array; join?: Array; orderBy?: { [key: string]: "ASC" | "DESC" }; } -export type ConditionStructure = { - [opt in WhereOptions]: Partial | { [column: string]: Partial }; -}; - -export type ConditionOperation = { - [op in WhereOperation]: FieldType | Array | { start: FieldType; end: FieldType }; -}; +export type ConditionStructure = + | { + type: WhereType; + column: string; + operation: WhereOperation; + value: ConditionValue; + } + | { + type: WhereType; + condition: Array; + }; +export type ConditionValue = FieldType | Array | { start: FieldType; end: FieldType }; export type FieldType = number | string | Date | boolean; -export type WhereOptions = "OR" | "AND"; +export type WhereType = "OR" | "AND" | "_"; export type WhereOperation = | "eq" // Equal @@ -38,15 +43,31 @@ export type WhereOperation = const exampleQuery: DynamicQueryStructure = { select: ["firstname", "lastname"], table: "member", - where: { - AND: { - mail: { endsWith: "@gmail.com" }, - OR: { - firstname: { startsWith: "J" }, - lastname: { endsWith: "K" }, - }, + where: [ + { + type: "_", + column: "mail", + operation: "endsWith", + value: "@gmail.com", }, - }, + { + type: "AND", + condition: [ + { + type: "_", + column: "firstname", + operation: "startsWith", + value: "J", + }, + { + type: "OR", + column: "lastname", + operation: "startsWith", + value: "K", + }, + ], + }, + ], join: [ { select: "*", From 74a496a5e0dd7c134766052d7c0e562b0d19c42b Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Thu, 28 Nov 2024 17:53:22 +0100 Subject: [PATCH 04/15] query enhancement --- src/type/dynamicQueries.ts | 46 ++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/src/type/dynamicQueries.ts b/src/type/dynamicQueries.ts index 25abe12..629454f 100644 --- a/src/type/dynamicQueries.ts +++ b/src/type/dynamicQueries.ts @@ -6,22 +6,25 @@ export interface DynamicQueryStructure { orderBy?: { [key: string]: "ASC" | "DESC" }; } -export type ConditionStructure = +export type ConditionStructure = ( | { - type: WhereType; column: string; operation: WhereOperation; value: ConditionValue; } | { - type: WhereType; + invert?: boolean; condition: Array; - }; + } +) & { + concat: WhereType; + structureType: "condition" | "nested"; +}; export type ConditionValue = FieldType | Array | { start: FieldType; end: FieldType }; export type FieldType = number | string | Date | boolean; -export type WhereType = "OR" | "AND" | "_"; +export type WhereType = "OR" | "AND" | "_"; // _ represents initial where in (sub-)query export type WhereOperation = | "eq" // Equal @@ -45,22 +48,26 @@ const exampleQuery: DynamicQueryStructure = { table: "member", where: [ { - type: "_", + structureType: "condition", + concat: "_", column: "mail", operation: "endsWith", value: "@gmail.com", }, { - type: "AND", + structureType: "nested", + concat: "AND", condition: [ { - type: "_", + structureType: "condition", + concat: "_", column: "firstname", operation: "startsWith", value: "J", }, { - type: "OR", + structureType: "condition", + concat: "OR", column: "lastname", operation: "startsWith", value: "K", @@ -74,6 +81,27 @@ const exampleQuery: DynamicQueryStructure = { table: "communication", foreignColumn: "sendNewsletter", }, + { + select: "*", + table: "membership", + foreignColumn: "memberships", + join: [ + { + select: "*", + table: "membership_status", + foreignColumn: "status", + where: [ + { + structureType: "condition", + concat: "_", + column: "status", + operation: "eq", + value: "aktiv", + }, + ], + }, + ], + }, ], orderBy: { firstname: "ASC", From ee0d6ddcce087580337137dcb9f188d95a986427 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Thu, 12 Dec 2024 16:33:51 +0100 Subject: [PATCH 05/15] base controller layout --- .../admin/queryBuilderController.ts | 50 +++++++++++++++++++ src/helpers/dynamicQueryBuilder.ts | 36 ++++++++++--- src/routes/admin/communicationType.ts | 6 +-- src/routes/admin/index.ts | 5 +- src/routes/admin/queryBuilder.ts | 18 +++++++ src/type/permissionTypes.ts | 22 ++++++-- src/type/tableMeta.ts | 7 +++ 7 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 src/controller/admin/queryBuilderController.ts create mode 100644 src/routes/admin/queryBuilder.ts create mode 100644 src/type/tableMeta.ts diff --git a/src/controller/admin/queryBuilderController.ts b/src/controller/admin/queryBuilderController.ts new file mode 100644 index 0000000..ccab8df --- /dev/null +++ b/src/controller/admin/queryBuilderController.ts @@ -0,0 +1,50 @@ +import { Request, Response } from "express"; +import DynamicQueryBuilder from "../../helpers/dynamicQueryBuilder"; + +/** + * @description get all table metas + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getAllTableMeta(req: Request, res: Response): Promise { + let tableMetas = DynamicQueryBuilder.getAllTableMeta(); + + res.json(tableMetas); +} + +/** + * @description get meta by tablename + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getTableMetaByTablename(req: Request, res: Response): Promise { + const tablename = req.params.tablename; + let tableMeta = DynamicQueryBuilder.getTableMeta(tablename); + + res.json(tableMeta); +} + +/** + * @description execute Query + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function executeQuery(req: Request, res: Response): Promise { + let offset = parseInt((req.query.offset as string) ?? "0"); + let count = parseInt((req.query.count as string) ?? "25"); + const query = req.body.query; + + //build query to sql + //verify sql or return error + //let [rows, total] = await executeQuery(query, offset, count); + + res.json({ + rows: [], + total: 0, + offset: offset, + count: count, + }); +} diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts index ebd9e7a..59eadc6 100644 --- a/src/helpers/dynamicQueryBuilder.ts +++ b/src/helpers/dynamicQueryBuilder.ts @@ -1,14 +1,23 @@ import { dataSource } from "../data-source"; import { DynamicQueryStructure } from "../type/dynamicQueries"; +import { TableMeta } from "../type/tableMeta"; export default abstract class DynamicQueryBuilder { - public static buildQuery(query: DynamicQueryStructure) {} + public static allowedTables: Array = [ + "award", + "communication", + "communicationType", + "executivePosition", + "membershipStatus", + "qualification", + "member", + "memberAwards", + "memberExecutivePositions", + "memberQualifications", + "membership", + ]; - // use switch... for compare functions - // use NotBrackets/Brackets for nested conditions - // use joins by requesting table schema and setting correct column - - public static getTableMeta(tableName: string) { + public static getTableMeta(tableName: string): TableMeta { let { name, columns, relations } = dataSource.getMetadata(tableName); const uniqueColumns = columns.map((c) => ({ column: c.propertyName, type: c.type })); @@ -31,4 +40,19 @@ export default abstract class DynamicQueryBuilder { })), }; } + + public static getAllTableMeta(): Array { + 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() + } + + // use switch... for compare functions + // use NotBrackets/Brackets for nested conditions + // use joins by requesting table schema and setting correct column } diff --git a/src/routes/admin/communicationType.ts b/src/routes/admin/communicationType.ts index 2a797f6..4fc185f 100644 --- a/src/routes/admin/communicationType.ts +++ b/src/routes/admin/communicationType.ts @@ -25,7 +25,7 @@ router.get("/:id", async (req: Request, res: Response) => { router.post( "/", - PermissionHelper.passCheckMiddleware("create", "settings", "communication"), + PermissionHelper.passCheckMiddleware("create", "settings", "communication_type"), async (req: Request, res: Response) => { await createCommunicationType(req, res); } @@ -33,7 +33,7 @@ router.post( router.patch( "/:id", - PermissionHelper.passCheckMiddleware("update", "settings", "communication"), + PermissionHelper.passCheckMiddleware("update", "settings", "communication_type"), async (req: Request, res: Response) => { await updateCommunicationType(req, res); } @@ -41,7 +41,7 @@ router.patch( router.delete( "/:id", - PermissionHelper.passCheckMiddleware("delete", "settings", "communication"), + PermissionHelper.passCheckMiddleware("delete", "settings", "communication_type"), async (req: Request, res: Response) => { await deleteCommunicationType(req, res); } diff --git a/src/routes/admin/index.ts b/src/routes/admin/index.ts index b4f5d0c..2a292f3 100644 --- a/src/routes/admin/index.ts +++ b/src/routes/admin/index.ts @@ -10,8 +10,8 @@ import calendarType from "./calendarType"; import member from "./member"; import protocol from "./protocol"; - import calendar from "./calendar"; +import queryBuilder from "./queryBuilder"; import role from "./role"; import user from "./user"; @@ -22,7 +22,7 @@ var router = express.Router({ mergeParams: true }); router.use("/award", PermissionHelper.passCheckMiddleware("read", "settings", "award"), award); router.use( "/communicationtype", - PermissionHelper.passCheckMiddleware("read", "settings", "communication"), + PermissionHelper.passCheckMiddleware("read", "settings", "communication_type"), communicationType ); router.use( @@ -41,6 +41,7 @@ router.use("/calendartype", PermissionHelper.passCheckMiddleware("read", "settin router.use("/member", PermissionHelper.passCheckMiddleware("read", "club", "member"), member); router.use("/protocol", PermissionHelper.passCheckMiddleware("read", "club", "protocol"), protocol); router.use("/calendar", PermissionHelper.passCheckMiddleware("read", "club", "calendar"), calendar); +router.use("/querybuilder", PermissionHelper.passCheckMiddleware("read", "club", "query"), queryBuilder); router.use("/role", PermissionHelper.passCheckMiddleware("read", "user", "role"), role); router.use("/user", PermissionHelper.passCheckMiddleware("read", "user", "user"), user); diff --git a/src/routes/admin/queryBuilder.ts b/src/routes/admin/queryBuilder.ts new file mode 100644 index 0000000..d09b9c3 --- /dev/null +++ b/src/routes/admin/queryBuilder.ts @@ -0,0 +1,18 @@ +import express, { Request, Response } from "express"; +import { executeQuery, getAllTableMeta, getTableMetaByTablename } from "../../controller/admin/queryBuilderController"; + +var router = express.Router({ mergeParams: true }); + +router.get("/tables", async (req: Request, res: Response) => { + await getAllTableMeta(req, res); +}); + +router.get("/table/:tablename", async (req: Request, res: Response) => { + await getTableMetaByTablename(req, res); +}); + +router.post("/query", async (req: Request, res: Response) => { + await executeQuery(req, res); +}); + +export default router; diff --git a/src/type/permissionTypes.ts b/src/type/permissionTypes.ts index fc468a9..d5bb35e 100644 --- a/src/type/permissionTypes.ts +++ b/src/type/permissionTypes.ts @@ -8,11 +8,13 @@ export type PermissionModule = | "qualification" | "award" | "executive_position" - | "communication" + | "communication_type" | "membership_status" | "calendar_type" | "user" - | "role"; + | "role" + | "query" + | "query_store"; export type PermissionType = "read" | "create" | "update" | "delete"; @@ -44,15 +46,25 @@ export const permissionModules: Array = [ "qualification", "award", "executive_position", - "communication", + "communication_type", "membership_status", "calendar_type", "user", "role", + "query", + "query_store", ]; export const permissionTypes: Array = ["read", "create", "update", "delete"]; export const sectionsAndModules: SectionsAndModulesObject = { - club: ["member", "calendar", "newsletter", "protocol"], - settings: ["qualification", "award", "executive_position", "communication", "membership_status", "calendar_type"], + club: ["member", "calendar", "newsletter", "protocol", "query"], + settings: [ + "qualification", + "award", + "executive_position", + "communication_type", + "membership_status", + "calendar_type", + "query_store", + ], user: ["user", "role"], }; diff --git a/src/type/tableMeta.ts b/src/type/tableMeta.ts new file mode 100644 index 0000000..a50e115 --- /dev/null +++ b/src/type/tableMeta.ts @@ -0,0 +1,7 @@ +import { ColumnType } from "typeorm"; + +export interface TableMeta { + tableName: string; + columns: Array<{ column: string; type: ColumnType }>; + relations: Array<{ column: string; relationType: string; referencedTableName: string }>; +} From 63a0a60b128dd1c9e82ace7ec44f8f3932053184 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Fri, 13 Dec 2024 16:24:33 +0100 Subject: [PATCH 06/15] 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", + }, + ], }; From 2518a1046fc4ca77a6f38bbcdf9c61a15a49c02b Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Sat, 14 Dec 2024 15:44:17 +0100 Subject: [PATCH 07/15] builder change --- src/helpers/dynamicQueryBuilder.ts | 141 +++++++++++------------------ src/type/dynamicQueries.ts | 6 +- 2 files changed, 54 insertions(+), 93 deletions(-) diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts index 7bbdd25..e6f655d 100644 --- a/src/helpers/dynamicQueryBuilder.ts +++ b/src/helpers/dynamicQueryBuilder.ts @@ -1,4 +1,4 @@ -import { Brackets, DataSource, ObjectLiteral, SelectQueryBuilder } from "typeorm"; +import { Brackets, DataSource, NotBrackets, ObjectLiteral, SelectQueryBuilder, WhereExpressionBuilder } from "typeorm"; import { dataSource } from "../data-source"; import { ConditionStructure, DynamicQueryStructure } from "../type/dynamicQueries"; import { TableMeta } from "../type/tableMeta"; @@ -52,103 +52,93 @@ export default abstract class DynamicQueryBuilder { 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); + + this.buildDynamicQuery(query, queryObj); + + query.offset(offset); + 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, + private static buildDynamicQuery( + query: SelectQueryBuilder, queryObject: DynamicQueryStructure, depth: number = 0 - ): SelectQueryBuilder { + ): void { const alias = queryObject.table + "_" + depth; - // Handle SELECT if (queryObject.select == "*") { - queryBuilder.select(`${alias}.*`); + query.addSelect(`${alias}.*`); } else { - queryBuilder.select(queryObject.select.map((col) => `${alias}.${col}`)); + for (const select of queryObject.select) { + query.addSelect(`${alias}.${select}`); + } } - // Handle WHERE if (queryObject.where) { - this.applyWhere(queryBuilder, queryObject.where, alias); + this.applyWhere(query, 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}`; + for (const join of queryObject.join) { + query.leftJoinAndSelect(`${alias}.${join.foreignColumn}`, join.table + "_" + (depth + 1)); - queryBuilder[joinType](`${join.table}`, joinAlias, joinCondition); - - // Recursively handle sub-joins and their conditions - this.buildDynamicQuery(queryBuilder, join, depth + 1); - }); + this.buildDynamicQuery(query, join, depth + 1); + } } - // Handle ORDER BY if (queryObject.orderBy) { queryObject.orderBy.forEach((order) => { - queryBuilder.addOrderBy(`${alias}.${order.column}`, order.order); + query.addOrderBy(`${alias}.${order.column}`, order.order); }); } - - return queryBuilder; } - // Helper: Apply WHERE conditions - public static applyWhere( - queryBuilder: SelectQueryBuilder, + public static applyWhere( + query: SelectQueryBuilder | WhereExpressionBuilder, conditions: Array, alias: string ): void { - conditions.forEach((condition, index) => { - if (condition.structureType === "condition") { + for (const condition of conditions) { + 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); + if (condition.concat == "_" || condition.concat == "AND") { + query.andWhere(whereClause.query, whereClause.parameters); } else { - queryBuilder[whereMethod](whereClause.query, whereClause.parameters); + query.orWhere(whereClause.query, whereClause.parameters); + } + } else { + if (condition.concat == "AND") { + query.andWhere( + condition.invert == undefined || condition.invert == true + ? new Brackets((qb) => { + this.applyWhere(qb, condition.conditions, alias); + }) + : new NotBrackets((qb) => { + this.applyWhere(qb, condition.conditions, alias); + }) + ); + } else { + query.orWhere( + condition.invert == undefined || condition.invert == true + ? new Brackets((qb) => { + this.applyWhere(qb, condition.conditions, alias); + }) + : new NotBrackets((qb) => { + this.applyWhere(qb, condition.conditions, alias); + }) + ); } - } 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, + private static buildConditionClause( + condition: ConditionStructure & { structureType: "condition" }, 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 = {}; @@ -213,37 +203,8 @@ export default abstract class DynamicQueryBuilder { 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 } diff --git a/src/type/dynamicQueries.ts b/src/type/dynamicQueries.ts index 92f953c..5e69094 100644 --- a/src/type/dynamicQueries.ts +++ b/src/type/dynamicQueries.ts @@ -16,7 +16,7 @@ export type ConditionStructure = ( | { structureType: "nested"; invert?: boolean; - condition: Array; + conditions: Array; } ) & { concat: WhereType; @@ -52,7 +52,7 @@ export type OrderByStructure = { export type OrderByType = "ASC" | "DESC"; -const exampleQuery: DynamicQueryStructure = { +export const exampleQuery: DynamicQueryStructure = { select: ["firstname", "lastname"], table: "member", where: [ @@ -66,7 +66,7 @@ const exampleQuery: DynamicQueryStructure = { { structureType: "nested", concat: "AND", - condition: [ + conditions: [ { structureType: "condition", concat: "_", From 7497787ae4191aaec758a6f3a10b177643bf2630 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Sat, 14 Dec 2024 16:11:53 +0100 Subject: [PATCH 08/15] query store CRUD --- src/command/queryStoreCommand.ts | 12 +++ src/command/queryStoreCommandHandler.ts | 66 ++++++++++++++ .../admin/queryBuilderController.ts | 8 +- src/controller/admin/queryStoreController.ts | 91 +++++++++++++++++++ src/data-source.ts | 4 + src/entity/query.ts | 10 ++ src/factory/admin/queryStore.ts | 25 +++++ src/migrations/1734187754677-queryStore.ts | 25 +++++ src/routes/admin/index.ts | 2 + src/routes/admin/queryStore.ts | 45 +++++++++ src/service/queryStoreService.ts | 40 ++++++++ src/viewmodel/admin/queryStore.models.ts | 4 + 12 files changed, 327 insertions(+), 5 deletions(-) create mode 100644 src/command/queryStoreCommand.ts create mode 100644 src/command/queryStoreCommandHandler.ts create mode 100644 src/controller/admin/queryStoreController.ts create mode 100644 src/entity/query.ts create mode 100644 src/factory/admin/queryStore.ts create mode 100644 src/migrations/1734187754677-queryStore.ts create mode 100644 src/routes/admin/queryStore.ts create mode 100644 src/service/queryStoreService.ts create mode 100644 src/viewmodel/admin/queryStore.models.ts diff --git a/src/command/queryStoreCommand.ts b/src/command/queryStoreCommand.ts new file mode 100644 index 0000000..4404fbe --- /dev/null +++ b/src/command/queryStoreCommand.ts @@ -0,0 +1,12 @@ +export interface CreateQueryStoreCommand { + query: string; +} + +export interface UpdateQueryStoreCommand { + id: number; + query: string; +} + +export interface DeleteQueryStoreCommand { + id: number; +} diff --git a/src/command/queryStoreCommandHandler.ts b/src/command/queryStoreCommandHandler.ts new file mode 100644 index 0000000..7981204 --- /dev/null +++ b/src/command/queryStoreCommandHandler.ts @@ -0,0 +1,66 @@ +import { dataSource } from "../data-source"; +import { query } from "../entity/query"; +import InternalException from "../exceptions/internalException"; +import { CreateQueryStoreCommand, DeleteQueryStoreCommand, UpdateQueryStoreCommand } from "./queryStoreCommand"; + +export default abstract class QueryStoreCommandHandler { + /** + * @description create queryStore + * @param CreateQueryStoreCommand + * @returns {Promise} + */ + static async create(createQueryStore: CreateQueryStoreCommand): Promise { + return await dataSource + .createQueryBuilder() + .insert() + .into(query) + .values({ + query: createQueryStore.query, + }) + .execute() + .then((result) => { + return result.identifiers[0].id; + }) + .catch((err) => { + throw new InternalException("Failed creating queryStore", err); + }); + } + + /** + * @description update queryStore + * @param UpdateQueryStoreCommand + * @returns {Promise} + */ + static async update(updateQueryStore: UpdateQueryStoreCommand): Promise { + return await dataSource + .createQueryBuilder() + .update(query) + .set({ + queryStore: updateQueryStore.query, + }) + .where("id = :id", { id: updateQueryStore.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed updating queryStore", err); + }); + } + + /** + * @description delete queryStore + * @param DeleteQueryStoreCommand + * @returns {Promise} + */ + static async delete(deletQueryStore: DeleteQueryStoreCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(query) + .where("id = :id", { id: deletQueryStore.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed deleting queryStore", err); + }); + } +} diff --git a/src/controller/admin/queryBuilderController.ts b/src/controller/admin/queryBuilderController.ts index ccab8df..cd08b8d 100644 --- a/src/controller/admin/queryBuilderController.ts +++ b/src/controller/admin/queryBuilderController.ts @@ -37,13 +37,11 @@ export async function executeQuery(req: Request, res: Response): Promise { let count = parseInt((req.query.count as string) ?? "25"); const query = req.body.query; - //build query to sql - //verify sql or return error - //let [rows, total] = await executeQuery(query, offset, count); + let [rows, total] = await DynamicQueryBuilder.buildQuery(query, offset, count).getManyAndCount(); res.json({ - rows: [], - total: 0, + rows: rows, + total: total, offset: offset, count: count, }); diff --git a/src/controller/admin/queryStoreController.ts b/src/controller/admin/queryStoreController.ts new file mode 100644 index 0000000..6207e97 --- /dev/null +++ b/src/controller/admin/queryStoreController.ts @@ -0,0 +1,91 @@ +import { Request, Response } from "express"; +import QueryStoreFactory from "../../factory/admin/queryStore"; +import QueryStoreService from "../../service/queryStoreService"; +import { + CreateQueryStoreCommand, + DeleteQueryStoreCommand, + UpdateQueryStoreCommand, +} from "../../command/queryStoreCommand"; +import QueryStoreCommandHandler from "../../command/queryStoreCommandHandler"; + +/** + * @description get all queryStores + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getAllQueryStores(req: Request, res: Response): Promise { + let queryStores = await QueryStoreService.getAll(); + + res.json(QueryStoreFactory.mapToBase(queryStores)); +} + +/** + * @description get queryStore by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getQueryStoreById(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + + let queryStore = await QueryStoreService.getById(id); + + res.json(QueryStoreFactory.mapToSingle(queryStore)); +} + +/** + * @description create new queryStore + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function createQueryStore(req: Request, res: Response): Promise { + const query = req.body.query; + + let createQueryStore: CreateQueryStoreCommand = { + query: query, + }; + + await QueryStoreCommandHandler.create(createQueryStore); + + res.sendStatus(204); +} + +/** + * @description update queryStore + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateQueryStore(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + const query = req.body.query; + + let updateQueryStore: UpdateQueryStoreCommand = { + id: id, + query: query, + }; + + await QueryStoreCommandHandler.update(updateQueryStore); + + res.sendStatus(204); +} + +/** + * @description delete queryStore + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function deleteQueryStore(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + + let deleteQueryStore: DeleteQueryStoreCommand = { + id: id, + }; + + await QueryStoreCommandHandler.delete(deleteQueryStore); + + res.sendStatus(204); +} diff --git a/src/data-source.ts b/src/data-source.ts index 5271c7c..e4bb7af 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -44,6 +44,8 @@ import { reset } from "./entity/reset"; import { ResetToken1732358596823 } from "./migrations/1732358596823-resetToken"; import { SMSAlarming1732696919191 } from "./migrations/1732696919191-SMSAlarming"; import { SecuringCalendarType1733249553766 } from "./migrations/1733249553766-securingCalendarType"; +import { query } from "./entity/query"; +import { QueryStore1734187754677 } from "./migrations/1734187754677-queryStore"; const dataSource = new DataSource({ type: DB_TYPE as any, @@ -82,6 +84,7 @@ const dataSource = new DataSource({ protocolPrintout, calendar, calendarType, + query, ], migrations: [ Initial1724317398939, @@ -98,6 +101,7 @@ const dataSource = new DataSource({ ResetToken1732358596823, SMSAlarming1732696919191, SecuringCalendarType1733249553766, + QueryStore1734187754677, ], migrationsRun: true, migrationsTransactionMode: "each", diff --git a/src/entity/query.ts b/src/entity/query.ts new file mode 100644 index 0000000..b03d4e2 --- /dev/null +++ b/src/entity/query.ts @@ -0,0 +1,10 @@ +import { Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity() +export class query { + @PrimaryColumn({ generated: "increment", type: "int" }) + id: number; + + @Column({ type: "text", default: "" }) + query: string; +} diff --git a/src/factory/admin/queryStore.ts b/src/factory/admin/queryStore.ts new file mode 100644 index 0000000..478e48b --- /dev/null +++ b/src/factory/admin/queryStore.ts @@ -0,0 +1,25 @@ +import { query } from "../../entity/query"; +import { QueryStoreViewModel } from "../../viewmodel/admin/queryStore.models"; + +export default abstract class QueryStoreFactory { + /** + * @description map record to queryStore + * @param {queryStore} record + * @returns {QueryStoreViewModel} + */ + public static mapToSingle(record: query): QueryStoreViewModel { + return { + id: record.id, + query: record.query, + }; + } + + /** + * @description map records to queryStore + * @param {Array} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/migrations/1734187754677-queryStore.ts b/src/migrations/1734187754677-queryStore.ts new file mode 100644 index 0000000..b86fa23 --- /dev/null +++ b/src/migrations/1734187754677-queryStore.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner, Table } from "typeorm"; +import { DB_TYPE } from "../env.defaults"; + +export class QueryStore1734187754677 implements MigrationInterface { + name = "QueryStore1734187754677"; + + public async up(queryRunner: QueryRunner): Promise { + const variableType_int = DB_TYPE == "mysql" ? "int" : "integer"; + + await queryRunner.createTable( + new Table({ + name: "query", + columns: [ + { name: "id", type: variableType_int, isPrimary: true, isGenerated: true, generationStrategy: "increment" }, + { name: "query", type: "text", isNullable: false, default: "''" }, + ], + }), + true + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("query"); + } +} diff --git a/src/routes/admin/index.ts b/src/routes/admin/index.ts index 2a292f3..193213e 100644 --- a/src/routes/admin/index.ts +++ b/src/routes/admin/index.ts @@ -7,6 +7,7 @@ import executivePosition from "./executivePosition"; import membershipStatus from "./membershipStatus"; import qualification from "./qualification"; import calendarType from "./calendarType"; +import queryStore from "./queryStore"; import member from "./member"; import protocol from "./protocol"; @@ -37,6 +38,7 @@ router.use( ); router.use("/qualification", PermissionHelper.passCheckMiddleware("read", "settings", "qualification"), qualification); router.use("/calendartype", PermissionHelper.passCheckMiddleware("read", "settings", "calendar_type"), calendarType); +router.use("/querystore", PermissionHelper.passCheckMiddleware("read", "settings", "query_store"), queryStore); router.use("/member", PermissionHelper.passCheckMiddleware("read", "club", "member"), member); router.use("/protocol", PermissionHelper.passCheckMiddleware("read", "club", "protocol"), protocol); diff --git a/src/routes/admin/queryStore.ts b/src/routes/admin/queryStore.ts new file mode 100644 index 0000000..e160187 --- /dev/null +++ b/src/routes/admin/queryStore.ts @@ -0,0 +1,45 @@ +import express, { Request, Response } from "express"; +import PermissionHelper from "../../helpers/permissionHelper"; +import { + createQueryStore, + deleteQueryStore, + getAllQueryStores, + getQueryStoreById, + updateQueryStore, +} from "../../controller/admin/queryStoreController"; + +var router = express.Router({ mergeParams: true }); + +router.get("/", async (req: Request, res: Response) => { + await getAllQueryStores(req, res); +}); + +router.get("/:id", async (req: Request, res: Response) => { + await getQueryStoreById(req, res); +}); + +router.post( + "/", + PermissionHelper.passCheckMiddleware("create", "settings", "query_store"), + async (req: Request, res: Response) => { + await createQueryStore(req, res); + } +); + +router.patch( + "/:id", + PermissionHelper.passCheckMiddleware("update", "settings", "query_store"), + async (req: Request, res: Response) => { + await updateQueryStore(req, res); + } +); + +router.delete( + "/:id", + PermissionHelper.passCheckMiddleware("delete", "settings", "query_store"), + async (req: Request, res: Response) => { + await deleteQueryStore(req, res); + } +); + +export default router; diff --git a/src/service/queryStoreService.ts b/src/service/queryStoreService.ts new file mode 100644 index 0000000..93145f9 --- /dev/null +++ b/src/service/queryStoreService.ts @@ -0,0 +1,40 @@ +import { dataSource } from "../data-source"; +import { query } from "../entity/query"; +import InternalException from "../exceptions/internalException"; + +export default abstract class QueryStoreService { + /** + * @description get all queryStores + * @returns {Promise>} + */ + static async getAll(): Promise> { + return await dataSource + .getRepository(query) + .createQueryBuilder("queryStore") + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("queryStores not found", err); + }); + } + + /** + * @description get queryStore by id + * @returns {Promise} + */ + static async getById(id: number): Promise { + return await dataSource + .getRepository(query) + .createQueryBuilder("queryStore") + .where("queryStore.id = :id", { id: id }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("queryStore not found by id", err); + }); + } +} diff --git a/src/viewmodel/admin/queryStore.models.ts b/src/viewmodel/admin/queryStore.models.ts new file mode 100644 index 0000000..10d547b --- /dev/null +++ b/src/viewmodel/admin/queryStore.models.ts @@ -0,0 +1,4 @@ +export interface QueryStoreViewModel { + id: number; + query: string; +} From ac5a7e7427a14af535ceeb9851676cdf1334c853 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Mon, 16 Dec 2024 13:56:22 +0100 Subject: [PATCH 09/15] error pass and select fix --- .../admin/queryBuilderController.ts | 22 +++++++++++++------ src/helpers/dynamicQueryBuilder.ts | 15 +++++++++++-- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/controller/admin/queryBuilderController.ts b/src/controller/admin/queryBuilderController.ts index cd08b8d..fdf93a1 100644 --- a/src/controller/admin/queryBuilderController.ts +++ b/src/controller/admin/queryBuilderController.ts @@ -37,12 +37,20 @@ export async function executeQuery(req: Request, res: Response): Promise { let count = parseInt((req.query.count as string) ?? "25"); const query = req.body.query; - let [rows, total] = await DynamicQueryBuilder.buildQuery(query, offset, count).getManyAndCount(); + try { + let [rows, total] = await DynamicQueryBuilder.buildQuery(query, offset, count).getManyAndCount(); - res.json({ - rows: rows, - total: total, - offset: offset, - count: count, - }); + res.json({ + rows: rows, + total: total, + offset: offset, + count: count, + }); + } catch (error) { + res.status(500).send({ + json: error.sql, + code: error.code, + msg: error.sqlMessage, + }); + } } diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts index e6f655d..43d44c5 100644 --- a/src/helpers/dynamicQueryBuilder.ts +++ b/src/helpers/dynamicQueryBuilder.ts @@ -67,11 +67,22 @@ export default abstract class DynamicQueryBuilder { depth: number = 0 ): void { const alias = queryObject.table + "_" + depth; + let firstSelect = true; + let selects: Array = []; if (queryObject.select == "*") { - query.addSelect(`${alias}.*`); + let meta = this.getTableMeta(queryObject.table); + let relCols = meta.relations.map((r) => r.column); + selects = meta.columns.map((c) => c.column).filter((c) => !relCols.includes(c)); } else { - for (const select of queryObject.select) { + selects = queryObject.select; + } + + for (const select of selects) { + if (firstSelect) { + query.select(`${alias}.${select}`); + firstSelect = false; + } else { query.addSelect(`${alias}.${select}`); } } From 52a1f34162228ed3f9e826dd845cb1eeb5b4971c Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Mon, 16 Dec 2024 17:41:26 +0100 Subject: [PATCH 10/15] state pass --- src/controller/admin/queryBuilderController.ts | 6 ++++-- src/type/dynamicQueries.ts | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/controller/admin/queryBuilderController.ts b/src/controller/admin/queryBuilderController.ts index fdf93a1..312de93 100644 --- a/src/controller/admin/queryBuilderController.ts +++ b/src/controller/admin/queryBuilderController.ts @@ -41,14 +41,16 @@ export async function executeQuery(req: Request, res: Response): Promise { let [rows, total] = await DynamicQueryBuilder.buildQuery(query, offset, count).getManyAndCount(); res.json({ + stats: "success", rows: rows, total: total, offset: offset, count: count, }); } catch (error) { - res.status(500).send({ - json: error.sql, + res.json({ + stats: "error", + sql: error.sql, code: error.code, msg: error.sqlMessage, }); diff --git a/src/type/dynamicQueries.ts b/src/type/dynamicQueries.ts index 5e69094..9e1f046 100644 --- a/src/type/dynamicQueries.ts +++ b/src/type/dynamicQueries.ts @@ -43,7 +43,9 @@ export type WhereOperation = | "notNull" // Is not null | "between" // Is between | "startsWith" // Starts with - | "endsWith"; // Ends with + | "endsWith" // Ends with + | "timespanEq"; // Date before x years (YYYY-01-01 YYYY-12-31) +// TODO: age between | age equals | age greater | age smaller export type OrderByStructure = { column: string; From a718f74d24c30de608139d58a4532bc9c6d72bbc Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Mon, 16 Dec 2024 17:49:30 +0100 Subject: [PATCH 11/15] nested condition --- src/helpers/dynamicQueryBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts index 43d44c5..6bab2dc 100644 --- a/src/helpers/dynamicQueryBuilder.ts +++ b/src/helpers/dynamicQueryBuilder.ts @@ -121,7 +121,7 @@ export default abstract class DynamicQueryBuilder { query.orWhere(whereClause.query, whereClause.parameters); } } else { - if (condition.concat == "AND") { + if (condition.concat == "_" || condition.concat == "AND") { query.andWhere( condition.invert == undefined || condition.invert == true ? new Brackets((qb) => { From f4f293846bbefdaa0531b0ef7c7e132da8e9c9ab Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Tue, 17 Dec 2024 16:52:15 +0100 Subject: [PATCH 12/15] sql query usage --- .../admin/queryBuilderController.ts | 82 +++++++++++++++---- src/helpers/dynamicQueryBuilder.ts | 6 +- 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/controller/admin/queryBuilderController.ts b/src/controller/admin/queryBuilderController.ts index 312de93..be4a68e 100644 --- a/src/controller/admin/queryBuilderController.ts +++ b/src/controller/admin/queryBuilderController.ts @@ -1,5 +1,6 @@ import { Request, Response } from "express"; import DynamicQueryBuilder from "../../helpers/dynamicQueryBuilder"; +import { dataSource } from "../../data-source"; /** * @description get all table metas @@ -37,22 +38,71 @@ export async function executeQuery(req: Request, res: Response): Promise { let count = parseInt((req.query.count as string) ?? "25"); const query = req.body.query; - try { - let [rows, total] = await DynamicQueryBuilder.buildQuery(query, offset, count).getManyAndCount(); + if (typeof query == "string") { + const upperQuery = query.trim().toUpperCase(); + if (!upperQuery.startsWith("SELECT") || /INSERT|UPDATE|DELETE|ALTER|DROP|CREATE|TRUNCATE/.test(upperQuery)) { + return res.json({ + stats: "error", + sql: query, + code: "UNALLOWED", + msg: "Not allowed to change rows", + }); + } - res.json({ - stats: "success", - rows: rows, - total: total, - offset: offset, - count: count, - }); - } catch (error) { - res.json({ - stats: "error", - sql: error.sql, - code: error.code, - msg: error.sqlMessage, - }); + try { + let data: Array = []; + + const result = await dataSource + .transaction(async (manager) => { + data = await manager.query(query); + + throw new Error("AllwaysRollbackQuery"); + }) + .catch((error) => { + if (error.message === "AllwaysRollbackQuery") { + return { + stats: "success", + rows: data, + total: data.length, + offset: offset, + count: count, + }; + } else { + return { + stats: "error", + sql: error.sql, + code: error.code, + msg: error.sqlMessage, + }; + } + }); + res.send(result); + } catch (error) { + res.json({ + stats: "error", + sql: error.sql, + code: error.code, + msg: error.sqlMessage, + }); + } + } else { + try { + let [rows, total] = await DynamicQueryBuilder.buildQuery(query, offset, count).getManyAndCount(); + + res.json({ + stats: "success", + rows: rows, + total: total, + offset: offset, + count: count, + }); + } catch (error) { + res.json({ + stats: "error", + sql: error.sql, + code: error.code, + msg: error.sqlMessage, + }); + } } } diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts index 6bab2dc..effab6e 100644 --- a/src/helpers/dynamicQueryBuilder.ts +++ b/src/helpers/dynamicQueryBuilder.ts @@ -79,7 +79,7 @@ export default abstract class DynamicQueryBuilder { } for (const select of selects) { - if (firstSelect) { + if (firstSelect && depth == 0) { query.select(`${alias}.${select}`); firstSelect = false; } else { @@ -214,6 +214,10 @@ export default abstract class DynamicQueryBuilder { query += ` LIKE :${parameterKey}`; parameters[parameterKey] = `%${condition.value}`; break; + case "timespanEq": + query += ` BETWEEN :${parameterKey}_start AND :${parameterKey}_end`; + parameters[`${parameterKey}_start`] = new Date(new Date().getFullYear() - (condition.value as number), 0, 1); + parameters[`${parameterKey}_end`] = new Date(new Date().getFullYear() - (condition.value as number), 11, 31); } return { query, parameters }; From 7ca51c3670cce6110b52f52de003986c55f36d5b Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Wed, 18 Dec 2024 12:55:03 +0100 Subject: [PATCH 13/15] member data views --- src/data-source.ts | 10 ++ src/helpers/dynamicQueryBuilder.ts | 4 + src/index.ts | 3 +- .../1734520998539-memberDataViews.ts | 106 ++++++++++++++++++ src/views/memberExecutivePositionView.ts | 53 +++++++++ src/views/memberQualificationsView.ts | 53 +++++++++ src/views/memberView.ts | 37 ++++++ src/views/membershipsView.ts | 50 +++++++++ 8 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 src/migrations/1734520998539-memberDataViews.ts create mode 100644 src/views/memberExecutivePositionView.ts create mode 100644 src/views/memberQualificationsView.ts create mode 100644 src/views/memberView.ts create mode 100644 src/views/membershipsView.ts diff --git a/src/data-source.ts b/src/data-source.ts index e4bb7af..685d05c 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -46,6 +46,11 @@ import { SMSAlarming1732696919191 } from "./migrations/1732696919191-SMSAlarming import { SecuringCalendarType1733249553766 } from "./migrations/1733249553766-securingCalendarType"; import { query } from "./entity/query"; import { QueryStore1734187754677 } from "./migrations/1734187754677-queryStore"; +import { memberView } from "./views/memberView"; +import { memberExecutivePositionsView } from "./views/memberExecutivePositionView"; +import { memberQualificationsView } from "./views/memberQualificationsView"; +import { membershipView } from "./views/membershipsView"; +import { MemberDataViews1734520998539 } from "./migrations/1734520998539-memberDataViews"; const dataSource = new DataSource({ type: DB_TYPE as any, @@ -85,6 +90,10 @@ const dataSource = new DataSource({ calendar, calendarType, query, + memberView, + memberExecutivePositionsView, + memberQualificationsView, + membershipView, ], migrations: [ Initial1724317398939, @@ -102,6 +111,7 @@ const dataSource = new DataSource({ SMSAlarming1732696919191, SecuringCalendarType1733249553766, QueryStore1734187754677, + MemberDataViews1734520998539, ], migrationsRun: true, migrationsTransactionMode: "each", diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts index effab6e..ad0ba7b 100644 --- a/src/helpers/dynamicQueryBuilder.ts +++ b/src/helpers/dynamicQueryBuilder.ts @@ -16,6 +16,10 @@ export default abstract class DynamicQueryBuilder { "memberExecutivePositions", "memberQualifications", "membership", + "memberView", + "memberExecutivePositionsView", + "memberQualificationsView", + "membershipView", ]; public static getTableMeta(tableName: string): TableMeta { diff --git a/src/index.ts b/src/index.ts index 38374a4..b28e07a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import express from "express"; import { configCheck, SERVER_PORT } from "./env.defaults"; configCheck(); +import { PermissionObject } from "./type/permissionTypes"; declare global { namespace Express { export interface Request { @@ -16,12 +17,10 @@ declare global { } import { dataSource } from "./data-source"; - dataSource.initialize(); const app = express(); import router from "./routes/index"; -import { PermissionObject } from "./type/permissionTypes"; router(app); app.listen(process.env.NODE_ENV ? SERVER_PORT : 5000, () => { console.log(`listening on *:${SERVER_PORT}`); diff --git a/src/migrations/1734520998539-memberDataViews.ts b/src/migrations/1734520998539-memberDataViews.ts new file mode 100644 index 0000000..11b9656 --- /dev/null +++ b/src/migrations/1734520998539-memberDataViews.ts @@ -0,0 +1,106 @@ +import { DataSource, MigrationInterface, QueryRunner, View } from "typeorm"; +import { member } from "../entity/member"; +import { memberExecutivePositions } from "../entity/memberExecutivePositions"; +import { memberQualifications } from "../entity/memberQualifications"; +import { membership } from "../entity/membership"; + +export class MemberDataViews1734520998539 implements MigrationInterface { + name = "MemberDataViews1734520998539"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createView( + new View({ + name: "member_view", + expression: (datasource: DataSource) => + datasource + .getRepository(member) + .createQueryBuilder("member") + .addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge") + .addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear"), + }), + true + ); + await queryRunner.createView( + new View({ + name: "member_executive_positions_view", + expression: (datasource: DataSource) => + datasource + .getRepository(memberExecutivePositions) + .createQueryBuilder("memberExecutivePositions") + .select("executivePosition.id", "positionId") + .addSelect("executivePosition.position", "position") + .addSelect("member.id", "memberId") + .addSelect("member.salutation", "memberSalutation") + .addSelect("member.firstname", "memberFirstname") + .addSelect("member.lastname", "memberLastname") + .addSelect("member.nameaffix", "memberNameaffix") + .addSelect("member.birthdate", "memberBirthdate") + .addSelect( + "SUM(TIMESTAMPDIFF(DAY, memberExecutivePositions.start, COALESCE(memberExecutivePositions.end, CURRENT_DATE)))", + "durationInDays" + ) + .leftJoin("memberExecutivePositions.executivePosition", "executivePosition") + .leftJoin("memberExecutivePositions.member", "member") + .groupBy("executivePosition.id"), + }), + true + ); + await queryRunner.createView( + new View({ + name: "member_qualifications_view", + expression: (datasource: DataSource) => + datasource + .getRepository(memberQualifications) + .createQueryBuilder("memberQualifications") + .select("qualification.id", "qualificationId") + .addSelect("qualification.qualification", "qualification") + .addSelect("member.id", "memberId") + .addSelect("member.salutation", "memberSalutation") + .addSelect("member.firstname", "memberFirstname") + .addSelect("member.lastname", "memberLastname") + .addSelect("member.nameaffix", "memberNameaffix") + .addSelect("member.birthdate", "memberBirthdate") + .addSelect( + "SUM(TIMESTAMPDIFF(DAY, memberQualifications.start, COALESCE(memberQualifications.end, CURRENT_DATE)))", + "durationInDays" + ) + .leftJoin("memberQualifications.qualification", "qualification") + .leftJoin("memberQualifications.member", "member") + .groupBy("qualification.id"), + }), + true + ); + await queryRunner.createView( + new View({ + name: "membership_view", + expression: (datasource: DataSource) => + datasource + .getRepository(membership) + .createQueryBuilder("membership") + .select("status.id", "statusId") + .addSelect("status.status", "status") + .addSelect("member.id", "memberId") + .addSelect("member.salutation", "memberSalutation") + .addSelect("member.firstname", "memberFirstname") + .addSelect("member.lastname", "memberLastname") + .addSelect("member.nameaffix", "memberNameaffix") + .addSelect("member.birthdate", "memberBirthdate") + .addSelect( + "SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))", + "durationInDays" + ) + .leftJoin("membership.status", "status") + .leftJoin("membership.member", "member") + .groupBy("status.id"), + }), + true + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropView("membership_view"); + await queryRunner.dropView("member_qualifications_view"); + await queryRunner.dropView("member_executive_positions_view"); + await queryRunner.dropView("member_view"); + } +} diff --git a/src/views/memberExecutivePositionView.ts b/src/views/memberExecutivePositionView.ts new file mode 100644 index 0000000..ffc859e --- /dev/null +++ b/src/views/memberExecutivePositionView.ts @@ -0,0 +1,53 @@ +import { DataSource, ViewColumn, ViewEntity } from "typeorm"; +import { memberExecutivePositions } from "../entity/memberExecutivePositions"; +import { Salutation } from "../enums/salutation"; + +@ViewEntity({ + expression: (datasource: DataSource) => + datasource + .getRepository(memberExecutivePositions) + .createQueryBuilder("memberExecutivePositions") + .select("executivePosition.id", "positionId") + .addSelect("executivePosition.position", "position") + .addSelect("member.id", "memberId") + .addSelect("member.salutation", "memberSalutation") + .addSelect("member.firstname", "memberFirstname") + .addSelect("member.lastname", "memberLastname") + .addSelect("member.nameaffix", "memberNameaffix") + .addSelect("member.birthdate", "memberBirthdate") + .addSelect( + "SUM(TIMESTAMPDIFF(DAY, memberExecutivePositions.start, COALESCE(memberExecutivePositions.end, CURRENT_DATE)))", + "durationInDays" + ) + .leftJoin("memberExecutivePositions.executivePosition", "executivePosition") + .leftJoin("memberExecutivePositions.member", "member") + .groupBy("executivePosition.id"), +}) +export class memberExecutivePositionsView { + @ViewColumn() + durationInDays: number; + + @ViewColumn() + position: string; + + @ViewColumn() + positionId: number; + + @ViewColumn() + memberId: number; + + @ViewColumn() + memberSalutation: Salutation; + + @ViewColumn() + memberFirstname: string; + + @ViewColumn() + memberLastname: string; + + @ViewColumn() + memberNameaffix: string; + + @ViewColumn() + memberBirthdate: Date; +} diff --git a/src/views/memberQualificationsView.ts b/src/views/memberQualificationsView.ts new file mode 100644 index 0000000..9cd8bd6 --- /dev/null +++ b/src/views/memberQualificationsView.ts @@ -0,0 +1,53 @@ +import { DataSource, ViewColumn, ViewEntity } from "typeorm"; +import { memberQualifications } from "../entity/memberQualifications"; +import { Salutation } from "../enums/salutation"; + +@ViewEntity({ + expression: (datasource: DataSource) => + datasource + .getRepository(memberQualifications) + .createQueryBuilder("memberQualifications") + .select("qualification.id", "qualificationId") + .addSelect("qualification.qualification", "qualification") + .addSelect("member.id", "memberId") + .addSelect("member.salutation", "memberSalutation") + .addSelect("member.firstname", "memberFirstname") + .addSelect("member.lastname", "memberLastname") + .addSelect("member.nameaffix", "memberNameaffix") + .addSelect("member.birthdate", "memberBirthdate") + .addSelect( + "SUM(TIMESTAMPDIFF(DAY, memberQualifications.start, COALESCE(memberQualifications.end, CURRENT_DATE)))", + "durationInDays" + ) + .leftJoin("memberQualifications.qualification", "qualification") + .leftJoin("memberQualifications.member", "member") + .groupBy("qualification.id"), +}) +export class memberQualificationsView { + @ViewColumn() + durationInDays: number; + + @ViewColumn() + qualification: string; + + @ViewColumn() + qualificationId: number; + + @ViewColumn() + memberId: number; + + @ViewColumn() + memberSalutation: Salutation; + + @ViewColumn() + memberFirstname: string; + + @ViewColumn() + memberLastname: string; + + @ViewColumn() + memberNameaffix: string; + + @ViewColumn() + memberBirthdate: Date; +} diff --git a/src/views/memberView.ts b/src/views/memberView.ts new file mode 100644 index 0000000..d28c944 --- /dev/null +++ b/src/views/memberView.ts @@ -0,0 +1,37 @@ +import { DataSource, ViewColumn, ViewEntity } from "typeorm"; +import { member } from "../entity/member"; +import { Salutation } from "../enums/salutation"; + +@ViewEntity({ + expression: (datasource: DataSource) => + datasource + .getRepository(member) + .createQueryBuilder("member") + .addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge") + .addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear"), +}) +export class memberView { + @ViewColumn() + id: number; + + @ViewColumn() + salutation: Salutation; + + @ViewColumn() + firstname: string; + + @ViewColumn() + lastname: string; + + @ViewColumn() + nameaffix: string; + + @ViewColumn() + birthdate: Date; + + @ViewColumn() + todayAge: number; + + @ViewColumn() + ageThisYear: number; +} diff --git a/src/views/membershipsView.ts b/src/views/membershipsView.ts new file mode 100644 index 0000000..6d72bcc --- /dev/null +++ b/src/views/membershipsView.ts @@ -0,0 +1,50 @@ +import { DataSource, ViewColumn, ViewEntity } from "typeorm"; +import { membership } from "../entity/membership"; +import { Salutation } from "../enums/salutation"; + +@ViewEntity({ + expression: (datasource: DataSource) => + datasource + .getRepository(membership) + .createQueryBuilder("membership") + .select("status.id", "statusId") + .addSelect("status.status", "status") + .addSelect("member.id", "memberId") + .addSelect("member.salutation", "memberSalutation") + .addSelect("member.firstname", "memberFirstname") + .addSelect("member.lastname", "memberLastname") + .addSelect("member.nameaffix", "memberNameaffix") + .addSelect("member.birthdate", "memberBirthdate") + .addSelect("SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))", "durationInDays") + .leftJoin("membership.status", "status") + .leftJoin("membership.member", "member") + .groupBy("status.id"), +}) +export class membershipView { + @ViewColumn() + durationInDays: number; + + @ViewColumn() + status: string; + + @ViewColumn() + statusId: number; + + @ViewColumn() + memberId: number; + + @ViewColumn() + memberSalutation: Salutation; + + @ViewColumn() + memberFirstname: string; + + @ViewColumn() + memberLastname: string; + + @ViewColumn() + memberNameaffix: string; + + @ViewColumn() + memberBirthdate: Date; +} From 6b7b5a99d6d9bd9bd2b3289b6773b2f1a2b6c805 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Wed, 18 Dec 2024 22:27:33 +0100 Subject: [PATCH 14/15] query store --- src/command/queryStoreCommand.ts | 7 +++++-- src/command/queryStoreCommandHandler.ts | 7 +++++-- src/controller/admin/queryStoreController.ts | 6 ++++-- src/entity/query.ts | 3 +++ src/factory/admin/queryStore.ts | 3 ++- src/migrations/1734187754677-queryStore.ts | 1 + src/migrations/1734520998539-memberDataViews.ts | 6 ++++++ src/viewmodel/admin/queryStore.models.ts | 5 ++++- src/views/memberView.ts | 6 ++++++ 9 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/command/queryStoreCommand.ts b/src/command/queryStoreCommand.ts index 4404fbe..7043159 100644 --- a/src/command/queryStoreCommand.ts +++ b/src/command/queryStoreCommand.ts @@ -1,10 +1,13 @@ +import { DynamicQueryStructure } from "../type/dynamicQueries"; + export interface CreateQueryStoreCommand { - query: string; + title: string; + query: string | DynamicQueryStructure; } export interface UpdateQueryStoreCommand { id: number; - query: string; + query: string | DynamicQueryStructure; } export interface DeleteQueryStoreCommand { diff --git a/src/command/queryStoreCommandHandler.ts b/src/command/queryStoreCommandHandler.ts index 7981204..7eeb80c 100644 --- a/src/command/queryStoreCommandHandler.ts +++ b/src/command/queryStoreCommandHandler.ts @@ -15,7 +15,9 @@ export default abstract class QueryStoreCommandHandler { .insert() .into(query) .values({ - query: createQueryStore.query, + title: createQueryStore.title, + query: + typeof createQueryStore.query == "string" ? createQueryStore.query : JSON.stringify(createQueryStore.query), }) .execute() .then((result) => { @@ -36,7 +38,8 @@ export default abstract class QueryStoreCommandHandler { .createQueryBuilder() .update(query) .set({ - queryStore: updateQueryStore.query, + query: + typeof updateQueryStore.query == "string" ? updateQueryStore.query : JSON.stringify(updateQueryStore.query), }) .where("id = :id", { id: updateQueryStore.id }) .execute() diff --git a/src/controller/admin/queryStoreController.ts b/src/controller/admin/queryStoreController.ts index 6207e97..6c6e9c6 100644 --- a/src/controller/admin/queryStoreController.ts +++ b/src/controller/admin/queryStoreController.ts @@ -42,14 +42,16 @@ export async function getQueryStoreById(req: Request, res: Response): Promise { const query = req.body.query; + const title = req.body.title; let createQueryStore: CreateQueryStoreCommand = { + title: title, query: query, }; - await QueryStoreCommandHandler.create(createQueryStore); + let id = await QueryStoreCommandHandler.create(createQueryStore); - res.sendStatus(204); + res.status(200).send(id); } /** diff --git a/src/entity/query.ts b/src/entity/query.ts index b03d4e2..eb1bfe7 100644 --- a/src/entity/query.ts +++ b/src/entity/query.ts @@ -5,6 +5,9 @@ export class query { @PrimaryColumn({ generated: "increment", type: "int" }) id: number; + @Column({ type: "varchar", length: 255, unique: true }) + title: string; + @Column({ type: "text", default: "" }) query: string; } diff --git a/src/factory/admin/queryStore.ts b/src/factory/admin/queryStore.ts index 478e48b..7574ab8 100644 --- a/src/factory/admin/queryStore.ts +++ b/src/factory/admin/queryStore.ts @@ -10,7 +10,8 @@ export default abstract class QueryStoreFactory { public static mapToSingle(record: query): QueryStoreViewModel { return { id: record.id, - query: record.query, + title: record.title, + query: record.query.startsWith("{") ? JSON.parse(record.query) : record.query, }; } diff --git a/src/migrations/1734187754677-queryStore.ts b/src/migrations/1734187754677-queryStore.ts index b86fa23..6912110 100644 --- a/src/migrations/1734187754677-queryStore.ts +++ b/src/migrations/1734187754677-queryStore.ts @@ -12,6 +12,7 @@ export class QueryStore1734187754677 implements MigrationInterface { name: "query", columns: [ { name: "id", type: variableType_int, isPrimary: true, isGenerated: true, generationStrategy: "increment" }, + { name: "title", type: "varchar", length: "255", isNullable: false, isUnique: true }, { name: "query", type: "text", isNullable: false, default: "''" }, ], }), diff --git a/src/migrations/1734520998539-memberDataViews.ts b/src/migrations/1734520998539-memberDataViews.ts index 11b9656..d03fd94 100644 --- a/src/migrations/1734520998539-memberDataViews.ts +++ b/src/migrations/1734520998539-memberDataViews.ts @@ -15,6 +15,12 @@ export class MemberDataViews1734520998539 implements MigrationInterface { datasource .getRepository(member) .createQueryBuilder("member") + .select("member.id", "id") + .addSelect("member.salutation", "salutation") + .addSelect("member.firstname", "firstname") + .addSelect("member.lastname", "lastname") + .addSelect("member.nameaffix", "nameaffix") + .addSelect("member.birthdate", "birthdate") .addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge") .addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear"), }), diff --git a/src/viewmodel/admin/queryStore.models.ts b/src/viewmodel/admin/queryStore.models.ts index 10d547b..ff697c6 100644 --- a/src/viewmodel/admin/queryStore.models.ts +++ b/src/viewmodel/admin/queryStore.models.ts @@ -1,4 +1,7 @@ +import { DynamicQueryStructure } from "../../type/dynamicQueries"; + export interface QueryStoreViewModel { id: number; - query: string; + title: string; + query: string | DynamicQueryStructure; } diff --git a/src/views/memberView.ts b/src/views/memberView.ts index d28c944..08f147c 100644 --- a/src/views/memberView.ts +++ b/src/views/memberView.ts @@ -7,6 +7,12 @@ import { Salutation } from "../enums/salutation"; datasource .getRepository(member) .createQueryBuilder("member") + .select("member.id", "id") + .addSelect("member.salutation", "salutation") + .addSelect("member.firstname", "firstname") + .addSelect("member.lastname", "lastname") + .addSelect("member.nameaffix", "nameaffix") + .addSelect("member.birthdate", "birthdate") .addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge") .addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear"), }) From b4cdf4899a54586d7dc23616240397b19cd15b78 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Thu, 19 Dec 2024 10:32:28 +0100 Subject: [PATCH 15/15] prevent duplicate table names by depth --- src/helpers/dynamicQueryBuilder.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts index ad0ba7b..437fd9c 100644 --- a/src/helpers/dynamicQueryBuilder.ts +++ b/src/helpers/dynamicQueryBuilder.ts @@ -55,9 +55,10 @@ export default abstract class DynamicQueryBuilder { offset: number = 0, count: number = 25 ): SelectQueryBuilder { - let query = dataSource.getRepository(queryObj.table).createQueryBuilder(queryObj.table + "_0"); + let affix = Math.random().toString(36).substring(2); + let query = dataSource.getRepository(queryObj.table).createQueryBuilder(`${queryObj.table}_${affix}`); - this.buildDynamicQuery(query, queryObj); + this.buildDynamicQuery(query, queryObj, affix); query.offset(offset); query.limit(count); @@ -68,9 +69,10 @@ export default abstract class DynamicQueryBuilder { private static buildDynamicQuery( query: SelectQueryBuilder, queryObject: DynamicQueryStructure, + affix: string = "", depth: number = 0 ): void { - const alias = queryObject.table + "_" + depth; + const alias = queryObject.table + "_" + affix; let firstSelect = true; let selects: Array = []; @@ -97,9 +99,10 @@ export default abstract class DynamicQueryBuilder { if (queryObject.join) { for (const join of queryObject.join) { - query.leftJoinAndSelect(`${alias}.${join.foreignColumn}`, join.table + "_" + (depth + 1)); + let subaffix = Math.random().toString(36).substring(2); + query.leftJoinAndSelect(`${alias}.${join.foreignColumn}`, join.table + "_" + subaffix); - this.buildDynamicQuery(query, join, depth + 1); + this.buildDynamicQuery(query, join, subaffix, depth + 1); } }