前陣子我發現了一個名為 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中文網其他相關文章!