UNPKG

@opengis/fastify-table

Version:

core-plugins

288 lines (287 loc) 11.1 kB
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; }