@braindb/remark-dataview
Version:
markdown-graph-content-layer-database
258 lines (257 loc) • 9.31 kB
JavaScript
import nodeSql from "node-sql-parser";
const parser = new nodeSql.Parser();
const opt = { database: "Sqlite" };
import { lex as lexMeta, parse as parseMeta } from "fenceparser";
export const processMeta = (meta) => meta ? parseMeta(lexMeta(meta)) : {};
export function parse(query) {
const ast = parser.astify(query, opt);
if (Array.isArray(ast) && ast.length != 1)
throw new Error("Use only one select statement");
const statement = Array.isArray(ast) ? ast[0] : ast;
if (statement.type !== "select")
throw new Error("Use only one select statement");
return statement;
}
// https://github.com/taozhi8833998/node-sql-parser/blob/master/src/column.js
function columnName(col) {
if (col.as) {
if (typeof col.as === "string")
return col.as;
else
return parser.exprToSQL(col.as, opt);
}
return columnNameExpr(col.expr);
}
function columnNameExpr(expr) {
// @ts-ignore
if (expr.type === "column_ref")
return expr.column;
return parser.exprToSQL(expr, opt);
}
export function transform(query) {
const columns = [];
const newQueryColumns = [];
query.columns.forEach((col) => {
if (col.expr.type == "function") {
const func = col.expr;
if (func.name.name[0].type === "default" &&
func.name.name[0].value === "dv_md") {
if (func.args?.type === "expr_list" &&
Array.isArray(func.args?.value) &&
func.args.value.length === 1) {
columns.push({
dv: true,
name: columnName(col),
func: func.name.name[0].value,
args: func.args.value.map((arg) => columnNameExpr(arg)),
});
func.args.value.forEach((value) => newQueryColumns.push({ as: null, expr: value }));
return;
}
else {
throw new Error("dv_md requires exactly one param");
}
}
if (func.name.name[0].type === "default" &&
func.name.name[0].value === "dv_link") {
if (func.args?.type === "expr_list" &&
Array.isArray(func.args?.value) &&
(func.args.value.length === 2 || func.args.value.length === 0)) {
const args = func.args.value.length === 2
? func.args.value
: [
{
type: "column_ref",
column: "url",
table: "documents",
},
{
type: "binary_expr",
operator: "->>",
left: {
type: "column_ref",
table: "documents",
column: "frontmatter",
},
right: { type: "single_quote_string", value: "$.title" },
},
];
columns.push({
dv: true,
name: columnName(col),
func: func.name.name[0].value,
args: args.map((arg) => columnNameExpr(arg)),
});
args.forEach((value) => newQueryColumns.push({ as: null, expr: value }));
return;
}
else {
throw new Error("dv_link requires 0 or 2 params");
}
}
if (func.name.name[0].type === "default" &&
func.name.name[0].value === "dv_task") {
if (func.args?.type === "expr_list" &&
Array.isArray(func.args?.value) &&
(func.args.value.length === 2 || func.args.value.length === 0)) {
const args = func.args.value.length === 2
? func.args.value
: [
{
type: "column_ref",
column: "ast",
table: "tasks",
},
{
type: "column_ref",
column: "checked",
table: "tasks",
},
];
columns.push({
dv: true,
name: columnName(col),
func: func.name.name[0].value,
args: args.map((arg) => columnNameExpr(arg)),
});
args.forEach((value) => newQueryColumns.push({ as: null, expr: value }));
return;
}
else {
throw new Error("dv_task requires 0 or 2 params");
}
}
}
columns.push({ dv: false, name: columnName(col) });
newQueryColumns.push(col);
});
return {
query: parser.sqlify({ ...query, columns: newQueryColumns }, opt),
columns,
};
}
const text = (value) => ({ type: "text", value });
// const emphasis = (children: PhrasingContent[]): Emphasis => ({
// type: "emphasis",
// children,
// });
const strong = (children) => ({
type: "strong",
children,
});
const paragraph = (children) => ({
type: "paragraph",
children,
});
const link = (url, children) => ({
type: "link",
title: null,
url: url,
children,
});
const list = (children) => ({
type: "list",
ordered: false,
start: null,
spread: false,
children,
});
const listItem = (children, checked = null) => ({
type: "listItem",
spread: false,
checked,
children,
});
const root = (children, className) => ({
type: "root",
children,
data: {
hName: "div",
hProperties: {
className,
},
},
});
const columnToMdast = (column, row) => {
if (column.dv === false)
return [text(String(row[column.name]))];
switch (column.func) {
case "dv_md":
return JSON.parse(row[column.args[0]])?.children || [];
case "dv_link":
return [link(row[column.args[0]], [text(row[column.args[1]])])];
case "dv_task":
return [
listItem(JSON.parse(row[column.args[0]])?.children || [], Boolean(row[column.args[1]])),
];
default:
throw new Error(`Unknown function ${column.name}`);
}
};
const handleStar = (columns, rows) => {
const sqlCols = new Set(columns.flatMap((col) => (col.dv === false ? col.name : col.args)));
if (sqlCols.has("*")) {
const resCols = Object.keys(rows[0]).filter((col) => !sqlCols.has(col));
const starPos = columns.findIndex((col) => col.name === "*");
return [
...columns.slice(0, starPos),
...resCols.map((name) => ({ dv: false, name })),
...columns.slice(starPos + 1),
];
}
return columns;
};
export const generateTable = (columns, rows) => {
columns = handleStar(columns, rows);
if (rows.length === 0)
return "empty";
const first = rows[0];
if (first === null || typeof first !== "object")
return "wrong result";
const align = columns.map(() => "left");
return {
type: "table",
align,
children: [
{
type: "tableRow",
children: columns.map((value) => ({
type: "tableCell",
children: [text(value.name)],
})),
},
...rows.map((row) => ({
type: "tableRow",
children: columns.map((column) => ({
type: "tableCell",
children: columnToMdast(column, row),
})),
})),
],
};
};
export const generateList = (columns, rows, options = {}) => {
columns = handleStar(columns, rows);
if (columns.length === 1)
return list(rows.map((row) => listItem(columnToMdast(columns[0], row))));
if (columns.length === 2) {
const grouped = {};
const firstName = columns[0].dv == true ? columns[0].args[0] : columns[0].name;
rows.forEach((row) => {
const first = row[firstName];
grouped[first] = grouped[first] || [];
grouped[first].push(row);
});
return root(Object.values(grouped).flatMap((group) => {
const first = columnToMdast(columns[0], group[0]);
return [
paragraph([strong(first)]),
list(group.map((row) => {
const val = columnToMdast(columns[1], row);
return val[0].type === "listItem" ? val[0] : listItem(val);
})),
];
}), options.root_class);
}
throw new Error("List expects 1 or 2 columns");
};