dg.run

Connexion et base de données

Votre application est constituée de fichiers statiques, mais elle peut quand même avoir des utilisateurs et des données enregistrées. La plateforme fournit deux services sur l’adresse de votre application, sous /_dg/ :

  • Se connecter avec dg.run : les utilisateurs se connectent (ou créent un compte) sur dg.run et autorisent votre application. Vous ne gérez jamais de mots de passe ni de codes par e-mail.
  • Une petite base NoSQL : des documents JSON regroupés en collections, privés pour chaque utilisateur ou partagés avec tout le monde.

Il n’y a ni clés ni réglages. La plateforme sait quelle application appelle grâce à l’adresse sur laquelle elle tourne.

Installation

npm install @dgrun/sdk
import { createDg } from '@dgrun/sdk'

const dg = createDg()
await dg.ready // termine une connexion si l’utilisateur vient de revenir de dg.run

if (!dg.user) {
  button.onclick = () => dg.signIn()
} else {
  console.log(dg.user.email)
}

signIn() envoie le navigateur sur dg.run. L’utilisateur se connecte, voit ce que votre application demande, appuie sur Autoriser et revient sur votre application, connecté. S’il a déjà autorisé votre application, il revient directement sans qu’on le lui demande de nouveau.

dg.user vaut { id, email }. L’id est stable pour cet utilisateur dans votre application et différent dans chaque autre application.

Stocker des données

const todos = dg.db.collection('todos') // privé à chaque utilisateur

const doc = await todos.put({ title: 'Acheter du lait', done: false }) // nouveau document, clé aléatoire
await todos.put({ title: 'Œufs', done: false }, 'oeufs')               // votre clé : créer ou remplacer
await todos.patch('oeufs', { done: true })                             // changer quelques champs
const { items, hasMore } = await todos.list({
  where: [{ field: 'done', value: false }],
  orderBy: 'createdAt',
  desc: true,
  limit: 20,
})
await todos.get('oeufs')
await todos.delete('oeufs')

Chaque document revient sous la forme { key, data, owner, createdAt, updatedAt }.

Collections privées et publiques

collection('x') (par défaut) collection('x', 'public')
Qui peut lire seulement l’utilisateur qui l’a créé tous ceux qui utilisent votre application, connectés ou non
Qui peut créer les utilisateurs connectés les utilisateurs connectés
Qui peut modifier ou supprimer le propriétaire le propriétaire uniquement
Même clé pour deux utilisateurs deux documents distincts un seul document, possédé par celui qui l’a créé en premier

Utilisez des collections publiques pour un tableau partagé ou un classement, par exemple. list({ mine: true }) ne renvoie que les documents publics de l’utilisateur connecté, et owner sur chaque document vous permet d’afficher qui l’a écrit.

Ces règles sont appliquées par la plateforme, pas par votre code, si bien qu’un utilisateur ne peut pas les contourner avec les outils de développement du navigateur. Pour la même raison, il n’y a pas de rôle « admin » : si vous avez besoin de données que vous seul pouvez modifier, cela ne rentre pas encore dans ce cadre.

Requêtes

where accepte jusqu’à 5 conditions sur les champs de premier niveau de vos données : eq (par défaut), ne, gt, gte, lt, lte et in (avec une liste de valeurs). Les valeurs sont des chaînes, des nombres, des booléens ou null. orderBy vaut createdAt, updatedAt ou un champ de premier niveau. Utilisez limit (jusqu’à 100) et skip pour paginer.

Rôles et permissions

Donnez aux utilisateurs des étiquettes de permission (comme editor, admin ou billing.read) et indiquez, collection par collection, quelles étiquettes peuvent lire, créer, modifier et supprimer. Le serveur de dg.run applique chaque règle à chaque requête, donc rien ne dépend du code de votre application, et un utilisateur ne peut pas la contourner en modifiant votre application.

Configurez cela dans Mes applications → Données et utilisateurs → Accès :

  • Permissions par défaut : les étiquettes que chaque utilisateur reçoit, sauf si vous définissez les siennes individuellement. Sous Utilisateurs, vous pouvez donner à une personne sa propre liste (une liste vide signifie aucune étiquette) ; « utiliser la valeur par défaut de l’application » l’y ramène. Modifier une valeur par défaut touche tous ceux qui la suivent.
  • Règles de collection : pour chaque collection, une liste de bénéficiaires par opération : * (tout le monde, même déconnecté), user (un utilisateur connecté qui porte l’étiquette user : tout le monde l’a par défaut, mais vous pouvez la retirer des étiquettes par défaut de l’application ou de celles d’une personne ; sans elle, la personne reste connectée et garde ses propres enregistrements mais ne correspond à aucune règle qui accorde user), @owner (le créateur de cet enregistrement) ou n’importe quelle étiquette. Les mêmes listes s’appliquent à chaque enregistrement. Une collection sans règle se comporte comme avant (les collections privées appartiennent à chaque utilisateur ; les publiques sont lisibles par tous, créées par tout utilisateur connecté et modifiées uniquement par leur créateur).
  • Champs protégés : nommez les champs que seuls certains bénéficiaires peuvent définir ou modifier, dans chaque enregistrement, sans toucher à chaque enregistrement. Par exemple amount: admin permet à toute personne autorisée à modifier une transaction de changer sa note, mais seul un admin peut changer le montant. Une écriture qui touche un champ protégé sans permission échoue en entier avec field_forbidden ; renvoyer la même valeur ne pose pas de problème.
  • Permissions par enregistrement (facultatif) : activez-les et chaque enregistrement peut porter son propre acl indiquant qui peut le lire, le modifier ou le supprimer : une étiquette, *, user, ou une personne (user:<id>, l’id que vous voyez comme doc.owner).

Quelqu’un est autorisé à écrire un enregistrement quand au moins une couche dit oui (la règle de la collection, ou l’acl de l’enregistrement) ; les champs protégés restreignent ensuite les champs qu’il peut modifier. Exemples :

// Articles : tout le monde lit, les éditeurs écrivent, seuls les admins suppriment. Réglé une fois dans le tableau de bord :
//   read: * · create: editor · update: editor · delete: admin
const articles = dg.db.collection('articles', 'public')
await articles.put({ title: 'Bonjour' }, 'bonjour')    // 'forbidden' sauf si l’utilisateur est éditeur

// Tickets de support : privés pour leur auteur, mais le personnel avec l’étiquette `support` les voit et répond à tous.
//   read: @owner, support · update: @owner, support · delete: @owner
const tickets = dg.db.collection('tickets')
await tickets.list()                                   // support : les tickets de tous ; les autres : seulement les leurs
await tickets.list({ mine: true })                     // seulement les miens
await tickets.patch('t1', { answer: 'Terminé' }, { owner: ticket.owner })   // le ticket de quelqu’un d’autre, si les règles l’autorisent

// Transactions : les utilisateurs modifient leurs propres notes, mais seul un admin peut définir ou changer le montant.
//   read: @owner, admin · update: @owner, admin · fields: amount -> admin
const tx = dg.db.collection('transactions')
await tx.patch('t1', { note: 'déjeuner' })             // accepté pour le créateur
await tx.patch('t1', { amount: 1 })                    // DgError 'field_forbidden' (error.data.field === 'amount') sauf pour un admin

// Partager un enregistrement : avec une personne, une étiquette, tout le monde (collections avec permissions par enregistrement).
await docs.put({ text: '…' }, 'plan', { acl: { read: [`user:${friend.owner}`, 'reviewer'], update: ['reviewer'] } })

Une personne non autorisée reçoit forbidden (connectée) ou unauthorized (déconnectée). Un enregistrement que vous n’avez pas le droit de lire ressemble exactement à un enregistrement qui n’existe pas. Celui qui modifie un enregistrement ne s’en empare jamais : son créateur reste le propriétaire.

Afficher les bons écrans, même hors ligne

dg.user.permissions liste les étiquettes de l’utilisateur, et await dg.can('editor') / await dg.permissions() les lit depuis un jeton signé vérifié sur l’appareil avec la clé publique du serveur : ils fonctionnent donc sans réseau et ne peuvent pas être modifiés sur l’appareil. Les étiquettes sont signées dans les jetons de connexion à côté de l’id propre à l’application de l’utilisateur (dg.user.id).

Cela sert uniquement à afficher ou masquer l’interface. Le serveur ne fait jamais confiance au jeton : il lit les étiquettes de l’utilisateur dans sa base à chaque requête, si bien qu’une étiquette retirée cesse de fonctionner immédiatement. Sur un appareil qui reste hors ligne, la copie signée continue d’afficher les anciennes étiquettes jusqu’à son expiration (15 jours au plus) ou jusqu’à la prochaine connexion de l’appareil.

Limites

Taille d’un document 16 Ko de JSON
Documents par utilisateur, par application 1 000
Documents par application 10 000
Taille d’une requête 64 Ko
Imbrication 8 niveaux ; les noms de champs ne peuvent pas commencer par $ ni contenir .
Noms collections : lettres minuscules, chiffres, - et _ (40 au plus) ; clés : lettres, chiffres, . _ : - (128 au plus)

Quand une limite est atteinte, l’appel échoue avec quota_exceeded ou doc_too_large.

Erreurs

Chaque appel en échec lève une DgError avec un code : unauthorized (connectez-vous d’abord, ou la session a pris fin), forbidden (vos permissions ne l’autorisent pas), not_found, invalid_data, invalid_query, doc_too_large, quota_exceeded, app_not_found, et pour les rôles acl_disabled / invalid_acl.

Si l’utilisateur déconnecte votre application ou est bloqué, son appel suivant échoue avec unauthorized et le SDK le déconnecte localement.

TypeScript

Le SDK fournit ses types. Décrivez ce que vous stockez et chaque appel est vérifié :

interface Note { text: string; done: boolean }
const notes = dg.db.collection<Note>('notes')

await notes.put({ text: 'Acheter du lait', done: false })
await notes.list({ where: [{ field: 'done', value: false }], orderBy: 'updatedAt' }) // les noms de champs sont vérifiés par rapport à Note
await notes.put({ text: 1, done: false }) // erreur de compilation

Les interfaces et les alias type fonctionnent tous les deux. Une collection sans argument de type accepte n’importe quel objet JSON. Les appels en échec lèvent une DgError dont le code est typé, si bien que e.code === 'quota_exceeded' s’autocomplète.

Voir et gérer les données et les utilisateurs de votre application

Tout ce que votre application stocke, et toutes les personnes qui l’ont autorisée, se trouvent au même endroit : Mes applications → Données et utilisateurs (ouvrez-le depuis la fiche de votre application). Vous seul, le propriétaire de l’application, pouvez l’ouvrir. Un administrateur de dg.run peut aussi l’ouvrir pour aider lors d’un cas de support, et ce qu’un administrateur ouvre, modifie ou supprime y est consigné dans une piste d’audit.

La page ne montre que l’application en ligne. Les aperçus administrateur utilisent des données séparées qui n’apparaissent jamais ici. Tant que votre application n’est pas en ligne et que personne ne l’a autorisée, la page le dit. L’en-tête compte les utilisateurs connectés, les documents et les abonnés actifs, et il a trois onglets : Données, Utilisateurs et Accès.

Données

Chaque collection que votre application utilise, avec son type (privée ou publique) et son nombre de documents, collections privées comprises. Choisissez une collection pour filtrer la liste, ou Tous les documents. Chaque ligne montre la clé du document, sa collection, son propriétaire (son adresse e-mail), la date de sa dernière modification et sa taille. Les listes affichent 25 documents par page, la modification la plus récente d’abord.

  • Modifier ouvre le JSON du document. L’enregistrement suit les mêmes règles de format que le SDK : un objet JSON, 16 Ko au plus, aucun nom de champ commençant par $ ni contenant .. Dans une collection avec permissions par enregistrement, vous pouvez aussi modifier les listes acl du document (qui peut le lire, le modifier et le supprimer).
  • Supprimer retire le document définitivement (il n’y a pas d’annulation).
  • Vos modifications et suppressions atteignent immédiatement les pages qui surveillent la collection (voir Temps réel).
  • Vous êtes le propriétaire, donc les modifications faites ici ne sont bloquées ni par les règles de la collection ni par les champs protégés. Seules les limites de format et de taille s’appliquent.

Utilisateurs

Toutes les personnes qui ont autorisé votre application, la plus récente d’abord. Pour chacune, vous voyez :

Colonne Ce que c’est
E-mail L’adresse qu’elle a partagée avec votre application en l’autorisant. Un badge bloqué signifie que dg.run a bloqué le compte : ses appels échouent.
ID dans l’application Son dg.user.id, et l’owner de chaque document qu’elle crée. Il est différent dans chaque autre application, ce qui vous permet de faire le lien entre ce que vous voyez ici et ce que voit votre code.
Connecté Quand elle a autorisé votre application pour la première fois.
Documents Combien de documents elle possède dans votre application.
Offre, temps restant, renouvellement auto Ce qu’elle détient si vous vendez des offres : le nom de l’offre, les jours restants (ou À vie), et si elle se renouvelle. Ouvrir la liste renouvelle ou termine ce qui est dû, donc elle n’est jamais périmée.
Permissions Ses étiquettes de permission, ou « valeur par défaut de l’application ».

Pour chaque personne, vous pouvez :

  • Voir les données : lister uniquement ses documents (et les modifier ou les supprimer comme n’importe quel autre).
  • Modifier les permissions : lui donner sa propre liste d’étiquettes, ou la ramener aux étiquettes par défaut de l’application. Cela l’atteint à sa prochaine requête.
  • Déconnecter l’utilisateur : met fin à sa session immédiatement et efface tout ce qu’il a stocké dans votre application. Son appel suivant échoue avec unauthorized et le SDK le déconnecte sur son appareil. Il peut se reconnecter plus tard, en repartant de zéro. Cette action est irréversible.

Accès

Les étiquettes de permission par défaut et les règles de chaque collection : qui peut lire, créer, modifier et supprimer, les champs protégés et les permissions par enregistrement. C’est décrit dans Rôles et permissions, et une modification prend effet à la requête suivante.

Associer une personne à ses données

Un utilisateur signale un problème et vous donne son adresse e-mail. Ouvrez Utilisateurs, trouvez-le et appuyez sur Voir les données pour voir exactement ce qu’il voit. Dans votre propre code, la même personne est dg.user.id :

// La valeur de « ID dans l’application », et l’`owner` de chacun de ses documents :
const mine = (await notes.list({ limit: 100 })).items // ses propres documents privés
console.log(dg.user.id === mine[0]?.owner)            // true

// Collections publiques : distinguer les documents de l’auteur des autres
const guestbook = dg.db.collection<{ text: string }>('guestbook', 'public')
const { items } = await guestbook.list({ orderBy: 'createdAt', desc: true })
const own = items.filter((d) => d.owner === dg.user?.id)

Pour permettre à quelqu’un de modérer (supprimer les messages des autres, par exemple), donnez-lui une étiquette comme moderator sous Utilisateurs, ajoutez-la à la règle Supprimer de la collection sous Accès, et affichez le bouton uniquement aux personnes qui la portent :

const moderator = !!dg.user?.permissions.includes('moderator') // décide ce qu’on AFFICHE ; le serveur vérifie encore chaque appel
await guestbook.delete(doc.key, { owner: doc.owner })          // le message de quelqu’un d’autre ; 'forbidden' sauf si les règles l’autorisent

Bon à savoir

  • L’écran de consentement indique aux utilisateurs que vous pouvez voir ce que votre application stocke pour eux, et que votre application reçoit leur adresse e-mail. Ne stockez que ce qui est nécessaire.
  • Il n’y a pas encore d’export ni d’accès par clé d’API : tout se fait dans le tableau de bord.
  • La page ne demande aucun code dans votre application et rien à activer.

À savoir

  • La connexion dure 30 jours sur un appareil et se renouvelle tant que l’utilisateur continue d’utiliser l’application. Le SDK garde un jeton d’accès de 15 minutes en mémoire uniquement ; la partie durable est un cookie httpOnly que votre code ne peut pas lire. Faites await dg.ready avant de lire dg.user au chargement de la page. Après 30 jours d’inactivité, signIn() reconnecte l’utilisateur en une touche. signOut() met aussi fin à la session sur le serveur.
  • Temps réel. collection.live et collection.subscribe poussent les changements vers votre page ; sans connexion, live reliste toutes les 15 secondes. Voir Temps réel.
  • Hors ligne. Les appels échouent sans connexion. Si votre application fonctionne hors ligne (voir installation et mode hors ligne), gardez une copie locale de ce dont l’utilisateur a besoin et ne mettez rien sous /_dg/ en cache dans votre service worker.
  • _dg/ est réservé. Un paquet avec un dossier _dg à la racine est refusé.
  • Les aperçus administrateur utilisent leurs propres données, séparées, donc examiner une version ne touche jamais les données de vos utilisateurs en ligne.
  • Confidentialité. Votre application reçoit l’adresse e-mail de l’utilisateur et rien d’autre sur son compte. En tant que développeur, vous pouvez voir, modifier et supprimer tout ce que votre application stocke, données privées comprises : ne stockez que ce qui est nécessaire et dites à vos utilisateurs ce que vous gardez. Les utilisateurs peuvent déconnecter une application et effacer les données la concernant.

Vous voulez faire payer votre application ? Voir Offres et abonnements.

Temps réel

collection.subscribe et collection.live préviennent votre page quand des documents changent, sans interroger le serveur en boucle. Seuls les changements que l’utilisateur connecté a le droit de lire sont envoyés (les mêmes règles que get et list).

const notes = dg.db.collection<Note>('notes', 'public')

// Garder une liste à l’écran : appelée maintenant, après chaque changement et après une reconnexion.
const stop = notes.live({ orderBy: 'createdAt', desc: true, limit: 20 }, (page) => render(page.items))

// Ou réagir soi-même à chaque changement.
const off = notes.subscribe((e) => {
  if (e.type === 'put') upsert(e.doc)            // créé ou modifié
  else if (e.type === 'delete') remove(e.key)    // supprimé
  else list()                                    // 'reset' : reconnecté, vous avez peut-être manqué quelque chose
})

La connexion s’ouvre à votre premier abonnement et se ferme peu après l’arrêt du dernier. Les connexions sont renouvelées environ toutes les cinq minutes ; le SDK se reconnecte tout seul et envoie reset pour que vous puissiez relister. Si une connexion ne peut pas être établie, live revient à lister toutes les 15 secondes, et dg.realtime.status ('open' | 'connecting' | 'closed' | 'off', avec dg.realtime.onStatus(fn)) vous indique dans quel mode vous êtes. Au plus 20 collections peuvent être surveillées à la fois. Passez realtime: false à createDg pour désactiver.

Exemples

Des applications complètes que vous pouvez construire, compresser et téléverser (chaque dossier a un README avec les étapes, et un .zip prêt à l’emploi) :

  • basic-example : des îlots React sur des pages HTML simples en anglais et en français. Un livre d’or public et des notes privées, des types TypeScript, collection.live avec un badge Live, et un rôle moderator qui peut supprimer le message de n’importe qui (configuré dans Données et utilisateurs → Accès / Utilisateurs).
  • pro-notes : une offre gratuite, un essai gratuit, des offres mensuelles et annuelles et un achat unique, avec dg.billing.subscribe() et entitlement() (voir Offres et abonnements).
  • market-pwa : un compteur avec deux pages différentes sur une même adresse, pour la fiche de la marketplace et pour l’application installée.
  • todo-pwa : une petite application installable en Vite + React avec un service worker, comme point de départ pour l’installation et le mode hors ligne (sans connexion ni base de données).

basic-example et pro-notes importent @dgrun/sdk. Dans votre propre projet, lancez npm install @dgrun/sdk et regroupez-le avec Vite, esbuild ou n’importe quel bundler.