Vor einiger Zeit habe ich eine Datenbank namens ThingsDB entdeckt. Ich war neugierig und habe etwas gelesen. Ich habe herausgefunden, dass sie TCP-Konnektivität unterstützen, aber für einige bestimmte Plattformen keinen Treiber hatten, deshalb habe ich einen Treiber dafür für Javascript und PHP entwickelt.
Als ich an einem Javascript-Treiber arbeitete, wurde mir klar, dass es möglich wäre, ThingsDB direkt vom Frontend aus zu verwenden, ohne Backend oder Middleware. Sie können eine WebSocket-Verbindung (TCP) über den Browser öffnen. Deshalb habe ich mich an die Autoren von ThingsDB gewandt und sie haben Unterstützung für WebSocket hinzugefügt (verfügbar ab ThingsDB-Version 1.6). Auf diese Weise kann mein Javascript-Treiber vom Frontend (Browser) und auch vom Javascript-basierten Backend (z. B. node.js) verwendet werden. Ich habe hier einen Artikel über meinen PHP-Treiber geschrieben, in dem ich interessantes Feedback erhalten habe. Die Leute wollten mehr vom Potenzial von ThingsDB sehen. Aus diesem Grund habe ich beschlossen, keinen Artikel über meinen Javascript-Treiber zu schreiben, sobald ich ihn fertiggestellt habe, sondern habe beschlossen, dass es am besten wäre, eine Demo zu erstellen.
Um die Grundlagen von ThingsDB und dieser Demo zu verstehen, empfehle ich Ihnen, sie kontinuierlich zu lesen, während ich dabei bestimmte Funktionen erkläre. Ich gehe davon aus, dass Sie mit der Programmierung im Allgemeinen vertraut sind, zumindest in den Grundlagen. Und vielleicht etwas Javascript und jQuery.
Wenn Sie diesem Artikel folgen und Codeschnipsel in ThingsDB ausführen möchten, müssen Sie die im Installationshandbuch erwähnte angehängte Docker-Datei verwenden.
Das Wichtigste zuerst. Lassen Sie mich den Aufbau kurz erläutern.
ThingsDB enthält Sammlungen. Die Sammlung enthält Daten, Prozeduren, Aufgaben, Datentypen und Aufzählungen. Es gibt auch die vorherige Sammlung (Bereich) @thingsdb, die Benutzerzugriffskonten enthält und auch Prozeduren und Aufgaben enthalten kann. Schließlich gibt es noch den @node-Bereich, der derzeit nicht wichtig ist.
Alle benannten Dinge wie Daten, Prozeduren, Aufgaben, Datentypen und Aufzählungen werden von Entwicklern definiert, die ThingsDB implementieren. Die neue Instanz dieser Datenbank enthält nur eine leere Sammlung namens @:stuff und den Benutzerkontoadministrator. Ich verwende diese Sammlung als Hauptsammlung für diese Demo.
Wenn Sie eine Abfrage oder eine Prozedur in ThingsDB ausführen, müssen Sie angeben, für welche Sammlung sie ausgeführt werden soll. Das kann manchmal einschränkend sein, und wenn Sie eine Abfrage oder eine Prozedur für eine andere Sammlung ausführen müssen, gibt es eine Möglichkeit, dies zu erreichen. Es gibt ein Modul namens „thingsdb“ (book, GitHub), mit dem Sie als bestimmter Benutzer auf eine andere Sammlung aus der Sammlung zugreifen können. Meine Demo verwendet diese Funktion häufig beim Umgang mit Benutzerkonten, weshalb ich sie hier erwähne. Ich habe dieses Modul wie im Handbuch beschrieben installiert.
Berechtigungen erkläre ich etwas später, aber zu Ihrer Information: Das Benutzerkonto, das ich für dieses Modul erstellt habe, verfügt über die Berechtigungen Abfragen, Ändern, Gewähren bei Sammlung @thingsdb und Ändern, Gewähren bei Sammlung @:stuff.
Ich habe mich dafür entschieden, nur ThingsDB zu verwenden, und das bedeutet, dass ich deren Benutzerkonten verwenden musste. Ich musste mich um die Registrierung und Anmeldung kümmern, was aufgrund des fehlenden Backends etwas schwierig war. Natürlich könnte ich einen Authentifizierungsserver eines Drittanbieters (auth0 usw.) verwenden, aber ich wollte mich nicht auf etwas anderes verlassen.
Falls jemand ein Authentifizierungssystem eines Drittanbieters implementieren möchte, können Sie HTTP-Anfragen von ThingsDB mit dem Request-Modul (Buch, GitHub) stellen.
Um Benutzern die Registrierung zu ermöglichen, benötigte ich ein Benutzerkonto, um mit ThingsDB zu kommunizieren und die Registrierung durchzuführen. Die erforderlichen Anmeldeinformationen für dieses Konto würden jedoch im Javascript-Code veröffentlicht, was nicht sehr sicher klingt. Ich wollte mich nicht mit allen Sicherheitsproblemen befassen, aber ich wollte zumindest die einfachen implementieren. ThingsDB unterstützt die Erteilung von Berechtigungen für jedes Benutzerkonto speziell für jede Sammlung. Verfügbare Berechtigungen zum Gewähren sind „Abfragen“, „Ändern“, „Gewähren“, „Beitreten“ und „Ausführen“.
Ich kann Abfrage überhaupt nicht verwenden. Denn mit diesem Befehl können Sie alles auf ThingsDB ausführen und das Öffnen im Client-Browser stellt ein großes Sicherheitsproblem dar. Der Weg war klar, ich musste Prozeduren verwenden und einfach Ausführen für den Client zulassen.
Wichtige Information ist, dass die Benutzerkonten nicht nur über ein Passwort, sondern auch über Zugriffstokens verfügen (mit Ablauf, falls erforderlich).
Ich habe eine Sammlung @:auth und ein Benutzerkonto mit dem Namen aa (Authentifizierungskonto) erstellt und ihm die Berechtigung Ausführen für diese Sammlung erteilt. Die Sammlung @:auth enthält nur eine Prozedur namens register. Das alles bedeutet, dass der Benutzer aa nur eines tun kann, nämlich die Prozedur namens register auszuführen. Daher kann sein Zugriffstoken veröffentlicht werden.
Das Verfahrensregister erstellt ein neues Konto und gewährt die erforderlichen Berechtigungen. Der Code sieht so aus:
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; });
Ich schätze, das ist das erste Mal, dass Sie Code von ThingsDB sehen. Es ist mit geringfügigen Änderungen aus anderen Programmiersprachen bekannt. Was das Verfahren bewirkt:
E-Mail:, kann etwas verwirrend sein, ist aber eine Abkürzung, wenn Sie eine Variable an ein Argument übergeben möchten und das Argument und die Variable denselben Namen haben.
@t ist die Abkürzung für den @thingsdb-Bereich.
Da alles auf der ThingsDB-Seite fertig ist, habe ich eine einfache Website mit Registrierungsformular und ein paar Zeilen Javascript erstellt. Der Codeausschnitt, der es schafft, die Prozedur innerhalb von ThingsDB auszuführen, sieht so aus:
const thingsdb = new ThingsDB(); thingsdb.connect() .then(() => thingsdb.authToken(localStorage.getItem('aa'))) .then(() => thingsdb.run('@:auth', 'register', [ $('#email').val(), $('#password1').val() ]))
Ich behalte das Zugriffstoken des Benutzers aa im lokalen Speicher des Browsers.
Um die gesamte Implementierung zu sehen, schauen Sie hier:
Nachdem sich der Benutzer registrieren konnte, bestand der nächste Schritt darin, die Anmeldeaktion zu implementieren. Für die Anmeldung ist ein Passwort erforderlich, es wäre jedoch nicht sehr sicher, das Benutzerpasswort im Browser zu speichern. Die Lösung besteht darin, nach der Anmeldung ein Zugriffstoken (mit Ablauf) zu generieren und es an den Client zurückzugeben, wo es im Browser (z. B. sessionStorage) gespeichert werden kann. Deshalb habe ich in der @:stuff-Sammlung eine Prozedur erstellt, in der das registrierte Benutzerkonto über die erforderlichen Berechtigungen verfügt.
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); }; });
Die Erstellung des Tokens muss im @thingsdb-Bereich aufgerufen werden. In diesem Fall verwende ich erneut das Modul „thingsdb“. Der Javascript-Codeausschnitt zum Aufrufen dieser Prozedur sieht folgendermaßen aus:
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'; })
Das erhaltene Zugriffstoken wird im sessionStorage gespeichert.
Hier können Sie die gesamte Anmeldeseite überprüfen, die das Anmeldeformular und den erforderlichen Javascript-Code enthält:
Nach der Anmeldung wird der Benutzer hierher weitergeleitet, wo er einige Kontoaktionen und eine Liste seiner Todos hat. Dies erfordert die Angabe der Struktur und der Art und Weise, wie Todo-Daten gespeichert werden. Zu diesem Zweck können wir Datentypen verwenden. Ich habe den Typ Todo erstellt, der Name, Benutzer-ID und Elemente enthält. Der Typ Artikel verfügt über eine Beschreibung, einen geprüften Status und eine Todo-Referenz. Die Verbindung zwischen Todo und Item erfolgt über eine beidseitige Beziehung (Buch, Dokumente). Beide Typen sind in der @:stuff-Sammlung definiert.
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 diesem Codeabschnitt können Sie sehen, wie die Typen erstellt werden, welche Eigenschaften mit Datentypen sie haben und wie die Beziehung zwischen ihnen aufgebaut wird.
Aber das ist nur eine Definition. Wir müssen Todos irgendwo aufbewahren. Dafür erstellen wir eine Eigenschaft wie diese direkt in der Sammlung @:stuff. Ohne den Punkt wäre es nur variabel und nicht dauerhaft.
.todos = set();
Sobald die Datenstruktur fertig ist, gehen wir die einzelnen Aktionen durch.
Beim Laden der Übersichtsseite wird eine Anfrage zum Laden der Todos von Benutzern in ThingsDB gestellt. Zuerst benötigen wir eine Prozedur für die @:stuff-Sammlung, die eine Liste von Todos:
zurückgibt
new_procedure('list_todos', || { user_id = user_info().load().user_id; .todos.filter(|t| t.user_id == user_id); });
Filter ist eine Funktion, die am Set aufgerufen werden kann.
Jetzt können wir diese Prozedur mit einem Javascript-Code-Snippet wie diesem aufrufen (die Verarbeitung empfangener Daten entfällt):
const thingsdb = new ThingsDB(); thingsdb.connect() .then(() => thingsdb.authToken(sessionStorage.getItem('token'))) .then(() => thingsdb.run('@:stuff', 'list_todos')) .then(todos => { })
Sie können die gesamte Implementierung hier überprüfen:
Für diese Aktion habe ich die Prozedur „update_password“ erstellt, die die erneute Verwendung des Moduls „thingsdb“ erfordert. Benutzerkonten werden im @thingsdb-Bereich gespeichert.
new_procedure('update_password', |password| { email = user_info().load().name; if (is_email(email)) { thingsdb.query('@t', 'set_password(email, password);', { email:, password:, }); }; });
Ich verwende das HTML-Dialog-Tag, um ein neues Passwort einzugeben, und das Javascript-Code-Snippet zur Handhabung ist sehr einfach:
thingsdb.run('@:stuff', 'update_password', [$('#password1').val()])
Ich muss authToken nicht erneut aufrufen, da die Websocket-Verbindung seit der Anforderung zum Laden von Todos noch offen ist.
Sie können die gesamte Implementierung hier überprüfen:
Das Verfahren für diese Aktion entfernt nicht nur das Benutzerkonto, sondern auch seine Todos. Es sieht so aus:
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.
Das obige ist der detaillierte Inhalt vonLassen Sie mich eine Demo der ThingsDB Todo-App erklären. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!