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

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

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

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]刪除
最新教學 更多>
  • CSS可以根據任何屬性值來定位HTML元素嗎?
    CSS可以根據任何屬性值來定位HTML元素嗎?
    靶向html元素,在CSS 中使用任何屬性值,在CSS中,可以基於特定屬性(如下所示)基於特定屬性的基於特定屬性的emants目標元素: 字體家庭:康斯拉斯(Consolas); } 但是,出現一個常見的問題:元素可以根據任何屬性值而定位嗎?本文探討了此主題。 的目標元素有任何任何屬性值,...
    程式設計 發佈於2025-07-07
  • 如何使用Depimal.parse()中的指數表示法中的數字?
    如何使用Depimal.parse()中的指數表示法中的數字?
    在嘗試使用Decimal.parse(“ 1.2345e-02”中的指數符號表示法表示的字符串時,您可能會遇到錯誤。這是因為默認解析方法無法識別指數符號。 成功解析這樣的字符串,您需要明確指定它代表浮點數。您可以使用numbersTyles.Float樣式進行此操作,如下所示:[&& && && ...
    程式設計 發佈於2025-07-07
  • 如何解決AppEngine中“無法猜測文件類型,使用application/octet-stream...”錯誤?
    如何解決AppEngine中“無法猜測文件類型,使用application/octet-stream...”錯誤?
    appEngine靜態文件mime type override ,靜態文件處理程序有時可以覆蓋正確的mime類型,在錯誤消息中導致錯誤消息:“無法猜測mimeType for for file for file for [File]。 application/application/octet...
    程式設計 發佈於2025-07-07
  • PHP陣列鍵值異常:了解07和08的好奇情況
    PHP陣列鍵值異常:了解07和08的好奇情況
    PHP數組鍵值問題,使用07&08 在給定數月的數組中,鍵值07和08呈現令人困惑的行為時,就會出現一個不尋常的問題。運行print_r($月)返回意外結果:鍵“ 07”丟失,而鍵“ 08”分配給了9月的值。 此問題源於PHP對領先零的解釋。當一個數字帶有0(例如07或08)的前綴時,PHP將...
    程式設計 發佈於2025-07-07
  • 人臉檢測失敗原因及解決方案:Error -215
    人臉檢測失敗原因及解決方案:Error -215
    錯誤處理:解決“ error:( - 215)!empty()in Function openCv in Function MultSiscale中的“檢測”中的錯誤:在功能檢測中。”當Face Cascade分類器(即面部檢測至關重要的組件)未正確加載時,通常會出現此錯誤。 要解決此問題,必...
    程式設計 發佈於2025-07-07
  • 如何檢查對像是否具有Python中的特定屬性?
    如何檢查對像是否具有Python中的特定屬性?
    方法來確定對象屬性存在尋求一種方法來驗證對像中特定屬性的存在。考慮以下示例,其中嘗試訪問不確定屬性會引起錯誤: >>> a = someClass() >>> A.property Trackback(最近的最新電話): 文件“ ”,第1行, attributeError:SomeClass實...
    程式設計 發佈於2025-07-07
  • 如何在Java字符串中有效替換多個子字符串?
    如何在Java字符串中有效替換多個子字符串?
    在java 中有效地替換多個substring,需要在需要替換一個字符串中的多個substring的情況下,很容易求助於重複應用字符串的刺激力量。 However, this can be inefficient for large strings or when working with nu...
    程式設計 發佈於2025-07-07
  • MySQL中如何高效地根據兩個條件INSERT或UPDATE行?
    MySQL中如何高效地根據兩個條件INSERT或UPDATE行?
    在兩個條件下插入或更新或更新 solution:的答案在於mysql的插入中...在重複鍵更新語法上。如果不存在匹配行或更新現有行,則此功能強大的功能可以通過插入新行來進行有效的數據操作。如果違反了唯一的密鑰約束。 實現所需的行為,該表必須具有唯一的鍵定義(在這種情況下為'名稱'...
    程式設計 發佈於2025-07-07
  • 為什麼使用Firefox後退按鈕時JavaScript執行停止?
    為什麼使用Firefox後退按鈕時JavaScript執行停止?
    導航歷史記錄問題:JavaScript使用Firefox Back Back 此行為是由瀏覽器緩存JavaScript資源引起的。要解決此問題並確保在後續頁面訪問中執行腳本,Firefox用戶應設置一個空功能。 警報'); }; alert('inline Alert')...
    程式設計 發佈於2025-07-07
  • 如何在Chrome中居中選擇框文本?
    如何在Chrome中居中選擇框文本?
    選擇框的文本對齊:局部chrome-inly-ly-ly-lyly solument 您可能希望將文本中心集中在選擇框中,以獲取優化的原因或提高可訪問性。但是,在CSS中的選擇元素中手動添加一個文本 - 對屬性可能無法正常工作。 初始嘗試 state)</option> < o...
    程式設計 發佈於2025-07-07
  • Java為何無法創建泛型數組?
    Java為何無法創建泛型數組?
    通用陣列創建錯誤 arrayList [2]; JAVA報告了“通用數組創建”錯誤。為什麼不允許這樣做? 答案:Create an Auxiliary Class:public static ArrayList<myObject>[] a = new ArrayList<my...
    程式設計 發佈於2025-07-07
  • 如何使用FormData()處理多個文件上傳?
    如何使用FormData()處理多個文件上傳?
    )處理多個文件輸入時,通常需要處理多個文件上傳時,通常是必要的。 The fd.append("fileToUpload[]", files[x]); method can be used for this purpose, allowing you to send multi...
    程式設計 發佈於2025-07-07
  • 解決MySQL插入Emoji時出現的\\"字符串值錯誤\\"異常
    解決MySQL插入Emoji時出現的\\"字符串值錯誤\\"異常
    Resolving Incorrect String Value Exception When Inserting EmojiWhen attempting to insert a string containing emoji characters into a MySQL database us...
    程式設計 發佈於2025-07-07
  • Go web應用何時關閉數據庫連接?
    Go web應用何時關閉數據庫連接?
    在GO Web Applications中管理數據庫連接很少,考慮以下簡化的web應用程序代碼:出現的問題:何時應在DB連接上調用Close()方法? ,該特定方案將自動關閉程序時,該程序將在EXITS EXITS EXITS出現時自動關閉。但是,其他考慮因素可能保證手動處理。 選項1:隱式關閉終...
    程式設計 發佈於2025-07-07
  • 如何在Java中正確顯示“ DD/MM/YYYY HH:MM:SS.SS”格式的當前日期和時間?
    如何在Java中正確顯示“ DD/MM/YYYY HH:MM:SS.SS”格式的當前日期和時間?
    如何在“ dd/mm/yyyy hh:mm:mm:ss.ss”格式“ gormat 解決方案: args)拋出異常{ 日曆cal = calendar.getInstance(); SimpleDateFormat SDF =新的SimpleDateFormat(“...
    程式設計 發佈於2025-07-07

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

Copyright© 2022 湘ICP备2022001581号-3