UNPKG

@opengis/fastify-table

Version:

core-plugins

640 lines (638 loc) 25.1 kB
import config from "../../../../config.js"; import { handlebars, handlebarsSync } from "../../../helpers/index.js"; import getFilterSQL from "../../../plugins/table/funcs/getFilterSQL/index.js"; import getAccess from "../../../plugins/crud/funcs/getAccess.js"; import setToken from "../../../plugins/crud/funcs/setToken.js"; import getToken from "../../../plugins/crud/funcs/getToken.js"; import gisIRColumn from "../../../plugins/table/funcs/gisIRColumn.js"; import { applyHook } from "../../../../utils.js"; import getSelect from "../../../plugins/table/funcs/getSelect.js"; import setOpt from "../../../plugins/crud/funcs/setOpt.js"; import getOpt from "../../../plugins/crud/funcs/getOpt.js"; import getFilter from "../../../plugins/table/funcs/getFilter.js"; import logger from "../../../plugins/logger/getLogger.js"; import getTemplate from "../../../plugins/table/funcs/getTemplate.js"; import getMeta from "../../../plugins/pg/funcs/getMeta.js"; import pgClients from "../../../plugins/pg/pgClients.js"; import metaFormat from "../../../plugins/table/funcs/metaFormat/index.js"; import extraDataGet from "../../../plugins/extra/extraDataGet.js"; import locales from "../controllers/utils/locales.js"; import conditions from "../controllers/utils/conditions.js"; const components = { "vs-widget-file": "select 'vs-widget-file' as component, count(*) from crm.files where entity_id=$1 and file_status<>3", "vs-widget-comments": "select 'vs-widget-comments' as component, count(*) from crm.communications where entity_id=$1", }; const mockReply = { response: {}, // redirect: (txt) => txt, redirect: (txt) => Object.assign(mockReply.response, { body: txt, statusCode: mockReply.response.statusCode || 200, }), status: (statusCode) => { Object.assign(mockReply.response, { status: statusCode }); return mockReply; }, send: (txt) => Object.assign(mockReply.response, txt), }; function getOrder(queryOrder, queryDesc, defaultOrder, columnList, iscalled = false) { if (iscalled && queryOrder) { return `order by ${queryOrder}`; } if (!queryOrder) { return `order by ${defaultOrder || "true::boolean"} nulls last`; } const orderArr = queryOrder.split(/[|]/g).map((el) => el.split(/[-]/)); const validOrder = orderArr .filter((el) => el[0] && (columnList || "").includes(el[0])) .map((el) => `${el[0]} ${queryDesc || el[1] === "desc" ? "desc" : ""}`) .join(","); return `order by ${validOrder || "true::boolean"} nulls last`; } const checkInline = {}; const maxLimit = 100; const defaultLimit = 20; export default async function dataAPI({ pg = pgClients.client, params, table, id, headers = {}, query = {}, user = {}, contextQuery, sufix = true, filterList, actions: actionsParam, columns: columnsParam, }, reply1, called) { const time = Date.now(); const timeArr = [Date.now()]; const { uid } = user; const reply = reply1 || mockReply; const checkQuery = (item) => user?.user_type === "superadmin" ? !item.includes("{{uid}}") : true; const paramsTable = params?.table || table; if (!paramsTable) { return reply .status(400) .send({ error: "not enough params: table", code: 400 }); } const hookData = (await applyHook("preData", { pg, table: paramsTable, id: params?.id || id, user, })); if (hookData?.message && hookData?.status) { const response = hookData.status >= 400 ? { error: hookData.message, code: hookData.status } : hookData.message; return reply.status(hookData.status).send(response); } /* from tableData START */ const tokenData1 = await getOpt(paramsTable, uid); const tokenData2 = await getToken({ uid, token: paramsTable, json: true, }); const tokenData = tokenData1 || tokenData2; const templateName = tokenData?.table || hookData?.table || paramsTable; const loadTable = await getTemplate("table", templateName); if (tokenData?.table && !loadTable) { const { rows = [], fields = [] } = await pg.query(`select ${tokenData.columns || "*"} from ${tokenData.table} where ${tokenData.query || "true"} limit 10`); const meta = await getMeta({ pg, table: tokenData.table }); const columns = (meta?.columns || fields).map((el) => ({ name: el.name, type: pg.pgType?.[el.dataTypeID], title: el.title, })); return { rows, columns }; } /* from tableData END */ // check sql inline fields count if (!checkInline[paramsTable] && loadTable?.sql?.length && loadTable.table) { const filterSql = loadTable.sql.filter((el) => !el?.disabled && (el.inline ?? true)); const sqlTable = filterSql .map((el, i) => ` left join lateral (${el.sql}) ${el.name || `t${i}`} on 1=1 `) ?.join("") || ""; const d = await Promise.all(filterSql.map((el, i) => pg .query(`select ${el.name || `t${i}`}.* from(select * from ${loadTable.table})t ${sqlTable} limit 0`) .then((item) => item.fields))); d.forEach((el, i) => { filterSql[i].inline = el.length === 1; filterSql[i].fields = el.map((f) => f.name); }); checkInline[paramsTable] = loadTable.sql; } else if (checkInline[paramsTable]) { loadTable.sql = checkInline[paramsTable]; } if (query?.sql === "0") return loadTable; if (!pg) { return reply.status(500).send("empty pg"); } const pkey = pg.pk?.[paramsTable] || pg.pk?.[paramsTable.replace(/"/g, "")]; if (!loadTable && !(tokenData?.table && pg.pk?.[tokenData?.table]) && !(called && pkey)) { return reply.status(404).send("template not found"); } const objectId = tokenData?.id || hookData?.id || params?.id || id; const { actions = [], query: accessQuery } = (await getAccess({ table: templateName, id: objectId, user, }, pg)); const body = loadTable || hookData || tokenData; const { table: table1, columns = [], sql, cardSql, form, meta, sqlColumns, public: ispublic, editable = false, } = loadTable || hookData || tokenData || params || { table }; if (!ispublic && !user?.uid && !called) { return reply.status(401).send({ error: "unauthorized", code: 401 }); } if (!actions.includes("view") && !config?.local && !called) { return reply.status(403).send({ error: "access restricted", code: 403 }); } const { list: filters = [] } = objectId ? {} : (await getFilter({ pg, table: templateName, user, }, null)) || {}; const tableMeta = await getMeta({ pg, table: table1 }); const viewSql = await getTemplate("view", table1); timeArr.push(Date.now()); if (tableMeta?.view) { if (!loadTable?.key && !tokenData?.key) { return reply .status(404) .send({ error: `key not found: ${table1}`, code: 404 }); } Object.assign(tableMeta, { pk: loadTable?.key || tokenData?.key }); } const { pk, columns: dbColumns = [] } = (viewSql ? { pk: loadTable?.key, columns: loadTable?.columns } : tableMeta) || {}; const columns1 = columns || dbColumns.map(({ name, title, dataTypeID }) => ({ name, title, type: pg.pgType?.[dataTypeID], })); columns1.forEach((col) => { Object.assign(col, locales[`${table1 || paramsTable}.${col.name}`] || {}); }); if (!pk) { return reply .status(404) .send({ error: `table not found: ${table1}`, code: 404 }); } const columnList = dbColumns.map((el) => el.name || el).join(","); const sqlTable = sql ?.filter?.((el) => !el?.disabled && !el.inline && el?.sql?.replace && (!el.sql.includes("{{uid}}") || uid)) .map((el, i) => ` left join lateral (${el.sql.replace("{{uid}}", uid)}) ${el.name || `t${i}`} on 1=1 `) ?.join("") || ""; const cardSqlFiltered = objectId ? cardSql?.filter?.((el) => !el?.disabled && el?.name && el?.sql?.replace) || [] : []; const cardSqlTable = cardSqlFiltered.length ? cardSqlFiltered .map((el, i) => ` left join lateral (${el.sql.replace("{{uid}}", uid)}) ct${i} on 1=1 `) .join("\n") || "" : ""; const sqlInline = loadTable?.sql ?.filter?.((el) => el.inline) ?.map((el) => `,(${el.sql})`) ?.join("") || ""; const { fields = [] } = !viewSql ? await pg.query(`select * ${sqlInline} from ${table1} t ${sqlTable} ${cardSqlTable} limit 0`) : await pg.query(`select * from (${viewSql})q limit 0`); const dbColumnsTable = fields.map((el) => el.name); const cols = columns .filter((el) => el.name !== "geom" && dbColumnsTable.includes(el.name)) .map((el) => `"${el.name || el}"`) .join(","); const metaCols = Object.keys(loadTable?.meta?.cls || {}).filter((el) => !cols.includes(el)).length ? `,${Object.keys(loadTable?.meta?.cls || {}) ?.filter((el) => !cols.includes(el)) .join(",")}` : ""; if (objectId && columnList.includes(objectId)) { return gisIRColumn({ pg, layer: paramsTable, column: objectId, sql: query?.sql, filter: query?.filter, search: query?.search, state: query?.state, custom: query?.custom, }); } const isdefault = !objectId ? filters.find((el) => el.default) : null; const checkFilter = [ query?.filter, query?.search, query?.state, query?.custom, isdefault, ].filter(Boolean).length; const fData = checkFilter ? await getFilterSQL({ pg, table: loadTable ? tokenData?.table || paramsTable : table1, filter: query?.filter, search: query?.search, state: query?.state, custom: query?.custom, uid, objectId, filterList, }) : {}; timeArr.push(Date.now()); const keyQuery = query?.key && (loadTable?.key || tokenData?.key) && !objectId ? `${loadTable?.key || tokenData?.key}=$1` : null; const limit = (called ? query?.limit || defaultLimit : Math.min(maxLimit, +(query?.limit || defaultLimit))) || defaultLimit; const offset = query?.page && query?.page > 0 && !objectId ? ` offset ${(query?.page - 1) * limit}` : ""; const order = getOrder(query?.order, query?.desc, loadTable?.order, columnList, !!called); if (config.trace) console.log(order); const search = loadTable?.meta?.search && query?.search ? `(${loadTable?.meta?.search ?.split(",") ?.map((el) => `${el} ilike '%${query?.search .replace(/%/g, "\\%") .replace(/'/g, "''")}%'`) .join(" or ")})` : null; const queryBbox = query?.bbox ? query?.bbox .replace(/ /g, ",") .split(",") ?.map((el) => el - 0) : []; const queryPolyline = meta?.bbox && query?.polyline ? `ST_Contains(ST_MakePolygon(ST_LineFromEncodedPolyline('${query?.polyline}')),${meta.bbox})` : undefined; const bbox = meta?.bbox && queryBbox.filter((el) => !Number.isNaN(el))?.length === 4 ? `${meta.bbox} && 'box(${queryBbox[0]} ${queryBbox[1]},${queryBbox[2]} ${queryBbox[3]})'::box2d ` : undefined; const interfaceName = headers?.referer?.match?.(/.*\/([^?]+)/)?.[1]; const interfaceQuery1 = interfaceName && pg?.pk ? await pg .query("select query from admin.routes where route_id=$1", [ interfaceName, ]) .then((el) => el.rows?.[0]?.query) : null; const interfaceQuery = params?.query ? await handlebars.compile(params?.query)({ user, uid }) : undefined; const where = [ objectId ? ` "${pk}" = $1` : null, keyQuery, loadTable?.query, tokenData?.query, fData?.q, search, accessQuery || "1=1", contextQuery, bbox, queryPolyline, interfaceQuery, interfaceQuery1, ] .filter(Boolean) .filter((el) => checkQuery(el)); const q = `select ${pk ? `"${pk}" as id,` : ""} ${objectId || query?.key ? "*" : columnsParam || sqlColumns || cols || "*"} ${metaCols} ${dbColumns.filter((el) => pg.pgType?.[el.dataTypeID] === "geometry").length && !columnsParam ? `,${dbColumns .filter((el) => pg.pgType?.[el.dataTypeID] === "geometry") .map((el) => `st_asgeojson("${el.name.replace(/'/g, "''")}")::json as "${el.name.replace(/'/g, "''")}"`) .join(",")}` : ""} from (select * ${sql ?.filter((el) => el.inline) .map((el) => `,(${el.sql})`) .join("") || ""} from ${viewSql ? `(${viewSql})` : table1} t ${sqlTable} ) t ${objectId ? cardSqlTable : ""} where ${where.join(" and ") || "true"} ${order} ${offset} limit ${limit}`.replace(/{{uid}}/g, uid); if (config.trace) console.log(q); if (query?.sql === "1") { return q; } const { rows = [] } = await pg .query(q, (objectId ? [objectId] : null) || (query?.key && loadTable.key ? [query?.key] : [])) .catch((err) => { console.error(err.toString()); throw err; }); if (!rows.length && headers?.referer?.includes?.("/card/") && headers?.referer?.includes?.(templateName)) { return reply .status(403) .send({ error: "access restricted: empty rows", code: 403 }); } timeArr.push(Date.now()); if (uid && rows.length && !config.security?.disableToken && (editable || (tokenData?.actions || actionsParam)?.includes("edit") || actions.includes("edit") || actions.includes("del"))) { rows.forEach((row) => { Object.assign(row, { token: setToken({ ids: [ JSON.stringify({ id: row.id, table: templateName, form: loadTable?.form, obj: tokenData?.obj || hookData?.obj, }), ], uid, array: 1, })?.[0], }); }); } const filterWhere = [ fData.q, search, bbox, queryPolyline, interfaceQuery, loadTable?.query, tokenData?.query, ] .filter(Boolean) .filter((el) => checkQuery(el)); const aggColumns = columns .filter((el) => el.agg) .reduce((acc, curr) => Object.assign(acc, { [curr.name]: curr.agg }), {}); const aggregates = dbColumns .map((el) => ({ name: el.name, type: pg.pgType?.[el.dataTypeID], })) .filter((el) => ["numeric", "double precision"].includes(el.type) && aggColumns[el.name]); const qCount = `select count(*)::int as total, count(*) FILTER(WHERE ${filterWhere.join(" and ") || "true"})::int as filtered ${aggregates.length ? `,${aggregates .map((el) => `${aggColumns[el.name]}(${el.name}) FILTER(WHERE ${filterWhere.join(" and ") || "true"}) as ${el.name}`) .join(",")}` : ""} from (select * ${sql ?.filter((el) => el.inline) .map((el) => `,(${el.sql})`) .join("") || ""} from ${viewSql ? `(${viewSql})` : table1} t ${sqlTable})t where ${[loadTable?.query, tokenData?.query, accessQuery, contextQuery] .filter(Boolean) .filter((el) => checkQuery(el)) .join(" and ") || "true"} `.replace(/{{uid}}/g, uid); if (query?.sql === "2") { return qCount; } const counts = keyQuery || objectId ? { total: rows.length, filtered: rows.length } : await pg .queryCache?.(qCount, { table: loadTable?.table || tokenData?.table, time: 5, }) .then((el) => el?.rows[0] || {}); timeArr.push(Date.now()); const { total, filtered } = counts || {}; const agg = Object.keys(counts) .filter((el) => !["total", "filtered"].includes(el)) .reduce((acc, el) => ({ ...acc, [el]: counts[el] }), {}); await extraDataGet({ rows, table: loadTable?.table, form }, pg); await metaFormat({ rows, table: templateName, sufix }, pg); timeArr.push(Date.now()); const status = []; if (loadTable?.meta?.status) { const statusColumn = loadTable.meta?.cls?.[loadTable.meta?.status] ? { name: loadTable.meta?.status, data: loadTable.meta?.cls?.[loadTable.meta?.status], } : loadTable.columns.find((col) => col.name === loadTable.meta?.status) || {}; const statusCls = statusColumn.data || statusColumn.option; const statusClsData = statusCls ? await getSelect(statusCls, pg) : {}; statusClsData?.arr?.forEach((el) => status.push({ ...el, count: rows.filter((row) => el.id === row[statusColumn.name]) .length, })); } const template = await getTemplate("card", templateName); const index = template?.find((el) => el[0] === "index.yml")?.[1] || {}; const html = {}; const { panels = [] } = index; const tokens = {}; if (template && objectId) { // tokens result if (!config.security?.disableToken && index?.tokens && typeof index?.tokens === "object" && !Array.isArray(index?.tokens)) { Object.keys(index.tokens || {}) .filter((key) => index?.tokens[key]?.public || actions?.includes?.("edit") || actions?.includes?.("add") || user?.user_type === "viewer" || !index?.tokens[key]?.table) .forEach((key) => { const item = index?.tokens[key]; Object.keys(item) .filter((el) => item[el]?.includes?.("{{")) .forEach((el) => { item[el] = handlebarsSync.compile(item[el])({ user, uid, id: objectId, data: rows[0], }); }); const token = item.form && item.table ? setToken({ ids: [JSON.stringify(item)], uid, array: 1, })[0] : setOpt(item, uid); tokens[key] = token; }); } // console.log('token'); panels?.forEach?.((panel) => panel?.items ?.filter?.((item) => item.token) ?.forEach?.((el1) => Object.assign(el1, { token: tokens[el1.token] }))); // conditions panels ?.filter((el) => el.items) .forEach((el) => { Object.assign(el, { items: el.items?.filter((item) => conditions(item.conditions, rows[0])), }); }); // title, count await Promise.all(panels .filter((el) => el.items) .map(async (el) => { const filtered1 = el.items.filter((item) => item.count?.toLowerCase?.().includes("select")); const data = await Promise.all(filtered1.map(async (item) => pg .query(item.count.replace(/{{id}}/g, objectId)) .then((item1) => item1.rows[0] || {}))); filtered1.forEach((el1, i) => { // el1.title = data[i].title; Object.assign(el1, data[i] || {}, data[i].count ? {} : { count: undefined }); }); const q1 = el.items .map((item) => item.component ? components[item.component] : null) .filter((item) => item) .join(" union all "); const counts1 = q1 && objectId ? await pg .query(q1, [objectId]) .then((e) => e.rows.reduce((acc, curr) => Object.assign(acc, { [curr.component]: curr.count }), {})) : {}; el.items ?.filter?.((item) => item.component) ?.forEach((item) => Object.assign(item, { count: counts1?.[item.component] })); })); // data result const data = {}; if (index?.data && index?.data?.[0]?.name) { await Promise.all(index.data .filter((el) => el?.name && el?.sql) .map(async (el) => { const q2 = handlebarsSync.compile(el.sql)({ data: rows[0], user, uid, id: objectId, }); const { rows: sqlData } = await pg.query(q2); data[el.name] = sqlData; })); } // html await Promise.all(template .filter((el) => typeof el[1] === "string" && el[0].includes(".hbs")) .map(async (el) => { try { const htmlContent = await handlebars.compile(el[1])({ ...rows[0], user, data, tokens, actions, }); const name = el[0].substring(0, el[0].lastIndexOf(".")); html[name] = htmlContent; } catch (err) { const name = el[0].substring(0, el[0].lastIndexOf(".")); logger.file("handlebars/error", { table, id: objectId, error: err.toString(), stack: err.stack, }); html[name] = "handlebars compile error"; } })); panels?.forEach?.((panel) => { panel?.items?.forEach?.((el) => { if (html[el.name]) { Object.assign(el, { html: html[el.name] }); } el.items?.forEach?.((el1) => { Object.assign(el1, { html: html[el1.name] }); }); }); }); } const route = pg.tlist?.includes?.("admin.routes") ? await pg .query("select route_id as path, title from admin.routes where enabled and alias=$1 limit 1", [paramsTable]) .then((el) => el.rows?.[0] || {}) : {}; const res = { time: { total: Date.now() - time, init: timeArr[1] - timeArr[0], filter: timeArr[2] - timeArr[1], data: timeArr[3] - timeArr[2], count: timeArr[4] - timeArr[3], format: timeArr[5] - timeArr[4], }, route, public: ispublic, tokens, card: !!(loadTable?.card ?? (await getTemplate("card", templateName))), actions: tokenData?.actions || actionsParam || actions || ["view"], total, filtered, count: rows.length, pk, form, agg, status, panels, html, rows, meta, columns: columns1, filters: filters?.map?.((el) => ({ ...el, sql: undefined, label: el.label || el.ua, })), }; if (!res.columns?.length && dbColumns?.length) { Object.assign(res, { columns: dbColumns.map(({ name, title, dataTypeID }) => ({ name, title, type: pg.pgType?.[dataTypeID], })), }); } if (uid && !config.security?.disableToken && actions.includes("add") && !objectId) { const addTokens = setToken({ ids: [ JSON.stringify({ table: templateName, form: loadTable?.form, obj: tokenData?.obj || hookData?.obj, }), ], uid, array: 1, }); Object.assign(res, { addToken: addTokens?.[0] }); } const result = await applyHook("afterData", { pg, table: loadTable?.table || tokenData?.table, id: objectId, template: templateName, payload: res, user, }); return result || res; }