feat(index): add character search screen

This commit is contained in:
Axel Cocat 2022-01-19 21:44:14 +01:00
parent 4d63acf21d
commit 46f3f24c4f
9 changed files with 449 additions and 5 deletions

View file

@ -65,6 +65,7 @@ export class Armory {
const indexController = new IndexController(this);
app.get("/", indexController.index.bind(indexController));
app.get("/search", indexController.search.bind(indexController));
const charsController = new CharacterController(this);
await charsController.load();

235
src/armory/DataTablesSsp.ts Normal file
View file

@ -0,0 +1,235 @@
import { Query } from "express-serve-static-core";
import { Connection, RowDataPacket } from "mysql2/promise";
export interface IResult {
recordsTotal: number;
recordsFiltered: number;
data: any[][];
}
export interface IColumnSettings {
name: string;
collation?: string;
formatter?: (data: string | number | null, row: any) => string;
table?: string;
}
export interface IColumnJoin {
table1: string;
column1: string;
table2: string;
column2: string;
kind: "INNER" | "FULL OUTER" | "LEFT" | "RIGHT";
}
export class DataTablesSsp {
public draw: number;
public joins: IColumnJoin[] = [];
public extraDataColumns: string[] = [];
private db: Connection;
private table: string;
private primaryKey: string;
private columnSettings: IColumnSettings[];
private start: number;
private length: number;
private _order: {
column: number,
dir: string,
}[];
private columns: {
data: number,
name: string,
searchable: boolean,
orderable: boolean,
search: {
value: string,
regex: boolean,
}
}[];
private search: {
value: string,
regex: boolean,
};
private wheres: string[] = [];
private filterBindings: (string | number)[] = [];
private customBindings: (string | number)[] = [];
private filterWhereSql: string = "1";
private customWhereSql: string = "1";
private limitSql: string = "";
private orderSql: string = "";
private joinSql: string = "";
public constructor(query: Query, db: Connection, table: string, primaryKey: string, columnSettings: IColumnSettings[]) {
this.start = parseInt(query.start as string, 10);
this.length = parseInt(query.length as string, 10);
this.draw = parseInt(query.draw as string, 10);
this._order = (query.order as { column: string, dir: string }[])
.map(order => { return { column: parseInt(order.column, 10), dir: order.dir, }; });
this.columns = (query.columns as { data: string, name: string, searchable: string, orderable: string, search: any }[])
.map(column => {
return {
data: parseInt(column.data, 10),
name: column.name,
searchable: column.searchable === "true",
orderable: column.orderable === "true",
search: { value: column.search.value, regex: column.search.regex === "true" },
};
});
this.search = {
value: (query.search as any).value as string,
regex: (query.search as any).regex === "true",
};
this.db = db;
this.table = table;
this.primaryKey = primaryKey;
this.columnSettings = columnSettings;
}
private limit() {
if (this.start !== undefined && this.length !== -1) {
this.limitSql = `LIMIT ${this.length} OFFSET ${this.start}`;
}
return this;
}
private order() {
if (this._order === undefined) {
return this;
}
const orderBy = [];
for (const order of this._order) {
const requestColumn = this.columns[order.column];
if (!requestColumn.orderable) {
continue;
}
const colSettings = this.columnSettings[requestColumn.data];
orderBy.push(`\`${colSettings.table || this.table}\`.\`${colSettings.name}\` ${order.dir}`);
}
orderBy.push(`\`${this.table}\`.\`${this.primaryKey}\``);
if (orderBy.length > 0) {
this.orderSql = "ORDER BY " + orderBy.join(", ");
}
return this;
}
private join() {
for (const join of this.joins) {
this.joinSql += `${join.kind} JOIN \`${join.table2}\` ON \`${join.table2}\`.\`${join.column2}\` = \`${join.table1}\`.\`${join.column1}\`\n`;
}
return this;
}
private filter() {
if (this.search.value?.length > 0) {
const filterWheres = [];
this.filterBindings = [];
for (const col of this.columns) {
if (!col.searchable) {
continue;
}
const colSettings = this.columnSettings[col.data];
const collate = colSettings.collation !== undefined ? `COLLATE ${colSettings.collation} ` : "";
filterWheres.push(`\`${colSettings.table || this.table}\`.\`${colSettings.name}\` ${collate}LIKE ?`);
this.filterBindings.push(`%${this.search.value}%`);
}
if (filterWheres.length > 0) {
this.filterWhereSql = "(" + filterWheres.map(w => `(${w})`).join(" OR ") + ")";
}
}
this.customWhereSql = this.wheres.map(w => `(${w})`).join(" AND ");
return this;
}
private buildSql(): string {
const columns = [
...this.columnSettings.map(c => `\`${c.table || this.table}\`.\`${c.name}\``),
...this.extraDataColumns,
];
return `
SELECT ${columns.join(", ")}
FROM ${this.table}
${this.joinSql}
WHERE
${this.filterWhereSql} AND
${this.customWhereSql}
${this.orderSql}
${this.limitSql}
`;
}
private buildTotalCountSql(): string {
return `
SELECT COUNT(\`${this.table}\`.\`${this.primaryKey}\`) AS \`count\`
FROM ${this.table}
${this.joinSql}
WHERE ${this.customWhereSql}
`;
}
private buildFilteredCountSql(): string {
return `
SELECT COUNT(\`${this.table}\`.\`${this.primaryKey}\`) AS \`count\`
FROM ${this.table}
${this.joinSql}
WHERE
${this.filterWhereSql} AND
${this.customWhereSql}
`;
}
public async run(): Promise<IResult> {
this.limit()
.order()
.join()
.filter();
const bindings = [...this.filterBindings, ...this.customBindings];
let [rows, fields] = await this.db.query(this.buildTotalCountSql(), this.customBindings);
const recordsTotal = rows[0].count;
[rows, fields] = await this.db.query(this.buildFilteredCountSql(), bindings);
const recordsFiltered = rows[0].count;
[rows, fields] = await this.db.query({
sql: this.buildSql(),
rowsAsArray: true,
values: bindings,
});
rows = (rows as any[][]).map(row => {
for (let i = 0; i < this.columnSettings.length; ++i) {
const col = this.columnSettings[i];
if (col.formatter !== undefined) {
row[i] = col.formatter(row[i], row);
}
}
return row;
});
return {
recordsTotal,
recordsFiltered,
data: rows,
};
}
public where(condition: string, binding?: string | number) {
this.wheres.push(condition);
if (binding !== undefined) {
this.customBindings.push(binding);
}
return this;
}
}

View file

@ -1,6 +1,32 @@
import * as express from "express";
import { Armory } from "../Armory";
import { DataTablesSsp } from "../DataTablesSsp";
const raceFiles = {
1: "human",
2: "orc",
3: "dwarf",
4: "nightelf",
5: "scourge",
6: "tauren",
7: "gnome",
8: "troll",
10: "bloodelf",
11: "draenei",
};
const classFiles = {
1: "warrior",
2: "paladin",
3: "hunter",
4: "rogue",
5: "priest",
6: "deathknight",
7: "shaman",
8: "mage",
9: "warlock",
11: "druid",
};
export class IndexController {
private armory: Armory;
@ -12,6 +38,51 @@ export class IndexController {
public async index(req: express.Request, res: express.Response): Promise<void> {
res.render("index.html", {
title: "Armory",
realms: this.armory.config.realms.map(r => r.name),
});
}
public async search(req: express.Request, res: express.Response): Promise<void> {
const realmName = req.query.realm as string;
const realm = realmName === undefined ?
this.armory.config.realms[0] :
this.armory.config.realms.find(r => r.name === realmName);
if (realm === undefined) {
res.status(400);
return;
}
const db = this.armory.getCharactersDb(realm.name);
let [rows, fields] = await db.query(`
SELECT CCSA.character_set_name FROM information_schema.\`TABLES\` T,
information_schema.\`COLLATION_CHARACTER_SET_APPLICABILITY\` CCSA
WHERE CCSA.collation_name = T.table_collation
AND T.table_schema = "${db.config.database}"
AND T.table_name = "characters"
`);
const charSet = rows[0].character_set_name;
const ssp = new DataTablesSsp(req.query, db, "characters", "guid", [
{ name: "name", collation: `${charSet}_general_ci` },
{ name: "name", table: "guild" },
{ name: "level" },
{ name: "race", formatter: (race, row) => `${raceFiles[race]}_${row[6] === 0 ? "male" : "female"}` },
{ name: "class", formatter: cls => classFiles[cls] },
{ name: "online", formatter: online => online === 1 },
]);
ssp.joins = [
{ table1: "characters", column1: "guid", table2: "guild_member", column2: "guid", kind: "LEFT" },
{ table1: "guild_member", column1: "guildid", table2: "guild", column2: "guildid", kind: "LEFT" },
];
ssp.extraDataColumns = ["`characters`.`gender`"];
const result = await ssp
.where("`deleteInfos_Account` IS NULL")
.run();
res.json({
draw: ssp.draw,
...result,
});
}
}