2024-12-14 15:44:17 +01:00
|
|
|
import { Brackets, DataSource, NotBrackets, ObjectLiteral, SelectQueryBuilder, WhereExpressionBuilder } from "typeorm";
|
2024-11-27 15:01:31 +01:00
|
|
|
import { dataSource } from "../data-source";
|
2024-12-27 13:17:23 +01:00
|
|
|
import { ConditionStructure, DynamicQueryStructure, FieldType, QueryResult } from "../type/dynamicQueries";
|
2024-12-12 16:33:51 +01:00
|
|
|
import { TableMeta } from "../type/tableMeta";
|
2024-11-27 15:01:31 +01:00
|
|
|
|
|
|
|
export default abstract class DynamicQueryBuilder {
|
2024-12-12 16:33:51 +01:00
|
|
|
public static allowedTables: Array<string> = [
|
|
|
|
"award",
|
|
|
|
"communication",
|
|
|
|
"communicationType",
|
|
|
|
"executivePosition",
|
|
|
|
"membershipStatus",
|
|
|
|
"qualification",
|
2025-01-25 12:16:20 +01:00
|
|
|
"salutation",
|
2024-12-12 16:33:51 +01:00
|
|
|
"member",
|
|
|
|
"memberAwards",
|
|
|
|
"memberExecutivePositions",
|
|
|
|
"memberQualifications",
|
|
|
|
"membership",
|
2024-12-18 12:55:03 +01:00
|
|
|
"memberView",
|
|
|
|
"memberExecutivePositionsView",
|
|
|
|
"memberQualificationsView",
|
|
|
|
"membershipView",
|
2024-12-12 16:33:51 +01:00
|
|
|
];
|
2024-11-27 15:01:31 +01:00
|
|
|
|
2024-12-12 16:33:51 +01:00
|
|
|
public static getTableMeta(tableName: string): TableMeta {
|
2024-11-27 15:01:31 +01:00
|
|
|
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,
|
|
|
|
})),
|
|
|
|
};
|
|
|
|
}
|
2024-12-12 16:33:51 +01:00
|
|
|
|
|
|
|
public static getAllTableMeta(): Array<TableMeta> {
|
|
|
|
return this.allowedTables.map((table) => this.getTableMeta(table));
|
|
|
|
}
|
|
|
|
|
2025-02-16 13:32:10 +01:00
|
|
|
public static buildQuery({
|
|
|
|
queryObj,
|
|
|
|
offset = 0,
|
|
|
|
count = 25,
|
|
|
|
noLimit = false,
|
|
|
|
}: {
|
|
|
|
queryObj?: DynamicQueryStructure;
|
|
|
|
offset?: number;
|
|
|
|
count?: number;
|
|
|
|
noLimit?: boolean;
|
|
|
|
}): SelectQueryBuilder<ObjectLiteral> {
|
2024-12-19 10:32:28 +01:00
|
|
|
let affix = Math.random().toString(36).substring(2);
|
|
|
|
let query = dataSource.getRepository(queryObj.table).createQueryBuilder(`${queryObj.table}_${affix}`);
|
2024-12-13 16:24:33 +01:00
|
|
|
|
2024-12-19 10:32:28 +01:00
|
|
|
this.buildDynamicQuery(query, queryObj, affix);
|
2024-12-14 15:44:17 +01:00
|
|
|
|
2025-02-16 13:32:10 +01:00
|
|
|
if (!noLimit) {
|
|
|
|
query.offset(offset);
|
|
|
|
query.limit(count);
|
|
|
|
}
|
2024-12-13 16:24:33 +01:00
|
|
|
|
2024-12-14 15:44:17 +01:00
|
|
|
return query;
|
2024-12-13 16:24:33 +01:00
|
|
|
}
|
|
|
|
|
2024-12-14 15:44:17 +01:00
|
|
|
private static buildDynamicQuery(
|
|
|
|
query: SelectQueryBuilder<ObjectLiteral>,
|
2024-12-13 16:24:33 +01:00
|
|
|
queryObject: DynamicQueryStructure,
|
2024-12-19 10:32:28 +01:00
|
|
|
affix: string = "",
|
2024-12-13 16:24:33 +01:00
|
|
|
depth: number = 0
|
2024-12-14 15:44:17 +01:00
|
|
|
): void {
|
2024-12-19 10:32:28 +01:00
|
|
|
const alias = queryObject.table + "_" + affix;
|
2024-12-16 13:56:22 +01:00
|
|
|
let firstSelect = true;
|
|
|
|
let selects: Array<string> = [];
|
2024-12-13 16:24:33 +01:00
|
|
|
|
|
|
|
if (queryObject.select == "*") {
|
2024-12-16 13:56:22 +01:00
|
|
|
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));
|
2024-12-13 16:24:33 +01:00
|
|
|
} else {
|
2024-12-16 13:56:22 +01:00
|
|
|
selects = queryObject.select;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const select of selects) {
|
2024-12-17 16:52:15 +01:00
|
|
|
if (firstSelect && depth == 0) {
|
2024-12-16 13:56:22 +01:00
|
|
|
query.select(`${alias}.${select}`);
|
|
|
|
firstSelect = false;
|
|
|
|
} else {
|
2024-12-14 15:44:17 +01:00
|
|
|
query.addSelect(`${alias}.${select}`);
|
|
|
|
}
|
2024-12-13 16:24:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (queryObject.where) {
|
2024-12-14 15:44:17 +01:00
|
|
|
this.applyWhere(query, queryObject.where, alias);
|
2024-12-13 16:24:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (queryObject.join) {
|
2024-12-14 15:44:17 +01:00
|
|
|
for (const join of queryObject.join) {
|
2024-12-19 10:32:28 +01:00
|
|
|
let subaffix = Math.random().toString(36).substring(2);
|
2024-12-19 12:18:52 +01:00
|
|
|
query.leftJoin(`${alias}.${join.foreignColumn}`, join.table + "_" + subaffix);
|
2024-12-13 16:24:33 +01:00
|
|
|
|
2024-12-19 10:32:28 +01:00
|
|
|
this.buildDynamicQuery(query, join, subaffix, depth + 1);
|
2024-12-14 15:44:17 +01:00
|
|
|
}
|
2024-12-13 16:24:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (queryObject.orderBy) {
|
|
|
|
queryObject.orderBy.forEach((order) => {
|
2024-12-14 15:44:17 +01:00
|
|
|
query.addOrderBy(`${alias}.${order.column}`, order.order);
|
2024-12-13 16:24:33 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-14 15:44:17 +01:00
|
|
|
public static applyWhere(
|
|
|
|
query: SelectQueryBuilder<ObjectLiteral> | WhereExpressionBuilder,
|
2024-12-13 16:24:33 +01:00
|
|
|
conditions: Array<ConditionStructure>,
|
|
|
|
alias: string
|
|
|
|
): void {
|
2024-12-14 15:44:17 +01:00
|
|
|
for (const condition of conditions) {
|
|
|
|
if (condition.structureType == "condition") {
|
2024-12-13 16:24:33 +01:00
|
|
|
const whereClause = this.buildConditionClause(condition, alias);
|
|
|
|
|
2024-12-14 15:44:17 +01:00
|
|
|
if (condition.concat == "_" || condition.concat == "AND") {
|
|
|
|
query.andWhere(whereClause.query, whereClause.parameters);
|
2024-12-13 16:24:33 +01:00
|
|
|
} else {
|
2024-12-14 15:44:17 +01:00
|
|
|
query.orWhere(whereClause.query, whereClause.parameters);
|
|
|
|
}
|
|
|
|
} else {
|
2024-12-16 17:49:30 +01:00
|
|
|
if (condition.concat == "_" || condition.concat == "AND") {
|
2024-12-14 15:44:17 +01:00
|
|
|
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);
|
|
|
|
})
|
|
|
|
);
|
2024-12-13 16:24:33 +01:00
|
|
|
}
|
|
|
|
}
|
2024-12-14 15:44:17 +01:00
|
|
|
}
|
2024-12-13 16:24:33 +01:00
|
|
|
}
|
|
|
|
|
2024-12-14 15:44:17 +01:00
|
|
|
private static buildConditionClause(
|
|
|
|
condition: ConditionStructure & { structureType: "condition" },
|
2024-12-13 16:24:33 +01:00
|
|
|
alias: string
|
|
|
|
): { query: string; parameters: Record<string, unknown> } {
|
|
|
|
const parameterKey = `${alias}_${condition.column}_${Math.random().toString(36).substring(2)}`;
|
|
|
|
let query = `${alias}.${condition.column}`;
|
|
|
|
let parameters: Record<string, unknown> = {};
|
|
|
|
|
|
|
|
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;
|
2024-12-17 16:52:15 +01:00
|
|
|
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);
|
2024-12-13 16:24:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return { query, parameters };
|
|
|
|
}
|
2024-12-27 13:17:23 +01:00
|
|
|
|
|
|
|
public static flattenQueryResult(result: Array<QueryResult>): Array<{ [key: string]: FieldType }> {
|
|
|
|
function flatten(row: QueryResult, prefix: string = ""): Array<{ [key: string]: FieldType }> {
|
|
|
|
let results: Array<{ [key: string]: FieldType }> = [{}];
|
|
|
|
|
|
|
|
for (const key in row) {
|
|
|
|
const value = row[key];
|
|
|
|
const newKey = prefix ? `${prefix}_${key}` : key;
|
|
|
|
|
|
|
|
if (Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null)) {
|
|
|
|
const arrayResults: Array<{ [key: string]: FieldType }> = [];
|
|
|
|
value.forEach((item) => {
|
|
|
|
const flattenedItems = flatten(item, newKey);
|
|
|
|
arrayResults.push(...flattenedItems);
|
|
|
|
});
|
|
|
|
|
|
|
|
const tempResults: Array<{ [key: string]: FieldType }> = [];
|
|
|
|
results.forEach((res) => {
|
|
|
|
arrayResults.forEach((arrRes) => {
|
|
|
|
tempResults.push({ ...res, ...arrRes });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
results = tempResults;
|
2025-01-05 16:09:39 +01:00
|
|
|
} else if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) {
|
2024-12-27 13:17:23 +01:00
|
|
|
const objResults = flatten(value as QueryResult, newKey);
|
|
|
|
const tempResults: Array<{ [key: string]: FieldType }> = [];
|
|
|
|
results.forEach((res) => {
|
|
|
|
objResults.forEach((objRes) => {
|
|
|
|
tempResults.push({ ...res, ...objRes });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
results = tempResults;
|
|
|
|
} else {
|
|
|
|
results.forEach((res) => {
|
2025-01-03 15:20:22 +01:00
|
|
|
if (String(value) != "undefined") res[newKey] = String(value);
|
2024-12-27 13:17:23 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
const flattenedResults: Array<{ [key: string]: FieldType }> = [];
|
|
|
|
|
|
|
|
result.forEach((item) => {
|
|
|
|
const flattenedItems = flatten(item);
|
|
|
|
flattenedResults.push(...flattenedItems);
|
|
|
|
});
|
|
|
|
|
|
|
|
return flattenedResults;
|
|
|
|
}
|
2024-12-30 14:47:00 +01:00
|
|
|
|
2025-02-16 13:32:10 +01:00
|
|
|
public static async executeQuery({
|
|
|
|
query = "",
|
|
|
|
offset = 0,
|
|
|
|
count = 25,
|
|
|
|
noLimit = false,
|
|
|
|
}: {
|
|
|
|
query: string | DynamicQueryStructure;
|
|
|
|
offset?: number;
|
|
|
|
count?: number;
|
|
|
|
noLimit?: boolean;
|
|
|
|
}): Promise<
|
2024-12-30 14:47:00 +01:00
|
|
|
| {
|
|
|
|
stats: "error";
|
|
|
|
sql: string;
|
|
|
|
code: string;
|
|
|
|
msg: string;
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
stats: "success";
|
|
|
|
rows: Array<{ [key: string]: FieldType }>;
|
|
|
|
total: number;
|
|
|
|
offset: number;
|
|
|
|
count: number;
|
|
|
|
}
|
|
|
|
> {
|
2025-03-19 15:19:03 +01:00
|
|
|
if (query == "member") {
|
|
|
|
query = memberQuery;
|
|
|
|
}
|
|
|
|
if (query == "memberByRunningMembership") {
|
|
|
|
query = memberByRunningMembershipQuery;
|
|
|
|
}
|
|
|
|
|
2024-12-30 14:47:00 +01:00
|
|
|
if (typeof query == "string") {
|
|
|
|
const upperQuery = query.trim().toUpperCase();
|
|
|
|
if (!upperQuery.startsWith("SELECT") || /INSERT|UPDATE|DELETE|ALTER|DROP|CREATE|TRUNCATE/.test(upperQuery)) {
|
|
|
|
return {
|
|
|
|
stats: "error",
|
|
|
|
sql: query,
|
|
|
|
code: "UNALLOWED",
|
|
|
|
msg: "Not allowed to change rows",
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
let data: Array<any> = [];
|
|
|
|
|
|
|
|
return await dataSource
|
|
|
|
.transaction(async (manager) => {
|
2025-03-19 15:19:03 +01:00
|
|
|
data = await manager.query(query.toString());
|
2024-12-30 14:47:00 +01:00
|
|
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
return {
|
|
|
|
stats: "error",
|
|
|
|
sql: error.sql,
|
|
|
|
code: error.code,
|
|
|
|
msg: error.sqlMessage,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
try {
|
2025-02-16 13:32:10 +01:00
|
|
|
let [rows, total] = await this.buildQuery({ queryObj: query, offset, count, noLimit }).getManyAndCount();
|
2024-12-30 14:47:00 +01:00
|
|
|
|
|
|
|
return {
|
|
|
|
stats: "success",
|
|
|
|
rows: this.flattenQueryResult(rows),
|
|
|
|
total: total,
|
|
|
|
offset: offset,
|
2025-02-16 13:32:10 +01:00
|
|
|
count: noLimit ? total : count,
|
2024-12-30 14:47:00 +01:00
|
|
|
};
|
|
|
|
} catch (error) {
|
|
|
|
return {
|
|
|
|
stats: "error",
|
|
|
|
sql: error.sql,
|
|
|
|
code: error.code,
|
|
|
|
msg: error.sqlMessage,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-11-27 15:01:31 +01:00
|
|
|
}
|
2025-03-19 15:19:03 +01:00
|
|
|
|
|
|
|
const memberQuery: DynamicQueryStructure = {
|
|
|
|
select: "*",
|
|
|
|
table: "member",
|
|
|
|
orderBy: [
|
|
|
|
{ column: "lastname", order: "ASC" },
|
|
|
|
{ column: "firstname", order: "ASC" },
|
|
|
|
],
|
|
|
|
};
|
|
|
|
|
|
|
|
const memberByRunningMembershipQuery: DynamicQueryStructure = {
|
|
|
|
select: "*",
|
|
|
|
table: "member",
|
|
|
|
join: [
|
|
|
|
{
|
|
|
|
select: "*",
|
|
|
|
table: "membership",
|
|
|
|
where: [{ structureType: "condition", concat: "_", operation: "null", column: "end", value: "" }],
|
|
|
|
foreignColumn: "memberships",
|
|
|
|
},
|
|
|
|
],
|
|
|
|
orderBy: [
|
|
|
|
{ column: "lastname", order: "ASC" },
|
|
|
|
{ column: "firstname", order: "ASC" },
|
|
|
|
],
|
|
|
|
};
|