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

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

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

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]刪除
最新教學 更多>
  • React:了解 React 的事件系統
    React:了解 React 的事件系統
    Overview of React's Event System What is a Synthetic Event? Synthetic events are an event-handling mechanism designed by React to ach...
    程式設計 發佈於2024-11-05
  • 為什麼在使用 Multipart/Form-Data POST 請求時會收到 301 Moved Permanently 錯誤?
    為什麼在使用 Multipart/Form-Data POST 請求時會收到 301 Moved Permanently 錯誤?
    Multipart/Form-Data POSTsMultipart/Form-Data POSTs嘗試使用multipart/form-data POST 資料時,可能會出現類似所提供的錯誤訊息遭遇。理解問題需要檢視問題的構成。遇到的錯誤是 301 Moved Permanently 回應,表示資...
    程式設計 發佈於2024-11-05
  • 如何使用日期和時間物件來確定 PHP 中的時間邊界?
    如何使用日期和時間物件來確定 PHP 中的時間邊界?
    確定PHP 中的時間邊界在此編程場景中,我們的任務是確定給定時間是否在預先定義的範圍內。具體來說,我們得到三個時間字串:當前時間、日出和日落。我們的目標是確定當前時間是否位於日出和日落的邊界時間之間。 為了應對這個挑戰,我們將使用 DateTime 類別。這個類別使我們能夠表示和操作日期和時間。我們...
    程式設計 發佈於2024-11-05
  • 如何使用 CSS 變換比例修復 jQuery 拖曳/調整大小問題?
    如何使用 CSS 變換比例修復 jQuery 拖曳/調整大小問題?
    jQuery 使用CSS 轉換縮放拖曳/調整大小問題: 當應用CSS 轉換時,特別是變換:矩陣(0.5, 0, 0, 0.5, 0, 0);,對於一個div 並在子元素上使用jQuery 的draggable() 和resizing() 插件,jQuery 所做的更改變得與滑鼠位置「不同步”。 解決...
    程式設計 發佈於2024-11-05
  • 如何修復 TensorFlow 中的「ValueError:無法將 NumPy 陣列轉換為張量(不支援的物件類型浮點)」錯誤?
    如何修復 TensorFlow 中的「ValueError:無法將 NumPy 陣列轉換為張量(不支援的物件類型浮點)」錯誤?
    TensorFlow:解決「ValueError: Failed to Convert NumPy Array to Tensor (Unsupported Object Type Float)」工作時遇到的常見錯誤TensorFlow 的錯誤是「ValueError:無法將NumPy 陣列轉換為T...
    程式設計 發佈於2024-11-05
  • 如何有效率判斷本機儲存項目是否存在?
    如何有效率判斷本機儲存項目是否存在?
    確定本地儲存專案是否存在使用 Web 儲存時,在存取或修改特定專案之前驗證它們是否存在至關重要。在本例中,我們想要確定 localStorage 中是否設定了特定項目。 當前方法檢查項目是否存在的當前方法似乎是:if (!(localStorage.getItem("infiniteScr...
    程式設計 發佈於2024-11-05
  • Java 中的原子是什麼?了解 Java 中的原子性和線程安全
    Java 中的原子是什麼?了解 Java 中的原子性和線程安全
    1. Java 原子簡介 1.1 Java 中什麼是原子? 在Java中,java.util.concurrent.atomic套件提供了一組支援對單一變數進行無鎖定線程安全程式設計的類別。這些類別統稱為原子變數。最常使用的原子類別包括 AtomicInteger ...
    程式設計 發佈於2024-11-05
  • 前端/後端主要設定檔
    前端/後端主要設定檔
    從 DevOps 的角度來看,了解 Java 和 Node.js(後端和前端)程式碼庫中的設定檔對於管理建置流程、部署和環境設定至關重要。以下是在 Java 和 Node.js 應用程式中需要注意的設定檔的完整清單: Java 應用程式 後端 pom.xml (Maven): 管理依...
    程式設計 發佈於2024-11-05
  • Python 中出現「意外縮排」錯誤的原因以及如何解決?
    Python 中出現「意外縮排」錯誤的原因以及如何解決?
    Python 中意外縮排的意義是什麼? 在 Python 程式設計領域,精心製作的縮排起著至關重要的作用定義程式碼的結構和流程。當這個縮排不經意地被打亂時,就會出現「unexpected indent」錯誤,提示需要立即修正。 錯誤訊息背後:Unexpected Indent本質Python 的語法...
    程式設計 發佈於2024-11-05
  • 在 Node.js 中什麼時候應該使用 `setImmediate` 和 `process.nextTick`?
    在 Node.js 中什麼時候應該使用 `setImmediate` 和 `process.nextTick`?
    了解setImmediate 和nextTick 之間的差異了解setImmediate 和nextTick 之間的差異Node.js 版本0.10 引入了setImmediate,這是一個旨在補充process.nextjs 版本的新API。這兩個函數都提供了非同步執行回呼的方法,但它們具有控制其...
    程式設計 發佈於2024-11-05
  • jQuery中如何有效率地取得隱藏元素的高度?
    jQuery中如何有效率地取得隱藏元素的高度?
    在 jQuery 中獲取隱藏元素的高度處理隱藏元素時,檢索其高度可能具有挑戰性。暫時顯示元素以測量其高度然後再次隱藏它的傳統方法似乎效率低下。有沒有更優化的解決方案? jQuery 1.4.2 方法這是一個使用 jQuery 1.4.2 的範例:$select.show(); optionHeigh...
    程式設計 發佈於2024-11-05
  • 為什麼我不能在 Go Struct 標籤中使用變數?
    為什麼我不能在 Go Struct 標籤中使用變數?
    在Go 結構體標籤中使用變數在Go 中,結構體標籤用於指定有關結構體中字段的元數據。雖然可以使用字串文字定義標籤,但嘗試在其位置使用變數會導致錯誤。 無效用法:const ( TYPE = "type" ) type Shape struct { Type str...
    程式設計 發佈於2024-11-05
  • Qopy:身為開發人員我最喜歡的剪貼簿管理器
    Qopy:身為開發人員我最喜歡的剪貼簿管理器
    身為開發人員,我一直在尋找可以讓我的工作流程更順暢、更有效率的工具。最近,我偶然發現了 Qopy,一個可以在 Linux 和 Windows 上運行的開源剪貼簿管理器。 什麼是Qopy? Qopy 是一個簡單的剪貼簿管理器,旨在改善標準剪貼簿體驗。它的設計宗旨是用戶友好、可靠且快速...
    程式設計 發佈於2024-11-05
  • 為什麼我的按鈕上的懸停效果不起作用?
    為什麼我的按鈕上的懸停效果不起作用?
    更改懸停時的按鈕顏色:替代解決方案嘗試更改懸停時按鈕的顏色時,如果出現以下情況,可能會令人沮喪該解決方案未能產生預期的效果。考慮提供的範例程式碼:a.button { ... } a.button a:hover{ background: #383; }此解決方案嘗試在連結懸停在「按...
    程式設計 發佈於2024-11-05
  • 僅使用 Python 建構前端
    僅使用 Python 建構前端
    對於專注於後端的開發人員來說,前端開發可能是一項艱鉅的、甚至是噩夢般的任務。在我職業生涯的早期,前端和後端之間的界線是模糊的,每個人都被期望能夠處理這兩者。 CSS,尤其是,是一場持續不斷的鬥爭;這感覺像是一個不可能的任務。 雖然我喜歡前端工作,但 CSS 對我來說仍然是一個複雜的挑戰,特別是因為...
    程式設計 發佈於2024-11-05

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

Copyright© 2022 湘ICP备2022001581号-3