Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query embed block supports executing JavaScript #9648

Closed
frostime opened this issue Nov 14, 2023 · 57 comments
Closed

Query embed block supports executing JavaScript #9648

frostime opened this issue Nov 14, 2023 · 57 comments
Assignees
Labels
Milestone

Comments

@frostime
Copy link
Contributor

frostime commented Nov 14, 2023

允许嵌入块内执行 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. 插件能力有限,就比如如果要使用插件,只能用「前端方案」,但是由于无法控制后端的逻辑,所以查询的结果会乱序
@88250 88250 added this to the backlog milestone Nov 14, 2023
@88250 88250 added Feature and removed Enhancement labels Nov 15, 2023
@88250
Copy link
Member

88250 commented Nov 16, 2023

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

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

image

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

@88250 88250 changed the title 允许嵌入块内执行 Js 以增强复杂查询能力的提案 Query embed block supports executing JavaScript Nov 16, 2023
@frostime
Copy link
Contributor Author

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

另外,这里的 //!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
Copy link
Member

88250 commented Nov 16, 2023

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

@frostime
Copy link
Contributor Author

frostime commented Nov 16, 2023

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

以下面这个例子为例,这个是我使用 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
Copy link
Member

88250 commented Nov 17, 2023

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

@frostime
Copy link
Contributor Author

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

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

@88250
Copy link
Member

88250 commented Nov 17, 2023

嗯,逻辑增强我知道,上面主要问的是实现上的参数格式。这样吧,我们再考虑看看,也许下个特性版 v2.11.0 就安排上。

@88250
Copy link
Member

88250 commented Nov 18, 2023

实现方案:

前端 @Vanessa219

@IAliceBobI
Copy link

image

是否还可以有另外的方案?

这里能否直接由js,给出符合渲染需要的数据格式。不需要js来拼装sql?

@frostime
Copy link
Contributor Author

image

是否还可以有另外的方案?

这里能否直接由js,给出符合渲染需要的数据格式。不需要js来拼装sql?

这个图是前端方案。不过目前来看官方选择的不是这个方案,而是另外一个后端方案。

Vanessa219 added a commit that referenced this issue Nov 22, 2023
@Vanessa219
Copy link
Member

目前前端支持 return Promise 或者 array

//!js 
return ["20230506210010-houyyvy"]

//!js 
const query = async () => {
    let blocks = await fetchSyncPost("/api/system/getConf", {});
    return ["20230506210010-houyyvy"]
}
return  query()

@zxhd863943427
Copy link
Contributor

能否进一步解偶,直接由js运行getEmbeBlock?

@Vanessa219
Copy link
Member

目前是通过执行 js 获取 id 数组,进一步解耦要如何做?

@zxhd863943427
Copy link
Contributor

初步想法是嵌入块直接使用js返回的html,但是从代码来看,/api/search/getEmbedBlock 的返回结果好像不是html数组
,使用的renderEmbeBlocks也不是返回html,而是直接操作protyle,emmm,感觉要改还是需要仔细想想怎么操作。

@88250
Copy link
Member

88250 commented Nov 22, 2023

现在这个方式我觉得还行吧,如果是想一步到位获取组装好的嵌入块结构,后续我们可以再提供其他接口。

@zxhd863943427
Copy link
Contributor

我先研究一下接口,还不知道真的返回了什么,新版还没测试。

主要想法不是在内核返回了什么,而是嵌入块能显示什么能否自定义。比如说,能否直接显示一个表格?

@88250
Copy link
Member

88250 commented Nov 22, 2023

那恐怕不行,这个和编辑器相关。

@Vanessa219
Copy link
Member

嗯嗯,顺便把返回空的情况处理一下,不要再走 renderEmbed 即可。你直接 PR 吧。

Vanessa219 added a commit that referenced this issue Nov 23, 2023
@zxhd863943427
Copy link
Contributor

还需要一个api来更新块的content,目前sql api只能查询,无法执行update操作。

@88250
Copy link
Member

88250 commented Nov 23, 2023

@zxhd863943427 你说的是内核还是前端?

@zxhd863943427
Copy link
Contributor

zxhd863943427 commented Nov 23, 2023

前端,需要一个新的api更新块的content,便于索引内容。

话说其实挂件块也是需要这个api的。

@88250 88250 mentioned this issue Nov 23, 2023
4 tasks
@Vanessa219
Copy link
Member

api/transactions 这个么?

@zxhd863943427
Copy link
Contributor

并不是,是 sqlite 的 blocks 表里的 content 字段,这个是用来搜索的字段,自定义渲染内容最好能自己更新这个字段,才能被搜索到。如果直接写在备注、别名等位置,虽然可以达成搜索的效果,但是会污染原有的 sy 文件。

@88250
Copy link
Member

88250 commented Nov 24, 2023

@zxhd863943427 你说的是 js 嵌入块需要更新 content 字段对吧?这里目前是不支持自动索引的,因为 go 里面没法执行 js:

image

所以是需要加个接口来主动更新 content 字段?

@zxhd863943427
Copy link
Contributor

对的,需要一个接口,至少要能实现这种操作:

 fetchSyncPost('/api/query/updateContent',{"id": id, "content": content})

@88250
Copy link
Member

88250 commented Nov 24, 2023

执行 searchEmbedBlock/getEmbedBlock 的时候后端会自动索引的,怕是没有必要重复索引了:

image

@zxhd863943427
Copy link
Contributor

但是,searchEmbedBlock/getEmbedBlock 自动索引的内容和自定义渲染的内容可能是不一样的。

@88250
Copy link
Member

88250 commented Nov 24, 2023

那等晚点我们还是提供一个更新 content 的内核接口吧。

@zxhd863943427
Copy link
Contributor

另外,api/search/getEmbedBlock的返回感觉缺了好多值:

 "block": {
                    "box": "20230917171404-1s8stgn",
                    "path": "/20231122202402-vcooo3s.sy",
                    "hPath": "test/2222222",
                    "id": "20231123131215-t1m9qa5",
                    "rootID": "",
                    "parentID": "",
                    "name": "",
                    "alias": "",
                    "memo": "",
                    "tag": "",
                    "content": "\u003cdiv data-node-id=/div\u003e...",
                    "fcontent": "",
                    "markdown": "而言,插件主要面向程序功能扩展,不面向内容块扩展。",
                    "folded": false,
                    "type": "NodeParagraph",
                    "subType": "",
                    "refText": "",
                    "refs": null,
                    "defID": "",
                    "defPath": "",
                    "ial": {
                        "alias": "11",
                        "bookmark": "111",
                        "custom-avs": "20230808232238-9qwehxq,20231124153111-pdtnnfo",
                        "custom-block": "1111",
                        "custom-temp": "111",
                        "id": "20231123131215-t1m9qa5",
                        "memo": "222",
                        "name": "111",
                        "updated": "20231123133525"
                    },
                    "children": null,
                    "depth": 0,
                    "count": 0,
                    "sort": 0,
                    "created": "",
                    "updated": "",
                    "riffCardID": "",
                    "riffCard": null
                },

什么部分是会正确返回,什么只是留一个空字符串,这个能说明吗

@88250
Copy link
Member

88250 commented Nov 24, 2023

好像提供更新 content 的接口会有问题,因为这个 content 是和 block 绑定的,如果提供更新 content 的接口的话 block 变动后就不一致了。

@88250
Copy link
Member

88250 commented Nov 24, 2023

嵌入块返回的结果很多字段没有填充,前端目前没有用上的就不填充了。

@zxhd863943427
Copy link
Contributor

好像提供更新 content 的接口会有问题,因为这个 content 是和 block 绑定的,如果提供更新 content 的接口的话 block 变动后就不一致了。

额,不乱用应该没问题,毕竟自定义渲染肯定是每次更新都会重新调用一次。

@88250
Copy link
Member

88250 commented Nov 24, 2023

改了原块就会被自动覆盖,失去效果了。数据库上最好是只读单向的,仅从 block -> content 方向索引,否则后面可能会引起其他问题。

@zxhd863943427
Copy link
Contributor

等等,和block绑定,是哪个block?

@88250
Copy link
Member

88250 commented Nov 24, 2023

编辑器里面的 block

@zxhd863943427
Copy link
Contributor

为什么getEmbedBlock没有违反这个规则呢?

@88250
Copy link
Member

88250 commented Nov 24, 2023

searchEmbedBlock/getEmbedBlock 仅更新嵌入块的 content,不能更新其他块。

@zxhd863943427
Copy link
Contributor

那能否同样提供一个能自定义内容、仅更新嵌入块的 content的api?

@88250
Copy link
Member

88250 commented Nov 24, 2023

能是能,就是感觉破坏单向的设计怕后面出事 😂

@88250
Copy link
Member

88250 commented Nov 24, 2023

后面试试这个接口 #9736

@Zuoqiu-Yingyi
Copy link
Contributor

@Vanessa219 @88250 该功能的具体使用方法是否未写入用户手册中❓

@88250
Copy link
Member

88250 commented Apr 26, 2024

没有写,哪位有时间的话帮忙补充一下吧,谢谢。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Already Done
Development

No branches or pull requests

7 participants