」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > 讓我解釋一下 ThingsDB Todo 應用程式演示

讓我解釋一下 ThingsDB Todo 應用程式演示

發佈於2024-08-28
瀏覽:667

Some time ago I've discovered a database called ThingsDB. I was curious about it and I did some reading. I've discovered they support TCP connectivity but they didn't have driver for some specific platforms therefore I've developed a driver for it for javascript and for php.

When I worked on javascript driver I've realized, it would be possible to use ThingsDB directly from frontend without any backend or middleware. You can open websocket (TCP) connection from browser, so I reached out to the authors of ThingsDB and they added support for websocket (available from ThingsDB version 1.6). This way my javascript driver can be used from frontend (browser) and also from javascript based backend (ex. node.js). I wrote article here about my php driver where I have received interesting feedback. People wanted to see more of the potential of ThingsDB. Based on that I chose not to write article about my javascript driver right after I've finished it, but I've decided it would be best to make demo.

To understand basics of ThingsDB and this demo I suggest you to read continuosly as I explain specific features on the way. I expect you are familiar with programming in general, at least basics. And maybe some javascript and jQuery.

If you want to follow this article by executing code snippets inside of ThingsDB, you have to use attached docker file mentioned in Installation guide.


ThingsDB structure introduction

First things first. Let me shortly explain the structure.

ThingsDB contains collections. Collection contains data, procedures, tasks, data types and enums. There is also prior collection (scope) @thingsdb which contains user access accounts and it can also contains procedures and tasks. Lastly there is @node scope which is not important right now.

  • Data - persistent stored data
  • Procedure - like function, can have arguments and it can return value
  • Tasks - planned events, like cron
  • Data types - like classes, it can have properties and methods
  • Enums - enumerators

All named things like data, procedures, tasks, data types and enums are defined by developers implementing ThingsDB. New instance of this database only contains empty collection called @:stuff and user account admin. I use this collection as main one for this demo.

Talking to yourself

When you execute query or run procedure on ThingsDB, you have to specify on which collection it will run. That can be sometimes limiting and if you have a need to execute query or run procedure on another collection, there is a way how to achieve that. There is a module called thingsdb (book, GitHub) which allows you to access another collection from collection as specific user. My demo use this feature heavily when dealing with user accounts which is reason I mention it here. I have installed this module as explained in manual.

I'll explain permissions little bit later but fyi: User account I've made for this module has permissions Query, Change, Grant on collection @thingsdb and Change, Grant on collection @:stuff.

User accounts

I chose to use only ThingsDB and that means I had to use their user accounts. I had to deal with registration and login which was little bit tricky because of the absence of backend. Of course I could use some third party authentification server (auth0, etc.), but I didn't want to rely on anything else.

In the case somebody wants to implement 3rd party auth system, you can do HTTP requests from ThingsDB with Request module (book, GitHub).

To allow users to register I needed some user account to communicate with ThingsDB and execute the registration. But required credentials for this account would be published in javascript code which doesn't sound very secure. I didn't want to deal with all security problems but I wanted to implement at least the simple ones. ThingsDB support granting permissions for each user account against each collection specifically. Available permissions to grant are Query, Change, Grant, Join and Run.

  • Query - query ThingsDB
  • Change - allow execution of code which does change of data
  • Grant - allow granting permissions
  • Join - allow join rooms
  • Run - run stored procedure

I'm not able to use Query at all. Because with this command you can execute anything on ThingsDB and opening this to the client browser pose huge security problem. The path was clear, I had to use procedures and just allow Run for client.

Important information to know is the user accounts doesn't have only password but also access tokens (with expiration if needed).

Prepare ThingsDB for registration

I've created collection @:auth and user account with name aa (auth account) and I gave him permission Run over this collection. Collection @:auth contains only one procedure called register. All this means, the user aa can do only one thing which is to run procedure called register. Therefore his access token can be published.

Procedure register does create new account and grant required permissions. The code looks like this:

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;
});

I guess this is your first time seeing code from ThingsDB. It is familiar to another programming languages just with slight changes. What the procedure does:

  • Accepts two arguments
  • Verify arguments email and password
  • Use module thingsdb explained in section Talking to yourself to run query
    • Check if email is already registered
    • Create new user account and set password
    • Grant required permissions

email:, can be a little bit confusing but it's a shorthand when you want to pass variable to argument and argument and variable has the same name.

@t is shortcut for @thingsdb scope.

Frontend implemention of registration

With everything ready at ThingsDB side I've created simple website with registration form and few lines of javascript. The code snippet which manages to run procedure inside of ThingsDB looks like this:

const thingsdb = new ThingsDB();
thingsdb.connect()
  .then(() => thingsdb.authToken(localStorage.getItem('aa')))
  .then(() => thingsdb.run('@:auth', 'register', [
    $('#email').val(), 
    $('#password1').val()
  ]))
  1. It performs authentification with token (for aa user account)
  2. Runs procedure register

I keep access token of user aa in browser localStorage.

To see whole implementation look here:

  • register.html
  • register.js

Login

After user is able to register, next step was to implement login action. For login password is required but it would be not very safe to store user password in browser. The solution is to generate access token (with expiration) after login and return it to client, where it can be stored in browser (ex. sessionStorage). So I've created procedure in @:stuff collection where registered user account has required permissions.

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);
    };
});

Creation of token has to be called on @thingsdb scope, in that case I use thingsdb module again. The javascript code snippet to call this procedure looks like this:

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';
  })

Obtained access token is stored in sessionStorage.

Here you can check whole login page which contains login form and required javascript code:

  • index.html
  • index.js

Overview

After login user is redictered here where he has some account actions and list of his Todos. That required to specify structure, how Todo data will be stored and for this purpose we can use data types. I created Todo type which has name, user_id and items. Item type has description, checked status and Todo reference. Connection between Todo and Item is made with both ways relation (book, docs). Both types are defined in @:stuff collection.

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');

In this piece of code you can see how the types are made, what properties with data types does they have and set up of relation between them.

But this is just definition. We need to store Todos somewhere. For that we create property directly on collection @:stuff like this. Without the dot it would be just variable and it won't be persistent.

.todos = set();

Now after the data structure is ready, let's go through each action.

Todos

Upon loading of overview page, request to load Todos of users to ThingsDB is made. First we need a procedure on @:stuff collection which returns list of Todos:

new_procedure('list_todos', || {
    user_id = user_info().load().user_id;
    .todos.filter(|t| t.user_id == user_id);
});

Filter is function available to call on set.

Now we can call this procedure with javascript code snippet like this (processing received data is omitted):

const thingsdb = new ThingsDB();
thingsdb.connect()
  .then(() => thingsdb.authToken(sessionStorage.getItem('token')))
  .then(() => thingsdb.run('@:stuff', 'list_todos'))
  .then(todos => { })

You can check the whole implementation here:

  • overview.html
  • overview.js

Password change

For this action I've created procedure update_password which requires to use thingsdb module again. User accounts are stored in @thingsdb scope.

new_procedure('update_password', |password| {
    email = user_info().load().name;
    if (is_email(email)) {
        thingsdb.query('@t', 'set_password(email, password);', {
            email:,
            password:,
        });
    };
});

I use html dialog tag to enter a new password and the javascript code snippet to handle it is very simple:

thingsdb.run('@:stuff', 'update_password', [$('#password1').val()])

I don't have to call authToken again because websocket connection is still open from the request to load Todos.

You can check the whole implementation here:

  • overview.html
  • overview.js

Delete account

Procedure for this action removes not only user account but also his Todos. It looks like this:

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:

  • overview.html
  • overview.js

Create Todo

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:

  • new_todo.html
  • new_todo.js

Todo detail

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.

Load Todo data

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

Change Todo item status

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

Delete Todo

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

Installation guide

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.

Let me explain a ThingsDB Todo app demo

No AI was used to generate this content, only the cover picture.

版本聲明 本文轉載於:https://dev.to/stefanak-michal/let-me-explain-a-thingsdb-todo-app-demo-2n9g?1如有侵犯,請聯絡[email protected]刪除
最新教學 更多>
  • 在GO中構造SQL查詢時,如何安全地加入文本和值?
    在GO中構造SQL查詢時,如何安全地加入文本和值?
    在go中構造文本sql查詢時,在go sql queries 中,在使用conting and contement和contement consem per時,尤其是在使用integer per當per當per時,per per per當per. [&​​​​&&&&&&&&&&&&&&&默元組方...
    程式設計 發佈於2025-07-08
  • 版本5.6.5之前,使用current_timestamp與時間戳列的current_timestamp與時間戳列有什麼限制?
    版本5.6.5之前,使用current_timestamp與時間戳列的current_timestamp與時間戳列有什麼限制?
    在時間戳列上使用current_timestamp或MySQL版本中的current_timestamp或在5.6.5 此限制源於遺留實現的關注,這些限制需要對當前的_timestamp功能進行特定的實現。 創建表`foo`( `Productid` int(10)unsigned not ...
    程式設計 發佈於2025-07-08
  • 如何簡化PHP中的JSON解析以獲取多維陣列?
    如何簡化PHP中的JSON解析以獲取多維陣列?
    php 試圖在PHP中解析JSON數據的JSON可能具有挑戰性,尤其是在處理多維數組時。 To simplify the process, it's recommended to parse the JSON as an array rather than an object.To do...
    程式設計 發佈於2025-07-08
  • 為什麼我的CSS背景圖像出現?
    為什麼我的CSS背景圖像出現?
    故障排除:CSS背景圖像未出現 ,您的背景圖像儘管遵循教程說明,但您的背景圖像仍未加載。圖像和样式表位於相同的目錄中,但背景仍然是空白的白色帆布。 而不是不棄用的,您已經使用了CSS樣式: bockent {背景:封閉圖像文件名:背景圖:url(nickcage.jpg); 如果您的html,cs...
    程式設計 發佈於2025-07-08
  • 如何在其容器中為DIV創建平滑的左右CSS動畫?
    如何在其容器中為DIV創建平滑的左右CSS動畫?
    通用CSS動畫,用於左右運動 ,我們將探索創建一個通用的CSS動畫,以向左和右移動DIV,從而到達其容器的邊緣。該動畫可以應用於具有絕對定位的任何div,無論其未知長度如何。 問題:使用左直接導致瞬時消失 更加流暢的解決方案:混合轉換和左 [並實現平穩的,線性的運動,我們介紹了線性的轉換。...
    程式設計 發佈於2025-07-08
  • PHP未來:適應與創新
    PHP未來:適應與創新
    PHP的未來將通過適應新技術趨勢和引入創新特性來實現:1)適應云計算、容器化和微服務架構,支持Docker和Kubernetes;2)引入JIT編譯器和枚舉類型,提升性能和數據處理效率;3)持續優化性能和推廣最佳實踐。 引言在編程世界中,PHP一直是網頁開發的中流砥柱。作為一個從1994年就開始發展...
    程式設計 發佈於2025-07-08
  • 為什麼使用Firefox後退按鈕時JavaScript執行停止?
    為什麼使用Firefox後退按鈕時JavaScript執行停止?
    導航歷史記錄問題:JavaScript使用Firefox Back Back 此行為是由瀏覽器緩存JavaScript資源引起的。要解決此問題並確保在後續頁面訪問中執行腳本,Firefox用戶應設置一個空功能。 警報'); }; alert('inline Alert')...
    程式設計 發佈於2025-07-08
  • 如何使用Python有效地以相反順序讀取大型文件?
    如何使用Python有效地以相反順序讀取大型文件?
    在python 中,如果您使用一個大文件,並且需要從最後一行讀取其內容,則在第一行到第一行,Python的內置功能可能不合適。這是解決此任務的有效解決方案:反向行讀取器生成器 == ord('\ n'): 緩衝區=緩衝區[:-1] ...
    程式設計 發佈於2025-07-08
  • 人臉檢測失敗原因及解決方案:Error -215
    人臉檢測失敗原因及解決方案:Error -215
    錯誤處理:解決“ error:((-215)!empty()in Function Multultiscale中的“ openCV 要解決此問題,必須確保提供給HAAR CASCADE XML文件的路徑有效。在提供的代碼片段中,級聯分類器裝有硬編碼路徑,這可能對您的系統不准確。相反,OPENCV提...
    程式設計 發佈於2025-07-08
  • 表單刷新後如何防止重複提交?
    表單刷新後如何防止重複提交?
    在Web開發中預防重複提交 在表格提交後刷新頁面時,遇到重複提交的問題是常見的。要解決這個問題,請考慮以下方法: 想像一下具有這樣的代碼段,看起來像這樣的代碼段:)){ //數據庫操作... 迴聲“操作完成”; 死(); } ? > ...
    程式設計 發佈於2025-07-08
  • 為什麼不````''{margin:0; }`始終刪除CSS中的最高邊距?
    為什麼不````''{margin:0; }`始終刪除CSS中的最高邊距?
    在CSS 問題:不正確的代碼: 全球範圍將所有餘量重置為零,如提供的代碼所建議的,可能會導致意外的副作用。解決特定的保證金問題是更建議的。 例如,在提供的示例中,將以下代碼添加到CSS中,將解決餘量問題: body H1 { 保證金頂:-40px; } 此方法更精確,避免了由全局保證金重置...
    程式設計 發佈於2025-07-08
  • 如何使用PHP將斑點(圖像)正確插入MySQL?
    如何使用PHP將斑點(圖像)正確插入MySQL?
    essue VALUES('$this->image_id','file_get_contents($tmp_image)')";This code builds a string in PHP, but the function call fil...
    程式設計 發佈於2025-07-08
  • 如何使用Depimal.parse()中的指數表示法中的數字?
    如何使用Depimal.parse()中的指數表示法中的數字?
    在嘗試使用Decimal.parse(“ 1.2345e-02”中的指數符號表示法表示的字符串時,您可能會遇到錯誤。這是因為默認解析方法無法識別指數符號。 成功解析這樣的字符串,您需要明確指定它代表浮點數。您可以使用numbersTyles.Float樣式進行此操作,如下所示:[&& && && ...
    程式設計 發佈於2025-07-08
  • Async Void vs. Async Task在ASP.NET中:為什麼Async Void方法有時會拋出異常?
    Async Void vs. Async Task在ASP.NET中:為什麼Async Void方法有時會拋出異常?
    在ASP.NET async void void async void void void void void void void的設計無需返回asynchroncon而無需返回任務對象。他們在執行過程中增加未償還操作的計數,並在完成後減少。在某些情況下,這種行為可能是有益的,例如未期望或明確...
    程式設計 發佈於2025-07-08

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3