前段时间我发现了一个名为 ThingsDB 的数据库。我对此很好奇,并读了一些书。我发现它们支持 TCP 连接,但没有针对某些特定平台的驱动程序,因此我为 javascript 和 php 开发了一个驱动程序。
当我研究 javascript 驱动程序时,我意识到,可以直接从前端使用 ThingsDB,而无需任何后端或中间件。您可以从浏览器打开 websocket (TCP) 连接,因此我联系了 ThingsDB 的作者,他们添加了对 websocket 的支持(可从 ThingsDB 版本 1.6 开始)。这样我的 javascript 驱动程序就可以从前端(浏览器)使用,也可以从基于 javascript 的后端(例如 node.js)使用。我在这里写了一篇关于我的 php 驱动程序的文章,我收到了有趣的反馈。人们希望看到 ThingsDB 的更多潜力。基于此,我选择在完成后不立即写关于我的 javascript 驱动程序的文章,但我决定最好制作演示。
要了解 ThingsDB 的基础知识和这个演示,我建议您继续阅读我解释的具体功能。 我希望您熟悉一般的编程,至少是基础知识。也许还有一些 javascript 和 jQuery。
如果您想通过在 ThingsDB 内部执行代码片段来完成本文的操作,则必须使用安装指南中提到的附加 docker 文件。
首先要做的事情。让我简短地解释一下结构。
ThingsDB 包含集合。集合包含数据、过程、任务、数据类型和枚举。还有先前的集合(范围)@thingsdb,其中包含用户访问帐户,并且还可以包含过程和任务。最后还有 @node 作用域,它现在并不重要。
所有命名的事物,如数据、过程、任务、数据类型和枚举,都是由实现 ThingsDB 的开发人员定义的。该数据库的新实例仅包含名为 @:stuff 的空集合和用户帐户 admin。我使用这个集合作为本次演示的主要集合。
当您在 ThingsDB 上执行查询或运行过程时,您必须指定它将在哪个集合上运行。这有时可能会受到限制,如果您需要在另一个集合上执行查询或运行过程,有一种方法可以实现这一点。有一个名为 thingsdb(书籍,GitHub)的模块,它允许您以特定用户的身份从集合中访问另一个集合。我的演示在处理用户帐户时大量使用此功能,这就是我在这里提到它的原因。我已经按照手册中的说明安装了该模块。
我稍后会解释权限,但仅供参考:我为此模块创建的用户帐户具有对集合 @thingsdb 的查询、更改、授予权限和对集合 @:stuff 的更改、授予权限。
我选择仅使用 ThingsDB,这意味着我必须使用他们的用户帐户。我必须处理注册和登录,由于没有后端,这有点棘手。当然,我可以使用一些第三方身份验证服务器(auth0等),但我不想依赖其他任何东西。
如果有人想要实现第 3 方身份验证系统,您可以使用请求模块(书籍,GitHub)从 ThingsDB 发出 HTTP 请求。
为了允许用户注册,我需要一些用户帐户来与 ThingsDB 通信并执行注册。但该帐户所需的凭据将在 JavaScript 代码中发布,这听起来不太安全。我不想处理所有安全问题,但我想至少实现简单的问题。 ThingsDB 支持为每个用户帐户专门针对每个集合授予权限。可授予的权限包括查询、更改、授予、加入和运行。
我根本无法使用查询。因为使用此命令您可以在 ThingsDB 上执行任何操作,并将其打开到客户端浏览器会带来巨大的安全问题。路径很清晰,我必须使用程序并只允许客户端运行。
需要了解的重要信息是用户帐户不仅有密码,还有访问令牌(如果需要的话,会过期)。
我创建了集合 @:auth 和名为 aa(auth 帐户)的用户帐户,并授予他运行 权限来处理此集合。集合@:auth 只包含一个名为register 的过程。所有这一切意味着,用户 aa 只能做一件事,那就是运行名为 register 的过程。因此他的访问令牌可以被发布。
程序注册确实创建新帐户并授予所需的权限。代码如下所示:
new_procedure('register', |email, password| { if (email.len() == 0 || password.len() == 0 || !is_email(email)) { raise('required values not provided'); }; thingsdb.query('@t', " if (has_user(email)) { raise('email already registered'); }; new_user(email); set_password(email, password); grant('@:stuff', email, RUN | CHANGE); ", { email:, password:, }); nil; });
我想这是您第一次看到 ThingsDB 的代码。它与另一种编程语言很相似,只是略有变化。该程序的作用:
电子邮件:,可能有点令人困惑,但当您想将变量传递给参数并且参数和变量具有相同名称时,它是一种简写。
@t 是 @thingsdb 范围的快捷方式。
在 ThingsDB 方面一切准备就绪后,我创建了带有注册表单和几行 JavaScript 的简单网站。设法在 ThingsDB 内部运行过程的代码片段如下所示:
const thingsdb = new ThingsDB(); thingsdb.connect() .then(() => thingsdb.authToken(localStorage.getItem('aa'))) .then(() => thingsdb.run('@:auth', 'register', [ $('#email').val(), $('#password1').val() ]))
我将用户 aa 的访问令牌保留在浏览器 localStorage 中。
要查看整个实现,请看这里:
用户能够注册后,下一步是实现登录操作。登录需要密码,但将用户密码存储在浏览器中不太安全。解决方案是在登录后生成访问令牌(有过期时间)并将其返回给客户端,然后将其存储在浏览器中(例如 sessionStorage)。因此,我在 @:stuff 集合中创建了程序,其中注册用户帐户具有所需的权限。
new_procedure('login', || { email = user_info().load().name; if (is_email(email)) { thingsdb.query('@t', "new_token(email, datetime().move('days', 1));", {email: }) .then(|token| token); }; });
令牌的创建必须在@thingsdb范围内调用,在这种情况下我再次使用thingsdb模块。调用此过程的 JavaScript 代码片段如下所示:
const thingsdb = new ThingsDB(); thingsdb.connect() .then(() => thingsdb.auth($('#email').val(), $('#password').val())) .then(() => thingsdb.run('@:stuff', 'login')) .then(token => { sessionStorage.setItem('token', token); window.location.href = './overview.html'; })
获取的访问令牌存储在sessionStorage中。
在这里您可以查看整个登录页面,其中包含登录表单和所需的 javascript 代码:
登录后,用户将被重定向到此处,其中他有一些帐户操作和他的待办事项列表。这需要指定结构、Todo 数据的存储方式,为此我们可以使用数据类型。我创建了 Todo 类型,其中包含 name、user_id 和 items。 项目类型有描述、检查状态和待办事项参考。 Todo 和 Item 之间的连接是通过双向关系(书籍、文档)建立的。这两种类型都在 @:stuff 集合中定义。
new_type('Item'); new_type('Todo'); set_type('Item', { description: "'str'," checked: 'bool', todo: 'Todo?', }); set_type('Todo', { name: 'str', items: '{Item}', user_id: 'int', }); mod_type('Item', 'rel', 'todo', 'items');
在这段代码中,您可以看到类型是如何创建的,它们具有哪些数据类型的属性以及它们之间的关系的设置。
但这只是定义。我们需要将 Todos 存储在某个地方。为此,我们直接在集合 @:stuff 上创建属性,如下所示。如果没有点,它只是可变的,并且不会持久。
.todos = set();
现在数据结构准备好后,让我们来看看每个动作。
加载概述页面后,会请求将用户的 Todos 加载到 ThingsDB。首先,我们需要一个关于 @:stuff 集合的过程,它返回 Todos 列表:
new_procedure('list_todos', || { user_id = user_info().load().user_id; .todos.filter(|t| t.user_id == user_id); });
过滤器是可在集合上调用的函数。
现在我们可以使用这样的 javascript 代码片段来调用此过程(省略处理接收到的数据):
const thingsdb = new ThingsDB(); thingsdb.connect() .then(() => thingsdb.authToken(sessionStorage.getItem('token'))) .then(() => thingsdb.run('@:stuff', 'list_todos')) .then(todos => { })
您可以在这里检查整个实现:
对于此操作,我创建了过程 update_password,它需要再次使用 thingsdb 模块。用户帐户存储在@thingsdb范围内。
new_procedure('update_password', |password| { email = user_info().load().name; if (is_email(email)) { thingsdb.query('@t', 'set_password(email, password);', { email:, password:, }); }; });
我使用 html 对话框标签输入新密码,处理它的 javascript 代码片段非常简单:
thingsdb.run('@:stuff', 'update_password', [$('#password1').val()])
我不必再次调用 authToken,因为 websocket 连接仍然从加载 Todos 的请求中打开。
您可以在这里检查整个实现:
此操作的过程不仅会删除用户帐户,还会删除他的待办事项。看起来像这样:
new_procedure('delete_user', || { email = user_info().load().name; if (is_email(email)) { .todos.remove(|todo| todo.user_id == user_id); thingsdb.query('@t', 'del_user(email);', {email: }); }; });
Remove is another function which can be called on set.
I had to use thingsdb module again. User accounts are stored in @thingsdb scope.
Call of this procedure can be done easily with javascript code snippet:
thingsdb.run('@:stuff', 'delete_user')
I don't have to call authToken again because websocket connection is still open from the request to load Todos.
Look at the whole implementation here:
User need a way to create new Todo. For that reason I made page new_todo and overview contains link to it. Form to create todo consist of todo name and items (descriptions). I decided to store new Todo with items in two steps, because originally I wanted to allow editing of Todo (which in the end didn't happen). Therefore I've created two new procedures.
new_procedure('create_todo', |name| { t = Todo{ name:, user_id: user_info().load().user_id, }; .todos.add(t); t.id(); }); new_procedure('add_todo_items', |todo_id, items| { todo = thing(todo_id); if (todo.user_id != user_info().load().user_id) { raise('Not yours'); }; todo.items.clear(); items.each(|i| { item = Item{ checked: false, description: "i," }; todo.items.add(item); }); });
First procedure to create todo returns it's id and second procedure deletes all items and adds new ones. I think if you read until here you are already getting hang of it and I don't have to explain .todos.add() or items.each() (set add, thing each).
What is new here is thing(todo_id). You can get reference to any thing (thing is like instance of class/data type) from collection by id. You don't have to know where is stored, you can just get it. Thing has assigned id when is stored persistently.
To perform defined action you just have to call it with javascript code snippet:
thingsdb.run('@:stuff', 'create_todo', [$('#name').val()]) .then((todo) => thingsdb.run('@:stuff', 'add_todo_items', [ todo, items.length ? items.map(function () { return $(this).val(); }).get() : [] ]))
Look at the whole implementation here:
Overview page shows list of user Todos. By clicking on it user is redirected to page where he can see Todo items, change their status and delete whole Todo list.
To load one specific Todo I've created new procedure:
new_procedure('list_todo', |todo_id| { todo = thing(todo_id); if (todo.user_id != user_info().load().user_id) { raise('Not yours'); }; return todo, 2; });
Now you are propably asking why there is return todo, 2;? With return you can set depth of data you want to return. With number 2 here returned data contains not only Todo itself, but also Items the Todo has relation with.
Because Todo id is passed as uri get parameter, the javascript code snippet to call this procedure looks like this:
thingsdb.run('@:stuff', 'list_todo', [ parseInt(location.search.match(/id=(\d+)/)[1]) ])
Look at the whole implementation here:
todo.html
todo.js
I render todo items as checklist, so to change status of item I've created new procedure:
new_procedure('mark_item', |item_id, checked| { item = thing(item_id); if (item.todo.user_id != user_info().load().user_id) { raise('Not yours'); }; item.checked = checked; nil; });
Because you can also uncheck, not only check item, javascript code snippet has to be like this:
thingsdb.run('@:stuff', 'mark_item', [ parseInt(this.id), $(this).is(':checked') ])
Look at the whole implementation here:
todo.html
todo.js
If we want to delete Todo, we don't have to delete items because they are not stored separately. If Todo is removed, no other reference exists for its items and they are automatically removed.
new_procedure('delete_todo', |todo_id| { todo = thing(todo_id); if (todo.user_id != user_info().load().user_id) { raise('Not yours'); }; .todos.remove(todo); });
Now the javascript code snippet is simple:
thingsdb.run('@:stuff', 'delete_todo', [ parseInt(location.search.match(/id=(\d+)/)[1]) ])
Look at the whole implementation here:
todo.html
todo.js
To simplify usage of this demo you can run ThingsDB in docker with Dockerfile. At the end of this file you find required commands as comments. Instance of ThingsDB made with this Dockerfile is based on specific branch which was not yet released and introduces using user_info() inside of collections.
Next simply open install.html which creates everything required in this ThingsDB instance and store access token of aa user to localStorage.
That's it. I hope I gave you basic insight into this technology. If you like my work you can buy me a tea.
No AI was used to generate this content, only the cover picture.
以上是让我解释一下 ThingsDB Todo 应用程序演示的详细内容。更多信息请关注PHP中文网其他相关文章!