UNPKG

@codenameryuu/adonis-datatable

Version:
434 lines (433 loc) 15.6 kB
import { DataTableAbstract } from "../datatable_abstract.js"; import { DatabaseQueryBuilder } from "@adonisjs/lucid/database"; import Helper from "../utils/helper.js"; import lodash from "lodash"; import collect from "collect.js"; import { sprintf } from "sprintf-js"; export default class DatabaseDataTable extends DataTableAbstract { query; nullsLast = false; prepared = false; keepSelectBindings = false; disableUserOrdering = false; constructor(query) { super(); this.query = query; this.$columns = query.columns; } static canCreate(source) { return source instanceof DatabaseQueryBuilder; } getConnection() { return this.query.knexQuery.client; } defaultOrdering() { const self = this; collect(this.request.orderableColumns()) .map((orderable) => { orderable["name"] = self.getColumnName(orderable["column"], true); return orderable; }) .reject((orderable) => self.isBlacklisted(orderable["name"]) && !self.hasOrderColumn(orderable["name"])) .each((orderable) => { const column = self.resolveRelationColumn(orderable["name"]); if (self.hasOrderColumn(orderable["name"])) { self.applyOrderColumn(orderable["name"], orderable); } else if (self.hasOrderColumn(column)) { self.applyOrderColumn(column, orderable); } else { const nullsLastSql = self.getNullsLastSql(column, orderable["direction"]); const normalSql = self.wrapColumn(column) + " " + orderable["direction"]; const sql = self.nullsLast ? nullsLastSql : normalSql; self.query.orderByRaw(sql); } }); } hasOrderColumn(column) { return this.$columnDef["order"][column] !== undefined; } applyOrderColumn(column, orderable) { let sql = this.$columnDef["order"][column]["sql"]; if (sql === false) { return; } if (typeof sql === "function") { sql(this.query, orderable["direction"]); } else { sql = sql.replace("$1", orderable["direction"]); const bindings = this.$columnDef["order"][column]["bindings"]; this.query.orderByRaw(sql, bindings); } } getNullsLastSql(column, direction) { const sql = this.config.get("datatables.nulls_last_sql", "%s %s NULLS LAST"); return sprintf(sql, column, direction).replace(":column", column).replace(":direction", direction); } attachAppends(data) { const appends = {}; for (const [key, value] of Object.entries(this.appends)) { if (typeof value === "function") { appends[key] = Helper.value(value(this.getFilteredQuery())); } else { appends[key] = value; } } appends["disableOrdering"] = this.disableUserOrdering; return { ...data, ...appends }; } getColumnSearchKeyword(i, raw = false) { const keyword = this.request.columnKeyword(i); if (raw || this.request.isRegex(i)) { return keyword; } return this.setupKeyword(keyword); } applyFilterColumn(query, columnName, keyword) { query = this.getBaseQueryBuilder(query); const callback = this.$columnDef["filter"][columnName]["method"]; callback(query, keyword); } resolveRelationColumn(column) { return column; } compileColumnSearch(i, column, keyword) { if (this.request.isRegex(i)) { this.regexColumnSearch(column, keyword); } else { this.compileQuerySearch(this.query, column, keyword, ""); } } regexColumnSearch(column, keyword) { column = this.wrapColumn(column); let sql = ""; switch (this.getConnection().driverName) { case "oracle": sql = !this.config.isCaseInsensitive() ? "REGEXP_LIKE( " + column + " , ? )" : "REGEXP_LIKE( LOWER(" + column + ") , ?, 'i' )"; break; case "pgsql": column = this.castColumn(column); sql = !this.config.isCaseInsensitive() ? column + " ~ ?" : column + " ~* ? "; break; default: sql = !this.config.isCaseInsensitive() ? column + " REGEXP ?" : "LOWER(" + column + ") REGEXP ?"; keyword = keyword.toLowerCase(); } this.query.whereRaw(sql, [keyword]); } compileQuerySearch(query, columnName, keyword, boolean = "or") { let column = this.addTablePrefix(query, columnName); column = this.castColumn(column); let sql = column + " LIKE ?"; if (this.config.isCaseInsensitive()) { sql = "LOWER(" + column + ") LIKE ?"; } const method = lodash.lowerFirst(`${boolean}WhereRaw`); query[method](sql, [this.prepareKeyword(keyword)]); } prepareKeyword(keyword) { if (this.config.isCaseInsensitive()) { keyword = keyword.toLowerCase(); } if (this.config.isStartsWithSearch()) { return `${keyword}%`; } if (this.config.isWildcard()) { keyword = Helper.wildcardString(keyword, "%"); } if (this.config.isSmartSearch()) { keyword = `%${keyword}%`; } return keyword; } async prepareQuery() { if (!this.prepared) { this.totalRecords = await this.totalCount(); await this.filterRecords(); this.ordering(); this.paginate(); } this.prepared = true; return this; } async filterRecords() { const initialQuery = this.query.clone(); if (this.autoFilter && this.request.isSearchable()) { this.filtering(); } if (typeof this.filterCallback === "function") { this.filterCallback(this.query); } this.columnSearch(); if (!this.$skipTotalRecords && this.query === initialQuery) { this.filteredRecords ??= this.totalRecords; } else { await this.filteredCount(); if (this.$skipTotalRecords) { this.totalRecords = this.filteredRecords; } } } hasFilterColumn(columnName) { return this.$columnDef["filter"][columnName] !== undefined; } wrapColumn(column) { return Helper.wrapColumn(column, true); } getBaseQueryBuilder(instance = undefined) { if (!instance) { instance = this.query; } return instance; } castColumn(column) { const driverName = this.getConnection().driverName; switch (driverName) { case "pgsql": return `CAST(${column} AS TEXT)`; case "firebird": return `CAST(${column} AS VARCHAR(255))`; default: return column; } } addTablePrefix(query, column) { if (!column.includes(".")) { const sql = this.getBaseQueryBuilder(query).toSQL().sql; const tableName = sql.match(/from\s+`?(\w+)`?/i)?.[1] || ""; let from = tableName; if (typeof from === "string") { if (from.includes(" as ")) { from = from.split(" as ")[1]; } column = `${from}.${column}`; } } return this.wrapColumn(column); } getFilteredQuery() { this.prepareQuery(); return this.query; } resolveCallback() { return this.query; } async results() { try { this.prepareContext(); const query = await this.prepareQuery(); const results = await query.dataResults(); const processed = this.processResults(results); return this.render(processed); } catch (error) { return this.errorResponse(error); } } async dataResults() { return await this.query; } async count() { const builder = this.query.clone(); const result = await builder.exec(); return result.length; } columnSearch() { const columns = this.request.columns(); for (let index = 0; index < columns.length; index++) { let column = this.getColumnName(index); if (column === null) { continue; } if (this.isBlacklisted(column) && !this.hasFilterColumn(column)) { continue; } if (this.request.hasColumnControl(index) && this.request.isColumnSearchable(index, false)) { this.applyColumnControlSearch(index, column); continue; } if (!this.request.isColumnSearchable(index)) { continue; } if (this.hasFilterColumn(column)) { const keyword = this.getColumnSearchKeyword(index, true); this.applyFilterColumn(this.getBaseQueryBuilder(), column, keyword); } else { column = this.resolveRelationColumn(column); const keyword = this.getColumnSearchKeyword(index); this.compileColumnSearch(index, column, keyword); } } } /** * Apply the search conditions sent by the DataTables ColumnControl extension. * * @see https://datatables.net/extensions/columncontrol/server-side */ applyColumnControlSearch(index, column) { if (this.request.hasColumnControlList(index)) { this.applyColumnControlList(column, this.request.columnControlList(index)); } const search = this.request.columnControlSearch(index); if (search && (search.value !== "" || search.logic === "empty" || search.logic === "notEmpty")) { if (this.hasFilterColumn(column)) { this.applyFilterColumn(this.getBaseQueryBuilder(), column, search.value); } else { this.compileColumnControlSearch(this.resolveRelationColumn(column), search); } } } applyColumnControlList(column, list) { const self = this; const resolved = this.resolveRelationColumn(column); this.query.where((query) => { for (const value of list) { self.compileExactMatch(query, resolved, String(value), "or"); } }); } compileExactMatch(query, columnName, value, boolean = "or") { let column = this.castColumn(this.addTablePrefix(query, columnName)); let keyword = value; if (this.config.isCaseInsensitive()) { column = `LOWER(${column})`; keyword = keyword.toLowerCase(); } const method = lodash.lowerFirst(`${boolean}WhereRaw`); query[method](`${column} = ?`, [keyword]); } compileColumnControlSearch(column, search) { const wrapped = this.addTablePrefix(this.query, column); const logic = search.logic; if (logic === "empty") { this.query.whereRaw(`(${wrapped} IS NULL OR ${wrapped} = ?)`, [""]); return; } if (logic === "notEmpty") { this.query.whereRaw(`(${wrapped} IS NOT NULL AND ${wrapped} != ?)`, [""]); return; } switch (search.type) { case "num": this.compileNumberSearch(wrapped, logic, search.value); break; case "date": this.compileDateSearch(wrapped, logic, search.value, search.mask); break; default: this.compileTextSearch(wrapped, logic, search.value); } } compileTextSearch(column, logic, value) { let target = this.castColumn(column); let keyword = value; if (this.config.isCaseInsensitive()) { target = `LOWER(${target})`; keyword = keyword.toLowerCase(); } switch (logic) { case "equal": this.query.whereRaw(`${target} = ?`, [keyword]); break; case "notEqual": this.query.whereRaw(`${target} != ?`, [keyword]); break; case "starts": this.query.whereRaw(`${target} LIKE ?`, [`${keyword}%`]); break; case "ends": this.query.whereRaw(`${target} LIKE ?`, [`%${keyword}`]); break; case "notContains": this.query.whereRaw(`${target} NOT LIKE ?`, [`%${keyword}%`]); break; case "contains": default: this.query.whereRaw(`${target} LIKE ?`, [`%${keyword}%`]); } } compileNumberSearch(column, logic, value) { const operators = { equal: "=", notEqual: "!=", greater: ">", greaterOrEqual: ">=", less: "<", lessOrEqual: "<=", }; const operator = operators[logic] ?? "="; this.query.whereRaw(`${column} ${operator} ?`, [Number(value)]); } compileDateSearch(column, logic, value, mask) { let target = column; if (mask && !/[Hhms]/.test(mask)) { target = `DATE(${column})`; } const operators = { equal: "=", notEqual: "!=", greater: ">", less: "<", }; const operator = operators[logic] ?? "="; this.query.whereRaw(`${target} ${operator} ?`, [value]); } filterColumn(column, callback) { this.$columnDef["filter"][column] = { method: callback }; return this; } orderColumns(columns, sql, bindings = []) { for (const column of Object.values(columns)) { this.orderColumn(column, sql.replace(":column", column), bindings); } return this; } orderColumn(column, sql, bindings = []) { this.$columnDef["order"][column] = { sql: sql, bindings: bindings }; return this; } orderByNullsLast() { this.nullsLast = true; return this; } paging() { const start = this.request.start(); const length = this.request.length(); const limit = length > 0 ? length : 10; this.query.offset(start).limit(limit); } addColumn(name, content, order = false) { this.pushToBlacklist(name); return super.addColumn(name, content, order); } globalSearch(keyword) { const self = this; this.query.where((query) => { collect(self.request.searchableColumnIndex()) .map((index) => super.getColumnName(index)) .filter(() => true) .reject((column) => self.isBlacklisted(column) && !self.hasFilterColumn(column)) .each((column) => { if (self.hasFilterColumn(column)) { self.applyFilterColumn(query, column, keyword); } else { self.compileQuerySearch(query, column, keyword); } }); }); } ordering() { if (this.disableUserOrdering) { return; } super.ordering(); } }