Skip to content

Query embed block supports executing JavaScript #9648

Closed
@frostime

Description

@frostime
Contributor

允许嵌入块内执行 Js 以增强复杂查询能力的提案

In what scenarios do you need this feature?

  • 需求:希望能允许在嵌入块内执行 JS,以弥补原始的 SQL 本身逻辑不足的缘故

  • 理由:SQL 虽然强大,但是由于缺失基本的顺序执行。条件片段等逻辑,在处理复杂逻辑查询方面会异常受限

    • 一方面:我个人高强度使用 SQL 查询,但是还是会遇到「即便写了非常复杂的 sql 语句仍然得不到满意的查询效果」的情况
    • 另一方面:友商的查询语法选择了自定义的 DSL,虽然不能像 SQL 那样通用强大,但是却可以定义简单的执行逻辑,从而在特定的复杂查询方面胜过 SQL
  • 思路

    • 不引入新的 DSL,而是直接允许使用 js 代码来控制嵌入块的内部逻辑
    • 不变更思源本体机制的前提下,通过一些小 trick 变相能在嵌入块中执行 js 逻辑
    • 原则:动刀子尽可能小,绝对不涉及大规模代码更新甚至重构

Describe the optimal solution

具体的实施方案分为两个部分:

  • 第一部分:如何告知思源,某一个嵌入块要执行 js 代码而非单纯的执行 SQL 查询
  • 第二部分:如何具体的将 js 翻译成需要查询的块。

1. 通过(在嵌入块开头加入 shebang || 设置自定义属性)来声明本嵌入块是否要执行 js

  • 方案总结

    1. 方案一:当嵌入块内容以以下声明为开头的时候,将这个嵌入块视为一个可执行 js 的嵌入

      //!js
    2. 方案二:设置自定义属性,特定属性的嵌入块会被认为应该作为 js 来执行

  • 具体说明

    嵌入块的相关代码在https://github.com/siyuan-note/siyuan/blob/master/app/src/protyle/render/blockRender.ts,其中第 32 行会读取嵌入块的内容并当作一个 sql 查询内容,然后交给 /api/search/searchEmbedBlock API 执行

    image

    所以只需要在 32 行后面加上判断即可获知改嵌入块是否需要用 js 来执行

2. 如何将 js 转化为嵌入块查询:两个独立的方案

  • 基本原理

    1. 规定: JS嵌入块最后一句,应当返回一个 Block[]

    2. 在前端部分,将 JS 嵌入块代码放入 Function 对象中,并提供一个 sql api 用于查询 sql

      • sql api 可以直接比照前端的 api
      • 为了保证数据安全,可以在 Function 对象内部禁用 fetch 等可能引起安全问题的 API
    3. 根据 JS 嵌入块代码返回的 Block 列表来构造嵌入块,具体而言有两种方案

      • 【优选】后端方案:让后端加一个 API,好处是非常直接直观后面也方便继续优化,坏处是后端需要加一个额外的 API
      • 【次选】前端方案:把 js 翻译成 selelct * from id in (...) 语句,坏处是不够直接且可能让前端代码逻辑变复杂,好处是后端完全无感
2.1 后端方案:后端提供一个新的 API,直接给定 block list 返回嵌入块查询

由弘哥首先提议

image

我后面也研究了一下后端嵌入块查询的调用过程,觉得应该可以实现。只需要去掉 search.go 的调用,直接用前端给的 block list 就行。

image

2.2 前端方案:将 js 执行过程拼接为新的 SQL 语句

基本思路是:

  • 执行 js 代码,获取 block list

  • 将 block list 拼成 select * from blocks where id in ("id1", "id2", ...),比如这样:

    const func = async () => {
        let sql = `select * from blocks where content like "%Python%" and root_id != '20231114083336-jti8mwu' and type not in ('t', 's') order by updated desc limit 8 ;`;
        const blocks = await api.sql(sql);
        let idList = blocks.map((block) => `"${block.id}"`);
        sql = `select * from blocks where id in (${idList.join(",")})`;
        return sql
    }
  • 将新的构造的 SQL 语句发往后端

  • 将返回结果按照发送时候的顺序重新排列一下

    • 我自己做了一下测试,发现 select * from blocks where id in ("id1", "id2", ...) 语句可以查询到正确的结果,但是嵌入块内部DOM的排列顺序却是乱的,所以为了保证顺序正确,需要前端在 searchEmbedBlock 的返回结果中将 response.data.blocks 重新排序一下。

image

补充:为什么不想做成插件

  1. 希望操作更加顺滑
  2. 希望复用原生的嵌入块显示能力
  3. 插件能力有限,就比如如果要使用插件,只能用「前端方案」,但是由于无法控制后端的逻辑,所以查询的结果会乱序

Activity

added this to the backlog milestone on Nov 14, 2023
88250

88250 commented on Nov 16, 2023

@88250
Member

后端方案中需要新增的内核接口麻烦描述一下入参出参。

另外,这里的 //!js 是参考 sh 吗?

image

可能在前端开发看来会理解为 非js 😂

changed the title [-]允许嵌入块内执行 Js 以增强复杂查询能力的提案[/-] [+]Query embed block supports executing JavaScript[/+] on Nov 16, 2023
moved this to Short Term in SiYuan Roadmapon Nov 16, 2023
frostime

frostime commented on Nov 16, 2023

@frostime
ContributorAuthor

后端方案中需要新增的内核接口麻烦描述一下入参出参。

另外,这里的 //!js 是参考 sh 吗?

image

可能在前端开发看来会理解为 非js 😂

关于出入参数,也许可以参考一下 /api/search/searchEmbedBlock 的接口?比如 searchEmbedBlock 的输入是

{
    embedBlockID: string,
    stmt: string,
    headingMode: 1 | 0,
    excludeIDs: [item.getAttribute("data-node-id"), protyle.block.rootID],
    breadcrumb: boolean
}

可以仿照这个接口,直接输入给定的 block id

{
    embedBlockID: string,
    includeIDs: string[],
    headingMode: 1 | 0,
    breadcrumb: boolean
}

至于输入和 searchEmbedBlock 保持一致,都是吧。

{
	blocks: blocks
}

那个 //!js 确实就是无脑模仿 sh,当时如果不合适也可以换成别的,比如就一个 //js;或者不一定用 shenbang,用块自定义属性判断也行。😂

88250

88250 commented on Nov 16, 2023

@88250
Member

能否来一段脚本示例,比如完成某个功能的,这样我们好具体再讨论看看。

frostime

frostime commented on Nov 16, 2023

@frostime
ContributorAuthor

能否来一段脚本示例,比如完成某个功能的,这样我们好具体再讨论看看。

以下面这个例子为例,这个是我使用 RunJs 插件运行的,这个插件就是基于 Function 来运行 js 的。

下面这个案例的目标是,将查询的结果,按照笔记本自定义排序的大小来排列。程序最后返回一个 Promise<Blocks[]> 对象,可以在调用 Function 对象的代码中获取返回值。

//js
const notebooks = window.siyuan.notebooks.reduce(
    (target, key, index) => { target[key['id']] = key; return target;},
    {}
);
/**
 * 查询的结果按照文档树上笔记本自定义排列的顺序排列
 */
const query = async () => {
    //为了方便测试,加了 order by random()
    let blocks = await api.sql(`
    select * from blocks where created like '20231116%' order by random() limit 16`);
    blocks.sort((b1, b2) => notebooks[b1.box].sort - notebooks[b2.box].sort);
    console.log(blocks.map((b) => {return {box: notebooks[b.box].name, content: b.content};}));
    return blocks;
}
//返回一个 block 列表,嵌入块就根据这个 block 的内容和顺序来展示
return query();

image

前端获取到返回的 Block[] 后,可以将对应的 id 传入includeIDs参数,从而来要求内核获取相应的笔记内容。


实际操作的时候,还可以做一些简化,比如可以允许用户只编写 query 函数内部的内容,然后前端部分将其包裹在一个 async () => {${code}} 内部从而规避 js 不能在顶层域中 await 的限制。

88250

88250 commented on Nov 17, 2023

@88250
Member

感谢,我还需要确认下,需要内核新开接口主要原因是 /api/query/sql 接口返回不了嵌入块接口需要的格式对吧,后半段要使用已有嵌入块渲染的代码?

frostime

frostime commented on Nov 17, 2023

@frostime
ContributorAuthor

感谢,我还需要确认下,需要内核新开接口主要原因是 /api/query/sql 接口返回不了嵌入块接口需要的格式对吧,后半段要使用已有嵌入块渲染的代码?

对,或者更确切的说是由于 SQL 语法本身的一些缺陷,有些复杂一点的查询难以或者无法实现;希望可以通过引入 js 的程序逻辑来增强查询的能力。

66 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

Status

Already Done

Relationships

None yet

    Development

    No branches or pull requests

      Participants

      @88250@Vanessa219@IAliceBobI@frostime@Zuoqiu-Yingyi

      Issue actions

        Query embed block supports executing JavaScript · Issue #9648 · siyuan-note/siyuan