ホームページ >ウェブフロントエンド >jsチュートリアル >ThingsDB Todo アプリのデモについて説明します
少し前に、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 (book, GitHub) というモジュールがあり、これを使用すると、特定のユーザーとしてコレクションから別のコレクションにアクセスできます。私のデモでは、ユーザー アカウントを処理するときにこの機能を頻繁に使用しています。それが、ここで言及する理由です。マニュアルの説明に従ってこのモジュールをインストールしました。
権限については後ほど説明しますが、参考までに: このモジュール用に作成したユーザー アカウントには、コレクション @thingsdb に対するクエリ、変更、付与とコレクション @:stuff に対する変更、付与の権限があります。
私は ThingsDB のみを使用することを選択しました。つまり、ThingsDB のユーザー アカウントを使用する必要がありました。バックエンドがないため、登録とログインに対処する必要があり、少し面倒でした。もちろん、サードパーティの認証サーバー (auth0 など) を使用することもできましたが、他のものには依存したくありませんでした。
サードパーティ認証システムを実装したい場合は、Request モジュール (書籍、GitHub) を使用して ThingsDB から HTTP リクエストを実行できます。
ユーザーが登録できるようにするには、ThingsDB と通信して登録を実行するためのユーザー アカウントが必要でした。ただし、このアカウントに必要な認証情報は JavaScript コードで公開されるため、あまり安全とは言えません。すべてのセキュリティ問題に対処したくはありませんでしたが、少なくとも単純なものは実装したかったのです。 ThingsDB は、各コレクションに対する各ユーザー アカウントへのアクセス許可の付与をサポートしています。付与できる権限は、Query、Change、Grant、Join、Run です。
クエリ がまったく使用できません。このコマンドを使用すると、ThingsDB 上で何でも実行でき、これをクライアント ブラウザで開くと、セキュリティ上大きな問題が発生するためです。パスは明確だったので、プロシージャを使用し、クライアントに対して 実行 を許可するだけでした。
知っておくべき重要な情報は、ユーザー アカウントにはパスワードだけでなく、アクセス トークン (必要に応じて有効期限あり) もあるということです。
コレクション @:auth と aa (認証アカウント) という名前のユーザー アカウントを作成し、このコレクションに対して 実行 する許可を彼に与えました。コレクション @:auth には、register というプロシージャが 1 つだけ含まれています。つまり、ユーザー 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 のコードを見るのはこれが初めてだと思います。わずかな変更を加えるだけで、他のプログラミング言語でもよく使用されます。この手順の内容:
email: は少し混乱するかもしれませんが、変数を引数と引数に渡し、変数が同じ名前である場合の省略表現です。
@t は @thingsdb スコープのショートカットです。
ThingsDB 側ですべての準備が整ったので、登録フォームと数行の JavaScript を備えたシンプルな Web サイトを作成しました。 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 データの保存方法を指定するために必要であり、この目的のためにデータ型を使用できます。名前、user_id、項目を持つ Todo タイプを作成しました。 Item タイプには、説明、チェックされたステータス、および Todo 参照が含まれます。 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');
このコード部分では、型がどのように作成されるか、データ型を持つどのようなプロパティがあり、それらの間の関係が設定されているかを確認できます。
しかし、これは単なる定義です。 Todo をどこかに保存する必要があります。そのために、次のようにコレクション @:stuff にプロパティを直接作成します。ドットがないと単なる変数となり、永続的ではなくなります。
.todos = set();
データ構造の準備ができたら、各アクションを見てみましょう。
概要ページの読み込み時に、ユーザーのTodosをThingsDBに読み込むリクエストが行われます。まず、Todo のリストを返す @:stuff コレクションのプロシージャが必要です:
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 => { })
ここで実装全体を確認できます:
このアクションのために、 thingsdb モジュールを再度使用する必要があるプロシージャ update_password を作成しました。ユーザー アカウントは @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()])
Todo をロードするリクエストから WebSocket 接続がまだ開いているため、authToken を再度呼び出す必要はありません。
ここで実装全体を確認できます:
このアクションの手順では、ユーザー アカウントだけでなく、Todo も削除されます。次のようになります:
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 中国語 Web サイトの他の関連記事を参照してください。