Il y a quelque temps, j'ai découvert une base de données appelée ThingsDB. J'étais curieux à ce sujet et j'ai fait quelques lectures. J'ai découvert qu'ils prennent en charge la connectivité TCP mais qu'ils n'avaient pas de pilote pour certaines plates-formes spécifiques, j'ai donc développé un pilote pour javascript et pour php.
Quand j'ai travaillé sur le pilote javascript, j'ai réalisé qu'il serait possible d'utiliser ThingsDB directement depuis le frontend sans aucun backend ni middleware. Vous pouvez ouvrir une connexion Websocket (TCP) à partir du navigateur. J'ai donc contacté les auteurs de ThingsDB et ils ont ajouté la prise en charge de Websocket (disponible à partir de la version 1.6 de ThingsDB). De cette façon, mon pilote javascript peut être utilisé depuis le frontend (navigateur) ainsi que depuis le backend basé sur javascript (ex. node.js). J'ai écrit ici un article sur mon pilote php dans lequel j'ai reçu des commentaires intéressants. Les gens voulaient voir davantage le potentiel de ThingsDB. Sur cette base, j'ai choisi de ne pas écrire d'article sur mon pilote javascript juste après l'avoir terminé, mais j'ai décidé qu'il serait préférable de faire une démo.
Pour comprendre les bases de ThingsDB et de cette démo, je vous suggère de la lire continuellement pendant que j'explique les fonctionnalités spécifiques en cours de route. Je suppose que vous connaissez la programmation en général, au moins les bases. Et peut-être du javascript et du jQuery.
Si vous souhaitez suivre cet article en exécutant des extraits de code dans ThingsDB, vous devez utiliser le fichier docker joint mentionné dans le guide d'installation.
Tout d’abord. Permettez-moi d'expliquer brièvement la structure.
ThingsDB contient des collections. La collection contient des données, des procédures, des tâches, des types de données et des énumérations. Il existe également une collection préalable (portée) @thingsdb qui contient des comptes d'accès utilisateur et peut également contenir des procédures et des tâches. Enfin, il y a la portée @node qui n'est pas importante pour le moment.
Tous les éléments nommés tels que les données, les procédures, les tâches, les types de données et les énumérations sont définis par les développeurs implémentant ThingsDB. La nouvelle instance de cette base de données contient uniquement une collection vide appelée @:stuff et l'administrateur du compte utilisateur. J'utilise cette collection comme collection principale pour cette démo.
Lorsque vous exécutez une requête ou exécutez une procédure sur ThingsDB, vous devez spécifier sur quelle collection elle s'exécutera. Cela peut parfois être limitant et si vous avez besoin d'exécuter une requête ou d'exécuter une procédure sur une autre collection, il existe un moyen d'y parvenir. Il existe un module appelé Thingsdb (book, GitHub) qui vous permet d'accéder à une autre collection depuis la collection en tant qu'utilisateur spécifique. Ma démo utilise largement cette fonctionnalité lorsqu'elle traite des comptes d'utilisateurs, c'est pourquoi je la mentionne ici. J'ai installé ce module comme expliqué dans le manuel.
J'expliquerai les autorisations un peu plus tard, mais pour info : le compte utilisateur que j'ai créé pour ce module a les autorisations Requête, Modification, Accorder sur la collection @thingsdb et Modification, Accorder sur la collection @:stuff.
J'ai choisi d'utiliser uniquement ThingsDB et cela signifie que j'ai dû utiliser leurs comptes d'utilisateurs. J'ai dû m'occuper de l'inscription et de la connexion, ce qui était un peu délicat en raison de l'absence de backend. Bien sûr, je pouvais utiliser un serveur d'authentification tiers (auth0, etc.), mais je ne voulais compter sur rien d'autre.
Dans le cas où quelqu'un souhaite implémenter un système d'authentification tiers, vous pouvez effectuer des requêtes HTTP depuis ThingsDB avec le module Request (livre, GitHub).
Pour permettre aux utilisateurs de s'inscrire, j'avais besoin d'un compte utilisateur pour communiquer avec ThingsDB et exécuter l'enregistrement. Mais les informations d'identification requises pour ce compte seraient publiées dans du code javascript, ce qui ne semble pas très sécurisé. Je ne voulais pas traiter tous les problèmes de sécurité mais je voulais implémenter au moins les plus simples. ThingsDB prend en charge l'octroi d'autorisations pour chaque compte d'utilisateur sur chaque collection spécifiquement. Les autorisations disponibles à accorder sont Requête, Modification, Accorder, Rejoindre et Exécuter.
Je ne parviens pas du tout à utiliser Query. Parce qu'avec cette commande, vous pouvez exécuter n'importe quoi sur ThingsDB et son ouverture sur le navigateur client pose un énorme problème de sécurité. Le chemin était clair, je devais utiliser des procédures et simplement autoriser Exécuter pour le client.
Une information importante à connaître est que les comptes utilisateurs n'ont pas seulement un mot de passe mais aussi des jetons d'accès (avec expiration si nécessaire).
J'ai créé une collection @:auth et un compte utilisateur avec le nom aa (compte auth) et je lui ai donné la permission Exécuter sur cette collection. La collection @:auth ne contient qu'une seule procédure appelée registre. Tout cela signifie que l'utilisateur aa ne peut faire qu'une seule chose : exécuter une procédure appelée registre. Son jeton d'accès peut donc être publié.
Le registre de procédure crée un nouveau compte et accorde les autorisations requises. Le code ressemble à ceci :
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; });
Je suppose que c'est la première fois que vous voyez du code de ThingsDB. Il est familier à d'autres langages de programmation avec de légères modifications. À quoi sert la procédure :
email :, peut être un peu déroutant mais c'est un raccourci lorsque vous voulez passer une variable à un argument et que l'argument et la variable ont le même nom.
@t est un raccourci pour la portée @thingsdb.
Avec tout prêt du côté de ThingsDB, j'ai créé un site Web simple avec un formulaire d'inscription et quelques lignes de javascript. L'extrait de code qui parvient à exécuter la procédure dans ThingsDB ressemble à ceci :
const thingsdb = new ThingsDB(); thingsdb.connect() .then(() => thingsdb.authToken(localStorage.getItem('aa'))) .then(() => thingsdb.run('@:auth', 'register', [ $('#email').val(), $('#password1').val() ]))
Je conserve le jeton d'accès de l'utilisateur aa dans le navigateur localStorage.
Pour voir l'intégralité de la mise en œuvre, regardez ici :
Une fois que l'utilisateur a pu s'inscrire, l'étape suivante consistait à mettre en œuvre l'action de connexion. Pour la connexion, un mot de passe est requis, mais il ne serait pas très sûr de stocker le mot de passe utilisateur dans le navigateur. La solution consiste à générer un jeton d'accès (avec expiration) après la connexion et à le renvoyer au client, où il peut être stocké dans le navigateur (ex. sessionStorage). J'ai donc créé une procédure dans la collection @:stuff où le compte utilisateur enregistré dispose des autorisations requises.
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); }; });
La création du jeton doit être appelée sur le scope @thingsdb, dans ce cas j'utilise à nouveau le module Thingsdb. L'extrait de code javascript pour appeler cette procédure ressemble à ceci :
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'; })
Le jeton d'accès obtenu est stocké dans sessionStorage.
Ici, vous pouvez consulter l'intégralité de la page de connexion qui contient le formulaire de connexion et le code javascript requis :
Après la connexion, l'utilisateur est réédité ici où il a quelques actions de compte et la liste de ses Todos. Cela nécessitait de spécifier la structure, la manière dont les données Todo seront stockées et à cette fin, nous pouvons utiliser des types de données. J'ai créé le type Todo qui a un nom, un user_id et des éléments. Le type Article a une description, un statut vérifié et une référence Todo. La connexion entre Todo et Item se fait dans les deux sens (livre, docs). Les deux types sont définis dans la collection @: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');
Dans ce morceau de code, vous pouvez voir comment les types sont créés, quelles propriétés ont-ils avec les types de données et établir la relation entre eux.
Mais ce n’est qu’une définition. Nous devons stocker Todos quelque part. Pour cela, nous créons une propriété directement sur la collection @: des trucs comme celui-ci. Sans le point, ce serait simplement variable et ce ne serait pas persistant.
.todos = set();
Maintenant, une fois la structure de données prête, passons en revue chaque action.
Lors du chargement de la page de présentation, une demande de chargement des Todos des utilisateurs dans ThingsDB est effectuée. Nous avons d’abord besoin d’une procédure sur la collection @:stuff qui renvoie la liste des Todos :
new_procedure('list_todos', || { user_id = user_info().load().user_id; .todos.filter(|t| t.user_id == user_id); });
Le filtre est une fonction disponible pour appeler sur le plateau.
Nous pouvons maintenant appeler cette procédure avec un extrait de code javascript comme ceci (le traitement des données reçues est omis) :
const thingsdb = new ThingsDB(); thingsdb.connect() .then(() => thingsdb.authToken(sessionStorage.getItem('token'))) .then(() => thingsdb.run('@:stuff', 'list_todos')) .then(todos => { })
Vous pouvez vérifier l'intégralité de la mise en œuvre ici :
Pour cette action, j'ai créé la procédure update_password qui nécessite d'utiliser à nouveau le module Thingsdb. Les comptes d'utilisateurs sont stockés dans la portée @thingsdb.
new_procedure('update_password', |password| { email = user_info().load().name; if (is_email(email)) { thingsdb.query('@t', 'set_password(email, password);', { email:, password:, }); }; });
J'utilise la balise de dialogue html pour saisir un nouveau mot de passe et l'extrait de code javascript pour le gérer est très simple :
thingsdb.run('@:stuff', 'update_password', [$('#password1').val()])
Je n'ai pas besoin d'appeler à nouveau authToken car la connexion websocket est toujours ouverte depuis la demande de chargement de Todos.
Vous pouvez vérifier l'intégralité de la mise en œuvre ici :
La procédure pour cette action supprime non seulement le compte utilisateur mais également ses Todos. Cela ressemble à ceci :
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.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!