@codenameryuu/adonis-datatable
Version:
Server side datatables package for Adonis JS
434 lines (433 loc) • 15.6 kB
JavaScript
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();
}
}