Merge pull request '#5-intelligent-groups' (#23) from #5-intelligent-groups into main
Reviewed-on: Ehrenamt/member-administration-server#23
This commit is contained in:
commit
e15507dce8
24 changed files with 1177 additions and 12 deletions
15
src/command/queryStoreCommand.ts
Normal file
15
src/command/queryStoreCommand.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { DynamicQueryStructure } from "../type/dynamicQueries";
|
||||||
|
|
||||||
|
export interface CreateQueryStoreCommand {
|
||||||
|
title: string;
|
||||||
|
query: string | DynamicQueryStructure;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateQueryStoreCommand {
|
||||||
|
id: number;
|
||||||
|
query: string | DynamicQueryStructure;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteQueryStoreCommand {
|
||||||
|
id: number;
|
||||||
|
}
|
69
src/command/queryStoreCommandHandler.ts
Normal file
69
src/command/queryStoreCommandHandler.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
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<number>}
|
||||||
|
*/
|
||||||
|
static async create(createQueryStore: CreateQueryStoreCommand): Promise<number> {
|
||||||
|
return await dataSource
|
||||||
|
.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into(query)
|
||||||
|
.values({
|
||||||
|
title: createQueryStore.title,
|
||||||
|
query:
|
||||||
|
typeof createQueryStore.query == "string" ? createQueryStore.query : JSON.stringify(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<void>}
|
||||||
|
*/
|
||||||
|
static async update(updateQueryStore: UpdateQueryStoreCommand): Promise<void> {
|
||||||
|
return await dataSource
|
||||||
|
.createQueryBuilder()
|
||||||
|
.update(query)
|
||||||
|
.set({
|
||||||
|
query:
|
||||||
|
typeof updateQueryStore.query == "string" ? updateQueryStore.query : JSON.stringify(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<void>}
|
||||||
|
*/
|
||||||
|
static async delete(deletQueryStore: DeleteQueryStoreCommand): Promise<void> {
|
||||||
|
return await dataSource
|
||||||
|
.createQueryBuilder()
|
||||||
|
.delete()
|
||||||
|
.from(query)
|
||||||
|
.where("id = :id", { id: deletQueryStore.id })
|
||||||
|
.execute()
|
||||||
|
.then(() => {})
|
||||||
|
.catch((err) => {
|
||||||
|
throw new InternalException("Failed deleting queryStore", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
108
src/controller/admin/queryBuilderController.ts
Normal file
108
src/controller/admin/queryBuilderController.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import DynamicQueryBuilder from "../../helpers/dynamicQueryBuilder";
|
||||||
|
import { dataSource } from "../../data-source";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
let offset = parseInt((req.query.offset as string) ?? "0");
|
||||||
|
let count = parseInt((req.query.count as string) ?? "25");
|
||||||
|
const query = req.body.query;
|
||||||
|
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data: Array<any> = [];
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
93
src/controller/admin/queryStoreController.ts
Normal file
93
src/controller/admin/queryStoreController.ts
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
const query = req.body.query;
|
||||||
|
const title = req.body.title;
|
||||||
|
|
||||||
|
let createQueryStore: CreateQueryStoreCommand = {
|
||||||
|
title: title,
|
||||||
|
query: query,
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = await QueryStoreCommandHandler.create(createQueryStore);
|
||||||
|
|
||||||
|
res.status(200).send(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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<any> {
|
||||||
|
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<any> {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
|
||||||
|
let deleteQueryStore: DeleteQueryStoreCommand = {
|
||||||
|
id: id,
|
||||||
|
};
|
||||||
|
|
||||||
|
await QueryStoreCommandHandler.delete(deleteQueryStore);
|
||||||
|
|
||||||
|
res.sendStatus(204);
|
||||||
|
}
|
|
@ -44,6 +44,13 @@ import { reset } from "./entity/reset";
|
||||||
import { ResetToken1732358596823 } from "./migrations/1732358596823-resetToken";
|
import { ResetToken1732358596823 } from "./migrations/1732358596823-resetToken";
|
||||||
import { SMSAlarming1732696919191 } from "./migrations/1732696919191-SMSAlarming";
|
import { SMSAlarming1732696919191 } from "./migrations/1732696919191-SMSAlarming";
|
||||||
import { SecuringCalendarType1733249553766 } from "./migrations/1733249553766-securingCalendarType";
|
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({
|
const dataSource = new DataSource({
|
||||||
type: DB_TYPE as any,
|
type: DB_TYPE as any,
|
||||||
|
@ -82,6 +89,11 @@ const dataSource = new DataSource({
|
||||||
protocolPrintout,
|
protocolPrintout,
|
||||||
calendar,
|
calendar,
|
||||||
calendarType,
|
calendarType,
|
||||||
|
query,
|
||||||
|
memberView,
|
||||||
|
memberExecutivePositionsView,
|
||||||
|
memberQualificationsView,
|
||||||
|
membershipView,
|
||||||
],
|
],
|
||||||
migrations: [
|
migrations: [
|
||||||
Initial1724317398939,
|
Initial1724317398939,
|
||||||
|
@ -98,6 +110,8 @@ const dataSource = new DataSource({
|
||||||
ResetToken1732358596823,
|
ResetToken1732358596823,
|
||||||
SMSAlarming1732696919191,
|
SMSAlarming1732696919191,
|
||||||
SecuringCalendarType1733249553766,
|
SecuringCalendarType1733249553766,
|
||||||
|
QueryStore1734187754677,
|
||||||
|
MemberDataViews1734520998539,
|
||||||
],
|
],
|
||||||
migrationsRun: true,
|
migrationsRun: true,
|
||||||
migrationsTransactionMode: "each",
|
migrationsTransactionMode: "each",
|
||||||
|
|
13
src/entity/query.ts
Normal file
13
src/entity/query.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
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;
|
||||||
|
}
|
26
src/factory/admin/queryStore.ts
Normal file
26
src/factory/admin/queryStore.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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,
|
||||||
|
title: record.title,
|
||||||
|
query: record.query.startsWith("{") ? JSON.parse(record.query) : record.query,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description map records to queryStore
|
||||||
|
* @param {Array<queryStore>} records
|
||||||
|
* @returns {Array<QueryStoreViewModel>}
|
||||||
|
*/
|
||||||
|
public static mapToBase(records: Array<query>): Array<QueryStoreViewModel> {
|
||||||
|
return records.map((r) => this.mapToSingle(r));
|
||||||
|
}
|
||||||
|
}
|
232
src/helpers/dynamicQueryBuilder.ts
Normal file
232
src/helpers/dynamicQueryBuilder.ts
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
export default abstract class DynamicQueryBuilder {
|
||||||
|
public static allowedTables: Array<string> = [
|
||||||
|
"award",
|
||||||
|
"communication",
|
||||||
|
"communicationType",
|
||||||
|
"executivePosition",
|
||||||
|
"membershipStatus",
|
||||||
|
"qualification",
|
||||||
|
"member",
|
||||||
|
"memberAwards",
|
||||||
|
"memberExecutivePositions",
|
||||||
|
"memberQualifications",
|
||||||
|
"membership",
|
||||||
|
"memberView",
|
||||||
|
"memberExecutivePositionsView",
|
||||||
|
"memberQualificationsView",
|
||||||
|
"membershipView",
|
||||||
|
];
|
||||||
|
|
||||||
|
public static getTableMeta(tableName: string): TableMeta {
|
||||||
|
let { name, columns, relations } = dataSource.getMetadata(tableName);
|
||||||
|
|
||||||
|
const uniqueColumns = columns.map((c) => ({ column: c.propertyName, type: c.type }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
tableName: name,
|
||||||
|
columns: [
|
||||||
|
...uniqueColumns,
|
||||||
|
...relations
|
||||||
|
.filter((r) => !uniqueColumns.some((c) => r.propertyName == c.column))
|
||||||
|
.map((r) => ({
|
||||||
|
column: r.propertyName,
|
||||||
|
type: r.inverseEntityMetadata?.columns.find((col) => col.propertyName === r.inverseSidePropertyPath)?.type,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
relations: relations.map((r) => ({
|
||||||
|
column: r.propertyName,
|
||||||
|
relationType: r.relationType,
|
||||||
|
referencedTableName: r.inverseEntityMetadata?.tableName,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getAllTableMeta(): Array<TableMeta> {
|
||||||
|
return this.allowedTables.map((table) => this.getTableMeta(table));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static buildQuery(
|
||||||
|
queryObj: DynamicQueryStructure,
|
||||||
|
offset: number = 0,
|
||||||
|
count: number = 25
|
||||||
|
): SelectQueryBuilder<ObjectLiteral> {
|
||||||
|
let affix = Math.random().toString(36).substring(2);
|
||||||
|
let query = dataSource.getRepository(queryObj.table).createQueryBuilder(`${queryObj.table}_${affix}`);
|
||||||
|
|
||||||
|
this.buildDynamicQuery(query, queryObj, affix);
|
||||||
|
|
||||||
|
query.offset(offset);
|
||||||
|
query.limit(count);
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static buildDynamicQuery(
|
||||||
|
query: SelectQueryBuilder<ObjectLiteral>,
|
||||||
|
queryObject: DynamicQueryStructure,
|
||||||
|
affix: string = "",
|
||||||
|
depth: number = 0
|
||||||
|
): void {
|
||||||
|
const alias = queryObject.table + "_" + affix;
|
||||||
|
let firstSelect = true;
|
||||||
|
let selects: Array<string> = [];
|
||||||
|
|
||||||
|
if (queryObject.select == "*") {
|
||||||
|
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 {
|
||||||
|
selects = queryObject.select;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const select of selects) {
|
||||||
|
if (firstSelect && depth == 0) {
|
||||||
|
query.select(`${alias}.${select}`);
|
||||||
|
firstSelect = false;
|
||||||
|
} else {
|
||||||
|
query.addSelect(`${alias}.${select}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryObject.where) {
|
||||||
|
this.applyWhere(query, queryObject.where, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryObject.join) {
|
||||||
|
for (const join of queryObject.join) {
|
||||||
|
let subaffix = Math.random().toString(36).substring(2);
|
||||||
|
query.leftJoinAndSelect(`${alias}.${join.foreignColumn}`, join.table + "_" + subaffix);
|
||||||
|
|
||||||
|
this.buildDynamicQuery(query, join, subaffix, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryObject.orderBy) {
|
||||||
|
queryObject.orderBy.forEach((order) => {
|
||||||
|
query.addOrderBy(`${alias}.${order.column}`, order.order);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static applyWhere(
|
||||||
|
query: SelectQueryBuilder<ObjectLiteral> | WhereExpressionBuilder,
|
||||||
|
conditions: Array<ConditionStructure>,
|
||||||
|
alias: string
|
||||||
|
): void {
|
||||||
|
for (const condition of conditions) {
|
||||||
|
if (condition.structureType == "condition") {
|
||||||
|
const whereClause = this.buildConditionClause(condition, alias);
|
||||||
|
|
||||||
|
if (condition.concat == "_" || condition.concat == "AND") {
|
||||||
|
query.andWhere(whereClause.query, whereClause.parameters);
|
||||||
|
} else {
|
||||||
|
query.orWhere(whereClause.query, whereClause.parameters);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (condition.concat == "_" || 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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static buildConditionClause(
|
||||||
|
condition: ConditionStructure & { structureType: "condition" },
|
||||||
|
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;
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import express from "express";
|
||||||
import { configCheck, SERVER_PORT } from "./env.defaults";
|
import { configCheck, SERVER_PORT } from "./env.defaults";
|
||||||
configCheck();
|
configCheck();
|
||||||
|
|
||||||
|
import { PermissionObject } from "./type/permissionTypes";
|
||||||
declare global {
|
declare global {
|
||||||
namespace Express {
|
namespace Express {
|
||||||
export interface Request {
|
export interface Request {
|
||||||
|
@ -16,12 +17,10 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
import { dataSource } from "./data-source";
|
import { dataSource } from "./data-source";
|
||||||
|
|
||||||
dataSource.initialize();
|
dataSource.initialize();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
import router from "./routes/index";
|
import router from "./routes/index";
|
||||||
import { PermissionObject } from "./type/permissionTypes";
|
|
||||||
router(app);
|
router(app);
|
||||||
app.listen(process.env.NODE_ENV ? SERVER_PORT : 5000, () => {
|
app.listen(process.env.NODE_ENV ? SERVER_PORT : 5000, () => {
|
||||||
console.log(`listening on *:${SERVER_PORT}`);
|
console.log(`listening on *:${SERVER_PORT}`);
|
||||||
|
|
26
src/migrations/1734187754677-queryStore.ts
Normal file
26
src/migrations/1734187754677-queryStore.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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<void> {
|
||||||
|
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: "title", type: "varchar", length: "255", isNullable: false, isUnique: true },
|
||||||
|
{ name: "query", type: "text", isNullable: false, default: "''" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropTable("query");
|
||||||
|
}
|
||||||
|
}
|
112
src/migrations/1734520998539-memberDataViews.ts
Normal file
112
src/migrations/1734520998539-memberDataViews.ts
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
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<void> {
|
||||||
|
await queryRunner.createView(
|
||||||
|
new View({
|
||||||
|
name: "member_view",
|
||||||
|
expression: (datasource: DataSource) =>
|
||||||
|
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"),
|
||||||
|
}),
|
||||||
|
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<void> {
|
||||||
|
await queryRunner.dropView("membership_view");
|
||||||
|
await queryRunner.dropView("member_qualifications_view");
|
||||||
|
await queryRunner.dropView("member_executive_positions_view");
|
||||||
|
await queryRunner.dropView("member_view");
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ router.get("/:id", async (req: Request, res: Response) => {
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/",
|
"/",
|
||||||
PermissionHelper.passCheckMiddleware("create", "settings", "communication"),
|
PermissionHelper.passCheckMiddleware("create", "settings", "communication_type"),
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
await createCommunicationType(req, res);
|
await createCommunicationType(req, res);
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ router.post(
|
||||||
|
|
||||||
router.patch(
|
router.patch(
|
||||||
"/:id",
|
"/:id",
|
||||||
PermissionHelper.passCheckMiddleware("update", "settings", "communication"),
|
PermissionHelper.passCheckMiddleware("update", "settings", "communication_type"),
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
await updateCommunicationType(req, res);
|
await updateCommunicationType(req, res);
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ router.patch(
|
||||||
|
|
||||||
router.delete(
|
router.delete(
|
||||||
"/:id",
|
"/:id",
|
||||||
PermissionHelper.passCheckMiddleware("delete", "settings", "communication"),
|
PermissionHelper.passCheckMiddleware("delete", "settings", "communication_type"),
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
await deleteCommunicationType(req, res);
|
await deleteCommunicationType(req, res);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,12 @@ import executivePosition from "./executivePosition";
|
||||||
import membershipStatus from "./membershipStatus";
|
import membershipStatus from "./membershipStatus";
|
||||||
import qualification from "./qualification";
|
import qualification from "./qualification";
|
||||||
import calendarType from "./calendarType";
|
import calendarType from "./calendarType";
|
||||||
|
import queryStore from "./queryStore";
|
||||||
|
|
||||||
import member from "./member";
|
import member from "./member";
|
||||||
import protocol from "./protocol";
|
import protocol from "./protocol";
|
||||||
|
|
||||||
import calendar from "./calendar";
|
import calendar from "./calendar";
|
||||||
|
import queryBuilder from "./queryBuilder";
|
||||||
|
|
||||||
import role from "./role";
|
import role from "./role";
|
||||||
import user from "./user";
|
import user from "./user";
|
||||||
|
@ -22,7 +23,7 @@ var router = express.Router({ mergeParams: true });
|
||||||
router.use("/award", PermissionHelper.passCheckMiddleware("read", "settings", "award"), award);
|
router.use("/award", PermissionHelper.passCheckMiddleware("read", "settings", "award"), award);
|
||||||
router.use(
|
router.use(
|
||||||
"/communicationtype",
|
"/communicationtype",
|
||||||
PermissionHelper.passCheckMiddleware("read", "settings", "communication"),
|
PermissionHelper.passCheckMiddleware("read", "settings", "communication_type"),
|
||||||
communicationType
|
communicationType
|
||||||
);
|
);
|
||||||
router.use(
|
router.use(
|
||||||
|
@ -37,10 +38,12 @@ router.use(
|
||||||
);
|
);
|
||||||
router.use("/qualification", PermissionHelper.passCheckMiddleware("read", "settings", "qualification"), qualification);
|
router.use("/qualification", PermissionHelper.passCheckMiddleware("read", "settings", "qualification"), qualification);
|
||||||
router.use("/calendartype", PermissionHelper.passCheckMiddleware("read", "settings", "calendar_type"), calendarType);
|
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("/member", PermissionHelper.passCheckMiddleware("read", "club", "member"), member);
|
||||||
router.use("/protocol", PermissionHelper.passCheckMiddleware("read", "club", "protocol"), protocol);
|
router.use("/protocol", PermissionHelper.passCheckMiddleware("read", "club", "protocol"), protocol);
|
||||||
router.use("/calendar", PermissionHelper.passCheckMiddleware("read", "club", "calendar"), calendar);
|
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("/role", PermissionHelper.passCheckMiddleware("read", "user", "role"), role);
|
||||||
router.use("/user", PermissionHelper.passCheckMiddleware("read", "user", "user"), user);
|
router.use("/user", PermissionHelper.passCheckMiddleware("read", "user", "user"), user);
|
||||||
|
|
18
src/routes/admin/queryBuilder.ts
Normal file
18
src/routes/admin/queryBuilder.ts
Normal file
|
@ -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;
|
45
src/routes/admin/queryStore.ts
Normal file
45
src/routes/admin/queryStore.ts
Normal file
|
@ -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;
|
40
src/service/queryStoreService.ts
Normal file
40
src/service/queryStoreService.ts
Normal file
|
@ -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<Array<query>>}
|
||||||
|
*/
|
||||||
|
static async getAll(): Promise<Array<query>> {
|
||||||
|
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<query>}
|
||||||
|
*/
|
||||||
|
static async getById(id: number): Promise<query> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
127
src/type/dynamicQueries.ts
Normal file
127
src/type/dynamicQueries.ts
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
export interface DynamicQueryStructure {
|
||||||
|
select: string[] | "*";
|
||||||
|
table: string;
|
||||||
|
where?: Array<ConditionStructure>;
|
||||||
|
join?: Array<DynamicQueryStructure & { foreignColumn: string }>;
|
||||||
|
orderBy?: Array<OrderByStructure>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConditionStructure = (
|
||||||
|
| {
|
||||||
|
structureType: "condition";
|
||||||
|
column: string;
|
||||||
|
operation: WhereOperation;
|
||||||
|
value: ConditionValue;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
structureType: "nested";
|
||||||
|
invert?: boolean;
|
||||||
|
conditions: Array<ConditionStructure>;
|
||||||
|
}
|
||||||
|
) & {
|
||||||
|
concat: WhereType;
|
||||||
|
structureType: "condition" | "nested";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConditionValue = FieldType | Array<FieldType> | { start: FieldType; end: FieldType };
|
||||||
|
export type FieldType = number | string | Date | boolean;
|
||||||
|
|
||||||
|
export type WhereType = "OR" | "AND" | "_"; // _ represents initial where in (sub-)query
|
||||||
|
|
||||||
|
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
|
||||||
|
| "timespanEq"; // Date before x years (YYYY-01-01 <bis> YYYY-12-31)
|
||||||
|
// TODO: age between | age equals | age greater | age smaller
|
||||||
|
|
||||||
|
export type OrderByStructure = {
|
||||||
|
column: string;
|
||||||
|
order: OrderByType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OrderByType = "ASC" | "DESC";
|
||||||
|
|
||||||
|
export const exampleQuery: DynamicQueryStructure = {
|
||||||
|
select: ["firstname", "lastname"],
|
||||||
|
table: "member",
|
||||||
|
where: [
|
||||||
|
{
|
||||||
|
structureType: "condition",
|
||||||
|
concat: "_",
|
||||||
|
column: "mail",
|
||||||
|
operation: "endsWith",
|
||||||
|
value: "@gmail.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
structureType: "nested",
|
||||||
|
concat: "AND",
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
structureType: "condition",
|
||||||
|
concat: "_",
|
||||||
|
column: "firstname",
|
||||||
|
operation: "startsWith",
|
||||||
|
value: "J",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
structureType: "condition",
|
||||||
|
concat: "OR",
|
||||||
|
column: "lastname",
|
||||||
|
operation: "startsWith",
|
||||||
|
value: "K",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
join: [
|
||||||
|
{
|
||||||
|
select: "*",
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
column: "firstname",
|
||||||
|
order: "ASC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: "lastname",
|
||||||
|
order: "ASC",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
|
@ -8,11 +8,13 @@ export type PermissionModule =
|
||||||
| "qualification"
|
| "qualification"
|
||||||
| "award"
|
| "award"
|
||||||
| "executive_position"
|
| "executive_position"
|
||||||
| "communication"
|
| "communication_type"
|
||||||
| "membership_status"
|
| "membership_status"
|
||||||
| "calendar_type"
|
| "calendar_type"
|
||||||
| "user"
|
| "user"
|
||||||
| "role";
|
| "role"
|
||||||
|
| "query"
|
||||||
|
| "query_store";
|
||||||
|
|
||||||
export type PermissionType = "read" | "create" | "update" | "delete";
|
export type PermissionType = "read" | "create" | "update" | "delete";
|
||||||
|
|
||||||
|
@ -44,15 +46,25 @@ export const permissionModules: Array<PermissionModule> = [
|
||||||
"qualification",
|
"qualification",
|
||||||
"award",
|
"award",
|
||||||
"executive_position",
|
"executive_position",
|
||||||
"communication",
|
"communication_type",
|
||||||
"membership_status",
|
"membership_status",
|
||||||
"calendar_type",
|
"calendar_type",
|
||||||
"user",
|
"user",
|
||||||
"role",
|
"role",
|
||||||
|
"query",
|
||||||
|
"query_store",
|
||||||
];
|
];
|
||||||
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
|
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
|
||||||
export const sectionsAndModules: SectionsAndModulesObject = {
|
export const sectionsAndModules: SectionsAndModulesObject = {
|
||||||
club: ["member", "calendar", "newsletter", "protocol"],
|
club: ["member", "calendar", "newsletter", "protocol", "query"],
|
||||||
settings: ["qualification", "award", "executive_position", "communication", "membership_status", "calendar_type"],
|
settings: [
|
||||||
|
"qualification",
|
||||||
|
"award",
|
||||||
|
"executive_position",
|
||||||
|
"communication_type",
|
||||||
|
"membership_status",
|
||||||
|
"calendar_type",
|
||||||
|
"query_store",
|
||||||
|
],
|
||||||
user: ["user", "role"],
|
user: ["user", "role"],
|
||||||
};
|
};
|
||||||
|
|
7
src/type/tableMeta.ts
Normal file
7
src/type/tableMeta.ts
Normal file
|
@ -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 }>;
|
||||||
|
}
|
7
src/viewmodel/admin/queryStore.models.ts
Normal file
7
src/viewmodel/admin/queryStore.models.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { DynamicQueryStructure } from "../../type/dynamicQueries";
|
||||||
|
|
||||||
|
export interface QueryStoreViewModel {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
query: string | DynamicQueryStructure;
|
||||||
|
}
|
53
src/views/memberExecutivePositionView.ts
Normal file
53
src/views/memberExecutivePositionView.ts
Normal file
|
@ -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;
|
||||||
|
}
|
53
src/views/memberQualificationsView.ts
Normal file
53
src/views/memberQualificationsView.ts
Normal file
|
@ -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;
|
||||||
|
}
|
43
src/views/memberView.ts
Normal file
43
src/views/memberView.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
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")
|
||||||
|
.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"),
|
||||||
|
})
|
||||||
|
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;
|
||||||
|
}
|
50
src/views/membershipsView.ts
Normal file
50
src/views/membershipsView.ts
Normal file
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in a new issue