@opengis/fastify-table
Version:
core-plugins
288 lines (287 loc) • 11.1 kB
JavaScript
import path from "node:path";
import { existsSync, readFileSync } from "node:fs";
import { config, getPGAsync, getTemplate, getSelectMeta, getMeta, applyHook, getSelectVal, logger, getSelect, metaFormat, getColumnCLS, pgClients, } from "../../../../utils.js";
const defaultLimit = 50;
async function getTableColumnMeta({ table, template, column, selectName, filtered, startsWith, key, pg = pgClients.client, }) {
if (!table || !column) {
return null;
}
const { columns, table: tableName } = template
? await getTemplate("table", template)
: { table, columns: [] };
const { data: clsName } = selectName
? { data: selectName }
: columns.find((el) => el.name === column) || {};
const { arr } = (await getSelect(clsName || column, pg)) || {};
const original = filtered
? `with c(id,text) as (select ${column} as id, ${column} as text from ${tableName} group by ${column}) select id, text from c`
: `with c(id,text) as (select ${column} as id, ${column} as text, count(*) from ${tableName} group by ${column} limit ${defaultLimit}) select * from c`;
return {
arr,
original,
searchQuery: startsWith && key
? `(left(lower("text"),${key.length}) = $1 )`
: '(lower("text") ~ $1 )',
pk: "id",
};
}
export default async function suggest(req, reply) {
const time = Date.now();
const { params, user, query = {}, pg: pg1 = pgClients.client } = req;
const { lang = "ua", parent = "" } = query;
const debugMode = config.local ||
user?.user_type?.includes?.("admin") ||
process.env.NODE_ENV === "test" ||
process.env.VITEST;
if (params?.data && params.data?.startsWith?.("hash-")) {
const filepath = path.join(process.cwd(), `/log/suggest/${params.data.replace("hash-", "")}.json`);
if (existsSync(filepath)) {
const { table, column } = JSON.parse(readFileSync(filepath, { encoding: "utf8" }) || "{}");
params.data = `${table}:${column}`;
}
}
const [table1, column1] = params.data?.includes(":")
? params.data.split(":")
: [query.token, query.column];
const selectName = query.sel || params.data;
const table = table1 || query.token;
const column = column1 || query.column;
if (!selectName) {
return reply.status(400).send({
error: "name is required",
code: 400,
});
}
const { body: hookBody } = table
? (await applyHook("preSuggest", {
pg: pg1,
table,
})) || {}
: {};
const body = await getTemplate("table", table);
const tableName = hookBody?.table || body?.table || table;
if (query.limit && query.limit < 0) {
return reply.status(400).send({
error: "param limit is invalid",
code: 400,
});
}
const meta = tableName && column
? await getTableColumnMeta({
table: tableName,
template: tableName && table && tableName !== table ? table : undefined,
column,
selectName,
filtered: query.key || query.val,
startsWith: !!query.start,
key: query.key,
})
: await getSelectMeta({
name: selectName,
nocache: query.nocache,
startsWith: !!query.start,
key: query.key,
parent,
});
if (meta?.minLength && query.key && query.key.length < meta?.minLength) {
return reply.status(400).send({
error: `min length: ${meta.minLength}`,
code: 400,
});
}
const limit = meta?.limit || defaultLimit;
const pg = meta?.db ? await getPGAsync(meta.db) : pg1;
if (!pg || !pg.pk || !pg.pgType) {
return reply.status(400).send({
error: "pg connection not established",
code: 400,
});
}
if (table && !pg?.pk?.[tableName]) {
return reply.status(400).send({
error: "param name is invalid: 1",
code: 400,
});
}
const columns = hookBody?.columns || body?.columns
? hookBody?.columns || body?.columns
: await getMeta({
pg,
table: tableName,
}).then((el) => el?.columns || []);
const { name: columnName, dataTypeID } = (columns || []).find((col) => col?.name === column) || {};
if (table && (!column || !columnName)) {
return reply.status(400).send({
error: "param name is invalid: 2",
code: 400,
});
}
if (!meta) {
return reply.status(404).send({
error: "Not found query select",
code: 404,
});
}
if (query.meta) {
return meta;
}
const { arr, searchQuery } = meta;
if (arr && table && column) {
const sqlCls = query.count
? `select value, count(*) from (select ${pg.pgType?.[dataTypeID]?.includes("[]")
? `unnest(${columnName})`
: `${columnName}`} as value from ${tableName.replace(/'/g, "''")} where ${hookBody?.query || body?.query || "1=1"})q group by value`
: `select array_agg(distinct value)::text[] from (select ${pg.pgType?.[dataTypeID]?.includes("[]")
? `unnest(${columnName})`
: `${columnName}`} as value from ${tableName.replace(/'/g, "''")} where ${hookBody?.query || body?.query || "1=1"})q`;
if (query.sql && debugMode) {
return sqlCls;
}
if (tableName && pg?.pk?.[tableName] && columnName) {
const qRes = await pg.queryCache(sqlCls, { table: tableName });
const vals = (query.count
? qRes.rows?.map?.((el) => el.value?.toString?.())
: qRes.rows?.[0]?.array_agg) || [];
const lower = query.key?.toLowerCase?.();
const data1 = query.key || query.val
? arr
?.filter((el) => !lower ||
(el[lang] || el.text)?.toLowerCase()?.indexOf(lower) !== -1)
?.filter((el) => !query.val || el.id === query.val)
: arr;
const data2 = data1.filter((el) => el.id && vals.includes(el.id.toString()));
const data = data2.slice(0, Math.min(query.limit || limit, limit));
if (data.length && query.count) {
const counts = qRes.rows?.reduce?.((acc, curr) => ({
...acc,
[curr.value]: curr.count,
}), {});
data.forEach((el) => Object.assign(el, { count: counts?.[el.id] || 0 }));
}
if (config.debug) {
logger.file("suggest/debug", {
type: 1,
table,
tableName,
column: columnName,
data,
data1,
data2,
query,
});
}
return {
time: Date.now() - time,
limit: Math.min(query.limit || limit, limit),
count: data.length,
total: arr.length,
mode: "array",
sql: debugMode ? sqlCls : undefined,
data,
};
}
}
if (arr) {
const lower = query.key?.toLowerCase();
const data1 = query.key || query.val
? arr
?.filter((el) => !lower ||
(el[lang] || el.text)?.toLowerCase()?.indexOf(lower) !== -1)
?.filter((el) => !query.val || el.id === query.val)
: arr;
const data = data1.slice(0, Math.min(query.limit || limit, limit));
if (config.debug) {
logger.file("suggest/debug", {
type: 2,
key: query.key,
data,
data1,
});
}
return {
time: Date.now() - time,
limit: Math.min(query.limit || limit, limit),
count: data.length,
total: arr.length,
mode: "array",
data,
};
}
// search
const search = query.key ? searchQuery : null;
// val
// const pk = meta.originalCols.split(',')[0];
// return meta;
const val = query.val
? ` ${meta.pk}=any('{${query.val.replace(/'/g, "''")}}')`
: "";
const where = [search, val, meta.pk ? `${meta.pk} is not null` : null]
.filter(Boolean)
.join(" and ") || "true";
const filter = table && pg.pk?.[table] && columnName && dataTypeID
? `id in (select ${pg.pgType[dataTypeID]?.includes("[]")
? `unnest(${columnName})`
: columnName} from ${table.replace(/'/g, "''")})`
: "true";
const sqlSuggest = `with c(id,text) as ( ${meta.original.replace(/{{parent}}/gi, parent)} where ${where} ${meta.original.includes("order by") ? "" : "order by 2"}) select * from c where ${filter} limit ${Math.min(query.limit || meta.limit || limit, limit)}`.replace(/{{uid}}/g, user?.uid || "0");
if (query.sql && debugMode) {
return sqlSuggest;
}
// query
const { rows: dataNew } = await pg.query(sqlSuggest, query.key ? [query.key.toLowerCase()] : []);
// in case id / text = Boolean
const data = dataNew.filter((el) => Object.hasOwn(el, "id") && Object.hasOwn(el, "text"));
if (tableName && column) {
const { name = query.sel || column, type = "select" } = getColumnCLS(tableName, column) || {};
await metaFormat({
rows: data,
table: tableName,
cls: { text: name },
sufix: true,
});
return {
time: Date.now() - time,
limit: Math.min(query.limit || meta.limit || limit, limit),
count: data.length,
total: meta.count - 0,
mode: type === "cls" ? "array" : "sql",
db: meta.db,
sql: debugMode ? sqlSuggest : undefined,
data: data.map((el) => ({
count: el.count,
id: el.id,
text: el.text_text || el.text,
...(el.text_data || {}),
})),
};
}
if (query.sel) {
const clsData = await getSelectVal({
pg,
name: query.sel,
values: data.map((el) => el.id),
});
data.forEach((el) => Object.assign(el, { text: clsData?.[el.id || ""] || el.id }));
}
if (config.debug) {
logger.file("suggest/debug", {
type: 3,
sel: query.sel,
data,
dataNew,
sqlSuggest,
// ids,
});
}
const message = {
time: Date.now() - time,
limit: Math.min(query.limit || meta.limit || limit, limit),
count: data.length,
total: meta.count - 0,
mode: "sql",
db: meta.db,
sql: debugMode ? sqlSuggest : undefined,
data,
};
return message;
}