Static site generators

Dans la longue liste des choses que je voulais regarder depuis longtemps : Jekyll et autres sites permettant de générer des sites statiques. Ca tombe bien, j’ai un sujet (prochaine journée). On va commencer par faire un choix.

Besoin

Pour le coup, mon besoin est assez « simple » : je veux pouvoir générer un site sur la base de fichier (JSON, CSVn Markdown, …) contenant les données.

Première sélection

Alors … autant que des générateurs, il y en a plein mais alors … plein … faire un choix va encore être très très compliqué … Pour aider voici deux premiers liens :

Ensuite, comme d’habitude, le résultat combiné de la recherche sous Qwant et Google de : « static site generator 2019 » qui donne :

Sur la base de tout cela et comme nous sommes en plein « Concours de l’eurovision », j’ai fait une liste en partant du principe suivant : 5 points au premier de la liste etc…

Cela donne :

  1. Jekyll (34 points),
  2. Gatsby (29 points),
  3. Hugo (28 points),
  4. NextJs (24 points),
  5. Hexo (12 points).

Alors c’est moche mais de base, je supprime NextJs pour la seule raison qu’il est (semble-t-il) basé sur React et comme c’est le cas de Gatsby, je ne vais pas en prendre deux dans la liste. C’est moche je sais.

En plus, j’en profite pour ajouter un autre : Stasis. Pourquoi ? D’après StaticGen c’est le seul qui ressort avec le filtre « Typescript ».

Deuxième sélection

Pour chacun des sélectionnés, je vais « simplement » parcourir la documentation pour me faire une idée. Le but est en d’en lister deux max si possible pour la suite des actions :).

Jekyll

Le step by step tutorial donne envie tellement cela paraît simple. Le langage de template (Liquid) est très similaire à ce que j’utilise.

Par contre, le côté ruby me gêne … Je n’ai jamais été très tenté par ce langage (c’est purement du ressenti …) et j’ai déjà une version de ruby sur ma machine, donc je vais devoir passer par de la conf …

Après dans les 30 minutes accordées, je n’ai pas pu voir le rendu en termes d’output … ni le déploiement…

Gastby.js

Ce serait le framework qui monte dans le domaine. Déjà premier point, via npx j’ai pu crée un site en quelque secondes et donc regarder un peu ce qu’il y a dedans …

Les premiers exemples montrent qu’il utilise React que je ne connais pas mais est-ce que cela pourrait être l’occasion un premier test ?

La difficulté que j’ai identifiée est dans le déploiement. En fait, le site généré n’est pas 100% static car l’ouvrir directement un navigateur ne fonctionne pas. Bon après, il y a peut-être une solution mais dans les 30 minutes : pas trouvé.

UPDATE : J’ai trouvé cet échange qui semble indiqué que ce n’est pas forcément le cas : https://github.com/gatsbyjs/gatsby/issues/4610.

J’aurais peut-être du prendre Next.Js …

Hugo

Au suivant … Bon, il à l’air bien celui-là ! Son seul défaut: il est basé sur Go mais les premiers pas semblent montrer que c’est principalement la partie template dans laquelle il faut jouer.

Forcément, j’ai cherché et pour le coup, il semble que l’élément généré soit vraiment un site static de base que l’on peut déposer n’importe où et çà: c’est bien !

Hexo

Le sous-titre est assez explicite : « A fast, simple & powerful blog framework ». Même si tous les autres mettent en avant le côté blog, ici c’est carrément dans le titre. De plus, il y a tout un chapitre qui est dédié à comment migrer et dans la liste, il y a les différents moteurs de blog les plus populaires.

Après, je pense qu’il doit être possible de l’utiliser pour faire autre chose. Il ne semble pas lié à framework particulier et semble compatible avec différents types de template dont « Nunjuncks » que j’avais choisi un peu par erreur mais que bon … je connais :).

Stasis

Bon … ca à l’air bien mais c’est en alpha de chez alpha !

Bilan

Alors c’est pas facile mais j’exclus Gatsby pour le fait que finalement il ne génère pas un site statique. De même Jekyll pour le coté Ruby.

Reste Hugo / Hexo …

Hugo semble être là pour générer des sites statiques et c’est finalement le template qui fait le taf.

Hexo c’est pareil au final (le template gère) mais semble être fait pour du blog … Après cela reste du javascript et en terme de langage de template.

Choix

Je vais pas le faire … Je vais faire les tutos de bases de deux pour décider :).

Hexo

Quelques notes …

  • Lien : https://hexo.io/docs/
  • Commandes générales :
    • npx hexo init ga-hexo # Génération du projet,
    • hexo server # Lancement du serveur local,
    • hexo server -l # Avec des logs
    • hexo server --draft # Idem mais en mode draft
    • hexo generate --watch --bail # Permet d'avoir une génération en continue et la remontée des erreurs
  • Génération de contenu :
    • hexo new [name] # Genère le contenu par defaut (_config.yml.default_layout)
    • hexo new [content] [name] # Idem mais pour un autre type de content (post, draft, page ...)
    • hexo publish [name] # pour publier le brouillon
  • Petites choses :
    • Sans le bail, le générateur ne retourne pas les erreurs,
    • Un changement de thème n’est pas pris en compte par le watch,
    • Il faut parfois relancer le serveur pour que les modifications soient pris en compte.

Retour

Le sous titre était clair : c’est un générateur de blog. Honnêtement, entre les thèmes et les datas, je pense qu’il est possible d’en faire autres choses mais l’effort serait conséquent et instable.

Il y a très longtemps, j’avais fait un template WordPress et quand je vois la page des variables et des helpers, j’ai un gros sentiment de déjà vu.

Un point qui me pose question : la gestion des fichiers « content » avec le site devenant « gros » … Je me dis que cela ne doit pas être évident …

Hugo

Liens

  • Hugo – Mike Dane,
  • https://www.youtube.com/watch?v=NSts93C9UeE
  • https://lord.re/posts/82-hugo-tutorial/
  • A Step-by-Step Guide: Victor-Hugo on Netlify,
  • Hugo Tutorial: How to Build & Host a (Very Fast) Static E-Commerce Site,
  • A Step-by-Step Guide: Victor-Hugo on Netlify

Quelques notes

Retour

L’outil est puissant et dispose de nombreuses fonctionnalités et donc méritent plus de temps. La génération est rapide et les concepts très similaire à ceux vus sur Hexo.

Par contre, la syntaxe de template est pas ultra évidente ou innée … Quelques exemples :

  • Un if : if and ($var1 eq $var2) ($var1 eq $var3),
  • Un filtre sur une liste : range where (where .Site.Pages ".IsNode" true) ".Type" "==" "categories"
  • La syntaxe du passage d’infos entre template ou shortcode …

Après, d’après la documentation, il s’agit de librairies issues du monde Go qui est le langage de Hugo :).

Bilan Final

Entre les deux derniers, mon choix va vers Hugo car les templates présents/disponibles semblent beaucoup plus ouverts en terme de type de sites déployés.

Maintenant ce choix n’est pas définitif. Je vais répondre à mon besoin immédiat via Hugo mais pour le prochain, je referais sans doute une sélection.

Mais ça … c’est pour un autre demain.

Documenter mon API

Introduction

Quand on est un intermittent du développement, il y a quelque chose de super compliqué : se souvenir ce que l’on a fait la dernière fois et que chaque modification ne casse pas tout …

Pour le moment, je palliais aux soucis en :

  • Committant souvent : me permet de revenir voir ce que j’ai fait,
  • Mettant en place un maximum de tests unitaires : ce qui peut-être frustrant quand on prototype mais bon.

Il reste un dernier souci : euh … l’API elle attend quoi déjà ?

Bref … faut documenter. Mais bon tant qu’à faire j’aimerais bien ne pas y passer trop de temps car bon : c’est quand même du dev perso / jetable !

Swagger

Dans les différents projets sur lesquels j’interviens, la documentation des API Rest est réalisé via Swagger ou un équivalent (Nelmio en php/symfony).

Ici, je ne vais repartir des outils proposés comme l’éditeur, la génération etc car … le mal est fait : l’API existe …

Alors autant dire que des solutions, il en existe plusieurs … et pour certaines, il existe même différentes combinaisons. Donc, j’ai fait des choix …

Swagger-jsdoc / Swagger-ui-express

Présentation

  • swagger-jsdoc  : permet d’utiliser la syntaxe JSDoc pour générer la documentation au format swagger,
  • swagger-ui-express : permet d’ajouter une route qui va exposer la documentation sous la forme d’une interface Swagger-ui. Ici dédié à Express.

Mise en place

Je vous passe les npm install habituel pour la mise en place directe.

Il faut commencer par mettre un peu de doc sur le service. L’idée est bien d’utiliser la documentation faîte sur les méthodes des contrôleurs directement :

/**
     * @swagger
     * /api/zones:
     *    get:
     *      description: Return all the zone
     */
    public getAll = async (req: IAppRequest, res: Response) => {
       const appResponse: AppResponse<any> = await ZoneService.getAll();
       return res.status(appResponse.code).json(appResponse);
    } // getAll

Ensuite, j’ai crée un fichier ~/src/config.doc.ts dont le but est de centraliser la mise en place de la documentation. Le fichier contient la configuration de la génération de la documentation par swagger-jsoc :

    private static options = {
        swaggerDefinition: {
          info: {
            title: "The Beer Project - API",
            version: "1.0.0",
            description: "API Rest for the Beer Project",
          },
        },
        // Uniquement les fichiers controllers
        apis: ["src/controllers/*"],
      };

    private static specs = swaggerJSDoc(ConfigDoc.options);

Puis, une fonction qui enregistre la route au niveau de l’application :

/**
     * Ajoute la route de la documentation
     * @param app
     */
    public static addDocRoute(app: any) {
        app.use("/api/docs", swagger.serve, swagger.setup(ConfigDoc.specs));
    }

Cette fonction utilise swagger-ui-express pour servir la route (swagger.serve) et passe les éléments générés par swagger-jsdoc en paramètre.

Test

Un petit npm start puis direction http://localhost:3000/api/doc et :

C’est déjà pas mal. Test ? Là au début, j’ai été un peu déçu car je n’ai eu aucune réponse … Le souci ne venait pas du code en tant que tel mais de la documentation. Pour que l’interface, gère une réponse, il faut lui fournir l’info :

/**
     * @swagger
     * /api/zones:
     *    get:
     *      description: Return all the zone
     *      responses:
     *        200:
     *         description: OK
     *         schema:
     *           type: object
     */
    public getAll = async (req: IAppRequest, res: Response) => {
       const appResponse: AppResponse<any> = await ZoneService.getAll();
       return res.status(appResponse.code).json(appResponse);
    } // getAll

Et là :

Bilan

Cà marche mais il faut doubler l’information et tout documenter manuellement …

TSOA

Présentation

En naviguant à droite et à gauche, j’ai fini par tomber sur ce projet : TSOA. Le principe me plaît car il semble qu’il soit possible de faire d’une pierre deux coups :

  • Des décorateurs qui définissent l’action,
  • Les décorateurs servent également à la documentation.

Et j’aime bien ce genre de principe.

Mise en place

Alors la documentation bien que longue ne contient pas de [Get Started] donc j’ai un galéré à savoir comment mettre en place tout le système et je ne suis même pas sûr d’avoir tout bon …

Tout commence par un npm install tsoa puis il faut créer le fichier tsoa.json qui va permettre de définir deux choses :

  • L’emplacement des contrôleurs : routes,
  • L’emplacement de la documentation générée : swagger.
{
    "swagger": {
        "outputDirectory": "./src/swagger",
        "entryFile": "./src/app.ts"
    },
    "routes": {
        "entryFile": "./src/app.ts",
        "routesDir": "./src/routes"
    }
}

Le projet va aller lire les sources et pour cela, il lui faut un point d’entrée « entryFile » qui peut-être votre fichier contenant la description de votre application.

Une fois cela fait, il faut aller modifier le contrôleur pour ajouter des décorateurs. Dans mon cas, j’ai ajouté @Route et @Get :

/**
 * Controller pour zone controller
 */
@Route("/tsoa/v1/zones")
export class TSOAZoneController {

    @Get()
    public async getZones(): Promise<IZone[]> {
        const appResponse: AppResponse<any> = await ZoneService.getAll();
        return new Promise<IZone[]>((resolve, reject) => {
            if (appResponse.success) {
                resolve(appResponse.data);
            } else {
                reject(appResponse.message);
            }
        });
    }

}

Premier petit truc : pour éviter des erreurs, il faut ajouter une entrée dans le fichier tsconfig.json :

 "experimentalDecorators": true

Sinon, les décorateurs ne passent pas.

Comme pour les autres contrôleurs, il faut l’ajouter dans le fichier d’entrée (app.ts dans mon cas):

// Oui le nom est pour test :o) 
import { TSOAZoneController } from "./controllers/api-tsoa.zone.controller";

Maintenant, il faut lancer une commande qui va générer un fichier contenant toutes les routes. Et oui, rien de magique ! C’est du code généré !

$> tsoa routes
$> tsoa swagger

La première va générer un fichier routes.ts dans le répertoire pointé par routeDir et le deuxième va générer un fichier swagger.json dans le répertoire paramétré plus haut.

Premier souci

Alors, un premier souci lors du lancement de la commande. Désolé, je n’ai pas la capture mais en gros: il n’arrivait à déterminer les types de mon objet. En fait, dans mon cas, ma classe hérité de Mongoose.Document et çà : il n’aime pas. Il faut des objets tout simple. J’ai fait les modifications et hop çà marche !

Plus d’informations ici.

Nouvelle génération

J’ai bien mon fichier de route et mon fichier swagger.json.

Pour le fichier de route, il faut l’enregistrer dans mon fichier app.ts (normal). Dans l’ordre :

  • Il faut importer le fichier de route généré juste avant : import { RegisterRoutes } from "./routes/routes";
  • Via l’export RegisterRoutes, on le laisse enregistrer ses routes : RegisterRoutes(this.app as express.Express);

La documentation indique qu’il est intéressant d’utiliser le package methodOverride pour ajouter des verbes supplémentaires. Enfin …. elle le dit pas mais c’est dans les exemples de code.

Test

On lance et miracle : ça marche.

Documentation

Pour la documentation, il y a encore un peu de travail mais pas tant que cela par rapport à ce que l’on a fait avant. En fait, il faut modifier la commande qui récupère la documentation pour qu’elle aille directement lire le fichier swagger.json généré :

/**
     * Ajoute la route de la documentation
     * @param app
     */
    public static addDocRoute(app: any) {
        // Version JSODC
        app.use("/api/jsDocs", swagger.serve, swagger.setup(ConfigDoc.specs));
        // Version TSOA
        app.use("/swagger.json", express.static(__dirname + "/swagger/swagger.json"));
        app.use("/api/tsoa", swagger.serve, swagger.setup(swaggerJSON));
    }

Et bim, ça marche. On profite même de la récupération des documentations réalisées directement sur les entités :

Générer à la volée

Alors par contre souci : il faut que cela rentre dans mon build de dev car sinon, je serais jamais à jour. Pour cela, deux nouvelles entrées dans le package json :

"watch-tsoa": "onchange 'src/**/*tsoa*.ts' -- npm run tsoa",
"tsoa": "tsoa routes && tsoa swagger"

La première appelle la deuxième si quelques choses dans mes contrôleurs et l’autre appelle les commandes TSOA.

Bilan

C’est pas tout mal … J’aime bien le principe qu’une information serve deux fois …

Mais il reste pas mal de choses à valider :

  • Gestion des POST, PUT & Co …
  • Et surtout la sécurité !!!

Bilan de la journée

Clairement la première solution est pratique pour un projet dans lequel tout est déjà en place. On vient vraiment documenter. Donc je vais la privilégier dans ce cas.

Par contre, j’ai une nouvelle API à faire (enfin … disons que j’ai envie) et je vais essayer de pousser TSOA.

Mais ça … c’est pour un autre demain.

Installer et packager des électrons (partie 2)

Retour !

Lors de ma dernière tentative, j’étais resté sur ma faim car je n’arrivais pas à builder le setup Windows sous Linux. De plus, je n’avais pas géré les mises à jour qui sont normalement une fonctionnalité de base d’Electron… Donc retour !

Remonter le fil !

LaunchFox

En me baladant sur les internets, j’étais tombé sur ce site LaunchFox. Sans creuser d’avantages, je m’étais noté de regarder car le projet toolkit + la possibilité de publication me tentait bien. Bon après quelques lectures du site, je me suis rendu compte que ce n’était pas encore en production … Par contre, cela m’a permis de regarder electron-toolkit.

Electron-toolkit

Il s’agit d’une application Electron dont le but semble d’aider dans la partie build ce qui est une bonne idée. Malheureusement après quelques tests, je me suis rendu compte que l’application était limitée pour le moment :

  • Pas de logs en cas d’erreur,
  • Conserve bien les paramètres dans le package.json mais ne s’en ressert pas …
    • Par contre, la documentation indique que le projet utilise electron-builder qui semble être un concurrent de electron-packager et que j’avais déjà croisé au moment de ma première tentative.

      Je n’avais pas creusé car je trouvais la documentation pas claire et complexe comparée à electron-packager …

      AutoUpdate

      Documentation officielle

      Après cette première piste, je suis revenu à l’auto-update en partant de la documentation officielle. Pour le coup, la documentation est assez clair. Plusieurs solutions sont proposées mais elles ont toutes un point commun (sauf erreur de ma part) : il faut un serveur applicatif ou un compte GitHub.

      Dans mon cas, la deuxième n’est pas envisageable et la deuxième m’embête un peu … et c’est là que suis tombé sur ce projet electron-simple-updater. La promesse : il suffit d’un fichier placé sur un espace pour que les mises à jour fonctionnent. Et cela m’arrangerait bien.

      Et devinez avec qui il est en relation : electron-builder ! Bref, faut que j’y passe !

      Electron-Builder

      Installation

      Comme d’habitude : npm install electron-builder --save-dev

      Configuration

      Après quelques tâtonnements, je suis parvenu à cette configuration :

      
          "build": {
              "appId": "[APP_ID]"
              , "copyright": "Moi@2018
              , "productName": "productName"
              , "asar": true
              ,"appImage": {
                  "systemIntegration": "doNotAsk"
              },
              "files": [
                  "!publisher.json",
                  "!README.md",
                  "!updates.json",
                  "!release-builds/**/*"
              ],
              "linux": {
                  "category": "Utils"
              },
              "win": {
                  "target": "squirrel"
              },
              "squirrelWindows": {
                  "iconUrl": "[URL]"
              }
      

      En fait, en prenant le temps, la documentation plus les projets exemples permettent de s’en sortir.

      Build

      La commande est simple "dist":"./node_modules/.bin/electron-builder -lw" . Par défaut, il ne réalise l’action que pour la plateforme utilisée. Il faut penser à ajouter -w (windows) ou -m (mac) si on est sous linux. Dans mon cas, cela donne -lw (linux + windows).

      Alors forcément, c’est là que les ennuis commencent … Enfin pour Windows car Linux, aucun souci ! Et finalement, je suis retombé sur le même problème que la dernière fois: il manque des dépendances …

      PAF dans la gueule !

      J’ai continué à lire la documentation et je suis tombé sur cette page : ici. Et là: PAF dans la gueule ! DOCKER !!! Mais oui bien sûr. Je l’utilise tous les jours et en plus ici, ils fournissent des images.

      Souci de timezone

      Avec la commande fournie, j’ai quand même un souci : Could not find file "/etc/localtime". Il s’agit d’un bug connu de mono (nécessaire pour le build pour Windows) mais qui n’est pas corrigé dans la version de l’image fournie. Pas grave, il suffit de changer la commande en ajoutant un paramètre :

      
      docker run --rm -ti  --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS_TAG|TRAVIS|TRAVIS_REPO_|TRAVIS_BUILD_|TRAVIS_BRANCH|TRAVIS_PULL_REQUEST_|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_')  --env ELECTRON_CACHE="/root/.cache/electron"  --env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder"  -v ${PWD}:/project  -v ${PWD##*/}-node-modules:/project/node_modules  -v ~/.cache/electron:/root/.cache/electron  -v ~/.cache/electron-builder:/root/.cache/electron-builder -v /etc/localtime:/etc/localtime electronuserland/builder:wine-mono /bin/bash -c "npm run dist"
      

      Le build fonctionne mais …

      Mais pas l’installer : « This a dummy update.exe […] ». Qwant n’étant pas si mal que cela, j’ai trouvé UNE information mais c’était la bonne. Il fallait nettoyer le répertoire nodes_modules : et pour le coup ça marche !

      Electron-simple-updater

      Alors en fait

      Je vais commencer par un autre package : electron-simple-publisher. Ce package doit permettre de publier et mettre à jour le fichier de release qui est utilisé par electron-simple-updated.

      Vous reprendrez bien du NPM

      npm install --save-dev electron-simple-publisher

      Configuration

      Ayant un FTP sous la main, je vais l’utiliser. J’ai crée (comme demandé), un fichier publisher.json reprennant l’exemple ici.

      La commande

      node_modules/.bin/publish all

      Le all est présent car défaut, il ne publie que celui de la plateforme en cours (c’est un peu une manie).

      Modification

      On fait comme il demande :

      
      // [...]
      const updater = require('electron-simple-updater
      // [...]
      // Gestion de la mise à jour
      updater.init();
      

      Le chemin vers le fichier update.json (qui gère les versions) est directement dans le package.json.

      Test !

      Et bien çà marche !

      Le test :

      • Installation de la version 1.0.1 manuellement sur un poste Windows,
      • Génération et publication de la version 1.0.2,
      • Relance de l’application –> constat que la version 1.0.2 est bien là
      • Relance de l’application –> c’est la version 1.0.2.

      Suite ?

      Il faut que je trouve un moyen d’éviter les relances …

      Bilan

      Je builde et publie depuis Linux une application Electron qui tourne sous Windows ! Bref, ça marche !

      D’ailleurs pour une fois, j’ai presque pas eu de soucis entre la documentation et l’action. Cela explique peut-être le résultat 🙂

webpack and Friends

webpack

Utilisant Angular depuis pas mal de temps et se servant de la CLI pour générer mes projets, j’ai entendu parlé de webpack. Je sais plus ou moins à quoi cela sert mais cela reste assez flou. Sur le principe dans mon cas très personnel (malheureusement, développeur du dimanche), j’ai pas forcément besoin de maîtriser cette partie. ng serve et ng build sont largement suffisant. Mais pour ma culture, je pense qu’il serait pas mal de savoir un peu ce qui se cache derrière cet outil !

Bundler : mais c’est quoi donc ?

Un peu d’histoire

Au début était l’HTML (bien) puis le CSS (pas bien) et enfin le JS (mieux) (je suis pas sûr de l’ordre). Comme toujours au début: c’était simple. Les pages étaient statiques et moches et donc simples. Mais comme d’habitude, les possibilités appelant les possibilités, les choses se sont complexifiées …

Les projets se trouvaient avec de plus en plus de code JS dont il fallait gérer l’inclusion et les dépendances avec tous les risques et problèmes. De plus comme l’indique cette vidéo, l’humain et la machine ne fonctionne pas pareil. Par exemple, l’un veut des choses découpées (pour mieux régner) et l’autre veut tout en un même si dans les deux cas c’est pour aller plus vite.

Pour faciliter cela différents outils ont été mis en place comme des tasks runners (Gulp, Grunt) qui vont permettre simplifier les choses. A cette époque, j’étais sur PHP/Symfony sans faire de JS donc j’ai un peu zappé cette partie. Maintenant, il semble que nous sommes passés au Bundler (tantan !)

L’objectif

L’objectif est de répondre au deux besoins: humains et machines. Pour l’humain pouvoir découper son projet, mettre des commentaires, etc… et au final n’avoir qu’un minimum de fichiers. Pour ce faire (dans les grandes lignes), le bundler va regrouper l’ensemble des fichiers JS (entres-autres) dans un seul et même fichier « bundle ». En plus, il sera capable d’appliquer différents traitements (suppression des commentaires, minification, treeshaking, ….) dans une optique d’optimisation du code en tant que tel mais également de la taille des paquets (bandes passantes).

Pour mieux comprendre !

Deux vidéos et quelques liens :

Jouons un peu !

Getting started

Plutôt que d’aller chercher des tutos externes, je suis simplement le guide fourni par webpack : ici. Une première chose qui chose avec ce que j’avais entendu: webpack4 n’a plus besoin de configuration par défaut. Bon après, dès la 2ème partie, on crée un fichier de configuration.

Assets Management

Dans cette partie, il y a un point qui m’a gêné : le fait de devoir un import du CSS dans le JS :

Je comprends bien le pourquoi et cela explique des choses par rapport à la configuration Angular mais sur le principe, déplacer le référencement du CSS de l’HTML vers le JS … A la limite, j’aurais mieux compris de devoir mettre le fichier dans webpack.config.js.

D’ailleurs, j’ai voulu tester sans la présence du fichier (normalement 0 config) mais je me prends la même erreur que la vidéo :

En fait, il faut bien déclarer les loaders car sinon il ne sait pas quoi faire … J’ai surement loupé une étape mais le 0 config semble limité …

Output Management

Ouf ! Le fichier HTML peut-être généré 🙂

Bilan

Honnêtement, je suis bien content de déléguer le build à l’équipe d’Angular CLI. A première vue, les concepts sont simples mais pour vraiment bien gérer et optimiser les choses, il doit falloir bien maîtriser les méandres de l’application !

L’objectif était d’avoir des notions sur les concepts et ben … on va dire que c’est bon 🙂

Installer et packager des électrons !

Introduction

Après une nouvelle période d’absence (été, projets), je reprends les jours R&D pour la rentrée ! Premier sujet de cette nouvelle: générer un installeur windows pour une application Electron.

Premiers pas

Electron Installer

Après une rapide recherche, je suis tombé sur plusieurs articles qui pointaient vers le même projet : electron-winstaller donc je pense que je vais regarder par là. Première chose : le projet utilise Squirrel. Mais c’est quoi donc Squirrel ?

Squirrel

Le titre du projet est assez évocateur : « Squirrel, It’s like ClickOne but Works » … Le but est de créer des installeur pour windows (IOs aussi). Le projet et sa documentation cible très clairement les applications .Net et utilise le système de package associé « NuGet ». Le système propose des mécanismes de mises à jour en arrière plan ce qui peut-être pratique !

J’essaye !

Package

Le projet utilisé pour test est une reprise et j’avais oublié qu’il n’avait pas de package … donc il faut commencer par remettre en place la possibilité de packager car c’est un prérequis

  • Installation du packager: npm install -g electron-packager,
  • Version Linux : electron-packager . teamweather --platform=linux --arch=x64 --out=release-builds --prune=true --overwrite
  • Version Windows : electron-packager . teamweather --platform=win32 --arch=x64 --out=release-builds --prune=true --overwrite

Le package ne pose aucun souci … pas une erreur rien. C’est louche mais ca marche

Installer

Script

La documentation décrit le code qu’il faut utiliser. Personnellement, j’ai suivi le conseil d’un des articles et j’ai externalisé le code dans une commande. Voici le code (qui est très proche de celui présent dans les articles …) :


// Dépendandes
const createWindowsInstaller = require('electron-winstaller').createWindowsInstaller
const path = require('path')

// Appel de la fonction
getInstallerConfig()
  .then(createWindowsInstaller)
  .catch((error) => {
    console.error(error.message || error)
    process.exit(1)
  })

  // La fonction de build
function getInstallerConfig () {
  // un petit log
  console.log('creating windows installer')
  // Point de départ
  const rootPath = path.join('./')
  // Sortie
  const outPath = path.join(rootPath, 'release-builds')

  // Action !!!
  return Promise.resolve({
    appDirectory: path.join(outPath, 'teamweather-win32-x64/'), // Là où est la version packagé
    authors: 'TeamWeather Team', // L'auteur
    noMsi: true,
    outputDirectory: path.join(outPath, 'windows-installer'), // Là où sera déposé le package
    exe: 'teamweather.exe', // l'exec
    setupExe: 'TeamWeatherAppInstaller.exe' // le nom du setup
  })
}

Pour builder, il suffit alors de lancer la commande : node installers/windows/createinstaller.js

Build

Alors ... comment dire que j'ai galéré. En même temps, c'est ma faute : j'étais sous Linux pour un installer Windows. J'ai essayé d'installer des éléments manquants (Mono) mais c'était jamais la bonne version ou je prennais le risque de casser ma distrib. Totale, je suis passer sous Windows et 5 minutes c'était réglé ... :

npm install --save-dev electron-winstaller
node installers/windows/createinstaller.js

Voilà !

Actions avant / après

Comme indiqué dans la documentation, il est possible de réaliser des commandes en mode "installation/désinstallation". Par exemple, après l'installation, enregistrer une clé de registre. Je n'ai pas eu le temps de pousser cette partie en raison des soucis de build mais le principe est assez simple

.

Suite à une action "setup", l'executable principale est appelée avec un paramètre du type '--squirrel-install'. Il faut donc détecter le paramètre et réaliser les actions que l'on souhaite en fonction du paramètre. Voici deux exemples :


case '--squirrel-install':
case '--squirrel-updated':
                // Ajout d'un raccourci
                spawnUpdate(['--createShortcut', exeName]);
                setTimeout(app.quit, 1000);
                return true;
case '--squirrel-uninstall':
                // Suppression du raccourci
                spawnUpdate(['--removeShortcut', exeName]);
                setTimeout(app.quit, 1000);
                return true;

Un point important : il faut que les traitements soient rapides ! En effet, Squirrel car couper l'application au bout d'une seconde ... Pas le temps de traîner !

Bilan

J'ai bien un installer mais j'ai pu regarder les mises à jour ... J'aurais pas du insister à vouloir builder un package windows sous linux ... Ce sera pour un autre demain.

Liens

Quelques liens :

Tu m’entends quand je te parle ? (partie 3)

Suite + 1

Dans la suite des journées 1 & 2, je continue le développement de ma petite application permettant d’échanger avec le téléphone. Aujourd’hui, on va essayer d’intégrer avec un ChatBot. Je sais, les chatbot c’est « So 2017 » et pas du tout 2018 mais est-ce ma faute à moi si je suis toujours en retard ? 😉

Choix

En regardant rapidement, je suis tomber sur plusieurs solutions :

Il y en a peut-être d’autres … Pour les premiers pas, je vais essayer « DialogFlow » pour deux raisons: un c’est l’ordre alphabétique et deux: c’est le premier qui sort dans les recherchers (Google ? ;)). Une autre raison est que je également trouvé un plugin cordova pour cette API.

Dialog Flow

Création d’un agent

Pour commencer, il faut créer un agent. Personnellement, je vois un agent comme un « projet ». Cet agent va recueillir les données entrantes ‘input’ et les faire passer dans les différentes règles mises en place. Au niveau interface, c’est simple, il faut cliquer sur « CREATE AGENT ».

Un formulaire va apparaître vous permettant de paramétrer la langue par défaut, la timezone et de lier au projet Google. Je me suis pas embêter: France, Madrid et nouveau projet.

Une fois l’agent crée …

Il est possible de créer directement des « Intents ». Les « Intents » comme le nom l’indique doivent être vus comme les intentions de l’utilisateur. Du moins, celle que l’on essaye de déterminer. Il est également possible de créer des entités. Suite aux différentes lectures, je vais commencer par créer des entités.

Pourquoi ? Parce que ?

Je commence par cela car lors de la création des intents, il est possible de « mapper » certains entrants avec justement des entités. Donc il me semble assez logique de commencer par cela.

Création de deux entités

Pour mon test, je crée deux entités : @quartier et @numero_animal. Voici la définition de quartier :

La première colonne correspond à la valeur qui sera recherchée et retournée. La deuxième colonne présente les différents synonymes possible. Cette liste est importante car même si DialogFlow propose des entités par défaut (country, last_name, date, nombre, …), il ne peut pas tout détecter et spécialement quelque chose d’aussi métier que le quartier …

.

Le numéro d’animal reprend le même principe. Au final, je n’étais pas obligé de créer cette entité car j’aurais pu demander à rechercher un nombre. Mais l’idée était de l’aider à sélectionner les bons nombres et pas se mélanger avec éventuellement d’autres informations.

Et maintenant l’intent

Dans le cas du test, je n’ai crée qu’un seul Intent qui permet d’aider à la saisie d’une mammite. Pour commencer, il faut mettre des phrases (Training Phrases) qui correspondent à des entrées possibles d’un utilisateur. Sur cette base d’exemples, DialogFlow sera capable de comprendre d’autres formats d’entrées plus ou moins correspondant :

Pour chaque phrase, il est possible (souhaitable) d’indiquer les champs qui vont être les différents paramètres attendus pour l’action finale (ici, la saisie d’une mammite). Dans l’exemple de la dernière phrase, j’ai mappé sur un numéro d’animal, une date et différents quartiers. Cela renseigne directement le tableau des paramètres :

J’ai marqué tous les champs comme étant obligatoire (required). Cela permet d’avoir une fonctionnalité très pratique : si les données ne sont présentes dans la première phrase saisie par l’utilisateur, il sera possible d’avoir directement une question pour chaque paramètre manquant. D’où le fait de pouvoir saisir un libellé (prompts) pour les paramètres obligatoires

Il faut également noter que le paramètre « quartier » est une liste pour pouvoir gérer qu’une mammite est potentiellement sur plusieurs quartiers…

Tests

L’interface permet de tester directement l’agent :

Dès le premier test, il est possible de voir que la phrase d’entrée ne correspond pas tout à fait aux phrases mais le système est quand même capable de faire le lien et d’extraire les informations.

Intégration dans le chat

Plugin

Comme indiqué dans le début de l’article, il existe un plugin cordova pour communiquer avec DialogFlow. Pour l’installer, la commande habituelle :

ionic cordova plugin add cordova-plugin-apiai

NOTE: le plugin est pour le moment compatible uniquement avec la version 1 de l'API. Une V2 est disponible. Il faut aller modifier cela dans les paramètres de l'agent.

Mise en place et utilisation

Simple !

Il faut commencer par activer le plugin avec la clé (qui se trouve dans les paramètres de l'agent) :


platform.ready().then(() => {
            ApiAIPromises.init({
                clientAccessToken: "[AGENT_TOKEN]"
            })
                .then((result) => console.log(result))
        });


puis il faut faire appel aux services avec l'input de l'utilisateur :


       ApiAIPromises.requestText({
            query: message
          })
          .then((data) => {
             this.zone.run(()=> {
                 let answer;
                 console.log(data);
                 if(data.result.actionIncomplete) { 
                     // l'action n'est pas complète donc on simplement pose la question
                    answer = data.result.fulfillment.speech;
                 } else {
                    // l'action est complète : on retourne un résultat
                    answer = this.chatService.getBotAnswer(data.result);
                 }
                 this.sendSystemMessage(answer);
                 this.tts.speak({
                             text: answer
                             , locale: 'fr-FR'
                             , rate: 0.85
                         });
             });

La complexité n'est pas là 🙂

Tests

Les images suivantes vous montrent différents tests réalisés

A noter la gestion des dates : il y a 2 jours fonctionne bien !

Un souci de liste

Un point que sur lequel j'ai buté : la détection d'une liste de quartiers. Sur l'interface de test :

Il semble être en mesure de détecter les quartiers ... Par contre, sur l'application :


Pourquoi : je ne sais pas encore ...

Bilan : mitigé

Honnêtement, je suis assez bluffé par le capacité d'adaptation du système. Par contre, je suis déçu de la partie documentation: je n'ai pas trouvé d'information ni de guide sur des choses qui me paraissent important :

  • Comment chaîner des actions ?
  • Comment permettre l'identification selon deux critères: numéro et nom animal par exemple ?
  • Comment reprendre un échange ? revenir en arrière dans la discussion ?

Mais bon... je n'ai fait qu'une journée ...

Update 1

Action suivante

Bon forcément j'ai fini par trouver comment faire une action suivante : il existe les "Follow-up intents" qui permettent de faire la suite d'un intent de départ.

L'élément intéressant est la notion de contexte. En effet, en créant ce nouvel "Intent", DialogFlow ajoute automatiquement un context en sortie du premier Intent et un context en entrée du second :


Le contexte contient les données ce qui permet de les utiliser dans le second Intent

Récupération de valeurs

Transition : la récupération des valeurs se fait par le contexte ! Et là, il est encore possible de les passer en réponses de plusieurs manières !

Il est possible de remettre comme des paramètres :

Comme Payload dans la réponse:

Mais également dans le contexte :

Bref ... on peut la trouver à plein d'endroits !

Adaptation du code

Avec les avancés, je peux modifier le code de mon application Android qui devient simplement :


 ApiAIPromises.requestText({
            query: message
          })
          .then((data) => {
             this.zone.run(()=> {
                 console.log(data);
                 const answer = data.result.fulfillment.speech;
                 this.sendSystemMessage(answer);
                 this.tts.speak({
                             text: answer
                             , locale: 'fr-FR'
                             , rate: 0.85
                         });
             });

En effet, je me suis arrangé pour que les réponses soient directement dans envoyé par l'Intent :

Liens

Tu m’entends quand je te parle ? (partie 2)

Suite

Dans la continuité de l’article précédent : ici. L’idée est d’essayer de faire une application mobile qui intègre les possibilités d’écoute d’un utilisateur.

Point de départ

Afin de partir d’un existant, je suis partie d’une application réalisée suite au tuto suivant : https://devdactic.com/ionic-realtime-socket-io/. D’ailleurs, pour info, le tuto est super intéressant et montre une fonctionnalité intéressante de socket.io. Une fois n’est pas coutume, tout fonctionne du premier coup !

En terme de reprise, l’idée est de reprendre l’application, la navigation etc… Par contre, je vais enlever toute la partie Socket … A ce demander pourquoi je l’ai repris en fait … Bref …

SpeechToText

Mise en place

Pour faire cela, j’ai repris le plugin vu lors de la première partie : ici et . L’installation est assez simple 🙂


$ ionic cordova plugin add cordova-plugin-speechrecognition
$ npm install --save @ionic-native/speech-recognition

Au niveau de l’utilisation, il faut suivre la documentation dans laquelle je trouve qu’il ne manque qu’une seule chose: l’enregistrement dans les modules. Mais bon c’est pas complique :

  • Ajout d’un import import { SpeechRecognition } from '@ionic-native/speech-recognition';
  • Ajout d’un provider : SpeechRecognition

Voici le code pour lancer l’écoute :

    listenUserMessage() {
        let options : SpeechRecognitionListeningOptions = {
            language: 'fr-FR'
            , matches: 1
            , prompt: ''
            , showPopup: false
            , showPartial: false
        }

        this.speechRecognition.startListening(options)
            .subscribe(
                (matches: Array) => {
                    this.zone.run(() => { 
                        this.sendMessage(this.nickname, matches[0]); 
                        this.answer(matches[0])
                    });
                },
                (onerror) => console.error('error:', onerror)
            )
    }

Quelques petits soucis 🙂

Normal ! Les voicis :

Matches

Une option doit permettre de limiter le nombre de retour : matches. Bon de mon côté, il s’en moque totalement … Pour des phrases, il m’en retourne 5 qui est la valeur par défaut … Bon, je prends le premier résultat.

Mise à jour de l’écran

A la réception des données, l’interface ne se mettait pas à jour. J’ai cru comprendre que c’était un souci d’évènement que je remonte mal. Pour résoudre le souci à court terme : this.zone.run()

TextToSpeech

La même chose mais avec un autre plugin !

Il faut utiliser : ici & qui s’installe comme le premier :


$ ionic cordova plugin add cordova-plugin-tts
$ npm install --save @ionic-native/text-to-speech

Code

Simple à faire peur :

 this.tts.speak({
                text: answer
                , locale: 'fr-FR'
                , rate: 0.85
            })

.

Un truc assez fort !

Si le texte contient « 15:34 », on entend 15 heures 34. Fort quand même …

Bilan

Je suis content, j’ai une application qui fonctionne 🙂 Elle écoute et réponds à quelques questions. Comme vu la dernière fois, ce n’est pas là que se passe les choses. il faudrait maintenant la pluguer vers quelque chose d’intelligent. Mais cela c’est autre demain !

Tu m’entends quand je te parle ?

En discutant avec un client, il m’a indiqué qu’il aimerait bien pouvoir piloter certaines applications par la parole … Tiens, effectivement, cela peut-être intéressant …

Web Speech API

Présentation

Il s’agit d’une API qui semble exister depuis plusieurs années (2014 ?) mais qui ne semblent ne pas être très encore supportée :

avec quelques compléments :

  • Chrome: ne supporte pas l’extension directe mais il faut passer par WebKit,
  • Firefox: il faut activer une option media.webspeech.recognition.enable.

Autant dire que c’est pas encore la reconnaissance totale (jeu de mot).

Trouver où le faire …

Alors pour tester c’est pas simple :). En fait, cela ne semble pas supporter dans Firefox même avec le flag passer à true (confirmé ici & ). J’ai essayé de faire fonctionner les exemples et cela n’a jamais fonctionné … Donc passage sous chrome enfin chromium chez moi.

Pour pas perdre trop de temps: il faut faire tourner sur un serveur local (Merci Docker !) sinon souci de sécurité. En cas de MEP (?!?!), ne pas oublier l’HTTPS car sinon cela ne marche pas !

Code

Le code de test est très simple et est une adaptation des articles de MDN:


// Init en 100% Chrome
var recognition = new webkitSpeechRecognition();
// Ne France Monsieur !
recognition.lang = 'fr-FR';
// On retourne tout !
recognition.interimResults = true; 
// Valeur par défaut
recognition.maxAlternatives = 1;


// Listeners
$('#btnStart').click(function() { recognition.start(); });
$('#btnStop').click(function() { recognition.stop(); });

recognition.onresult = function(event) {
    console.log(event);
}

Console

Regarder le contenu en console est intéressant car on voir l’avancée de le reconnaissance. La phrase prononcée est « bonjour messieurs ». Avec les différents résultats, le log contient les différentes reconnaissances :

Un test avec une phrase plus longue: ‘de bon matin, je parle à un micro qui ne me réponds pas’:

Comme il faut tout tester : un gros mot 🙂

T’arrêtes pas !

Par défaut, le système d’écoute s’arrête quand le système atteint une phrase « finale ». Pour le mettre en continue : recognition.continuous = true;. Je pense qu’un petit time out serait pas mal quand même :).

Mode connecté

Le message par MDN est assez clair :

Donc attention: vous êtes sur écoute !!!

Bilan

Sous Chrome cela fonctionne pas mal quand même. Il faut définir l’après et l’utilisation mais la reconnaissance de texte fonctionne quand même pas mal. Par contre, en regardant d’un peu plus près les dates des différents articles, on voit que le sujet ne semble pas avoir beaucoup bougé depuis les premiers travaux en 2014 …

Liens

Cordova – Plugin

Il existe un plugin pour intégrer le même mécanisme dans une application mobile : ici. Le documentation est assez clair et le plus compliqué a été de mettre à jour Cordova.

Pour info et d’après la documentation, le plugin utilise l’API de Google (ici) mais elle le fait au travers SpeechRecognizer. A noter que la documentation indique que l’écoute ne peut être continue.

Bilan

Bon ben c’est pas mal mais le vrai travail est plus dans l’interprétation mais c’est assez sympa. A la prochaine …

GraphQL – Partie 3

Un peu de Back !

Suite à la partie qui permettait de consommer l’api de GitHub, passage du côté obscur : le back !

GraphQL.org

Le site GraphQL.org propose pas mal de ressources et pour me remettre dans le bain, j’ai parcouru les pages graphql-js.

J’aime beaucoup l’exemple « Object Types » qui montre bien qu’il n’y a pas vraiment de magie: il faut bien implémenter les différentes « sous-champs » d’une future query…

Question: est-ce qu’il n’y a pas un risque de doubler certaines définitions entre une définition d’entités et la définition des types pour l’API ? Je suis sur qu’il doit y avoir une librairie pour çà … enfin une … JS Fatigue ?

C’est parti

Pas été facile de trouver une base pour avancer car beaucoup d’articles se basent sur une base de données « simulée » ou Prisma. Mais je préférais me baser directement sur une base de données comme SQLlite pour essayer d’avoir un maximum d’étapes. Je suis donc parti sur ce tuto qui utilise Apollo Server ce qui est assez logique avec le premier article.

Prisma

Un point sur Prisma cependant. Il s’agit d’un projet associé à GraphCool qui propose une abstraction de la base de données. L’idée est de décrire l’API et Prisma va « gérer » le lien avec le stockage.

A noter que pour le moment, j’ai l’impression que Prisma intègre en interne une base de données (MySQL) et ne permet pas la mise en place d’un lien vers une base de données existantes. Il faut passer par des imports / exports.

Mise en place

Dans le cadre du tuto, la mise en place est assez simple :

  • Création des entités,
  • Création du schéma (et donc redéfinition …),
  • Mise en place des resolvers : là ou les choses se passent :),

N+1

Présentation

A ce stade, la simple API que nous utilisons montrent un futur souci en cas de montée en complexité. Sur la base de la query suivant :


{
  allAuthors {
    id
    lastName
    firstName
    posts {
      id
      title
      author {
        id
        lastName
        firstName
      }
    }
  }
}

(je suis d’accord, c’est idiot de redemander l’auteur d’un post dans ce contexte mais c’est pour l’exemple).

La trace SQL donne le résultat suivant :

Après la requête de recherche des auteurs, l’API charge chaque liste de posts et pour chaque post, charge l’auteur. Au regard du code c’est complètement logique :


// [...]
Author: {
    posts(author) {
        return author.getPosts();
    }
[...]
Post: {
    author(post) {
      return post.getAuthor();
    }
}

Comme chaque méthode est appelée de manière unique et que chaque méthode effectue une requête : multiplication des requêtes !

Batch

Bon évidemment, le souci est connu et a été adressé et comme d'hab, il y a plusieurs solutions disponibles mais elles tournent autour du principe de regrouper les requêtes en batch. Voici quelques librairies trouvées au hasard des recherches :

  • DataLoader : librairie de Facebook qui permet de regrouper des "requêtes" et de gérer un cache,
  • GraphQL Batch Resolver: alternative se voulant plus simple à DataLoader compatible également avec GraphQL.js et graphql-tools,
  • JoinMonster: alternative à DataLoader mais avec une orientation plus SQL mais pas adapté à graphql-tools,
  • join-monster-graphql-tools-adapter: Lien issu de la doc de JoinMonster, il s'agit d'une version fonctionnant avec graphql-tools.

DataLoader

Pourquoi :

  • C'est celle qui est évoquée sur la page d'Apollo Server : ici,
  • Les autres se présentent comme des alternatives : autant prendre la base.

Mise en place : les auteurs des posts

Le code suivant est basé sur le post : Batching GraphQL Queries with DataLoader. Il est adapté au contexte du projet en cours (forcément).

Il faut commencer par mettre en place un loader :


export const authorLoaders = new DataLoader(ids => {
    return Author.findAll({
                            where: {
                                id: {
                                    [Op.in]: ids
                                }
                            }
                        })
                .then((authors) => {
                    const authorsByIds = _.keyBy(authors, "id");
                    return ids.map(authorId => authorsByIds[authorId]);
                });
});

Cette fonction prend en paramètre, une liste d'ids (ceux des auteurs recherchés) et appelle la fonction de recherche selon la syntaxe de Sequelize. Ensuite, il faut prendre les différents résultats et les trier dans l'ordre du tableau en paramètre. DataLoader va retourner le résultat à chaque appelant (à priori) en se basant sur l'ordre initial.

Ensuite, il faut intégrer ce loader dans le context de graphql :


graphQLServer.use(
  '/graphql',
  bodyParser.json(),
  graphqlExpress({
    schema
    , context: { postsLoaders, authorLoaders }
    // This option turns on tracing
    , tracing: false
  })
);

Et finalement, il faut modifier le code d'appel pour utiliser le loader au niveau du resolver :


  author(post, args, loaders) {
      return loaders.authorLoaders.load(post.authorId)
                            .then((author) => { return author; });
    },

Magie !

DataLoader va intercepter les différents appels et au moment opportun, va appeler le loader qui va retourner toutes les données. Au niveau trace SQL, il n'existe plus qu'une seule requête :


Executing (default): SELECT `id`, `firstName`, `lastName`, `createdAt`, `updatedAt` FROM `authors` AS `author` WHERE `author`.`id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Mise en place : les posts

Pour les posts c'est pareil mais plus compliqué. En fait, je n'ai pas trouvé de solution en tant que tel ... En effet, DataLoader propose deux méthodes :

  • load: pour une entrée attend une valeur en sortie,
  • loadMany: pour un tableau en entrée attend un tableau en sortie.

Dans le cas présent, le principe est d'avoir une entrée et plusieurs sorties. En effet, ici, l'idée est d'avoir les posts d'un auteur donc 1 -> N. La seule solution que j'ai trouvée est celle-ci :


export const postsLoaders = new DataLoader(authorIds => {
    return Post.findAll({
                            where: {
                                authorId: {
                                    [Op.in]: authorIds
                                }
                            }
                        })
                .then((posts) => {
                    const grouped = _.groupBy(posts, "authorId");
                    return authorIds.map((authorId) => {
                            return { posts: grouped[authorId] };
                        });
                });
});

Le principe est similaire au cas précédent : le retour est groupé par authorId. Ensuite dans la fonction map, je regroupe dans un variable avec une seule propriété ... Ce qui donne dans le resolver :


return loaders.postsLoaders.load(author.id).then((res2) => { return res2.posts; });

Je ne suis pas sûr que ce soit la bonne méthode ...

Résultats


Executing (default): SELECT `id`, `firstName`, `lastName`, `createdAt`, `updatedAt` FROM `authors` AS `author`;
Executing (default): SELECT `id`, `title`, `text`, `createdAt`, `updatedAt`, `authorId` FROM `posts` AS `post` WHERE `post`.`authorId` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Executing (default): SELECT `id`, `firstName`, `lastName`, `createdAt`, `updatedAt` FROM `authors` AS `author` WHERE `author`.`id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

A voir s'il est possible d'éviter les trois requêtes mais c'est toujours mieux que dans le cas précédent ...

Liens

Bilan

Bon après plusieurs jours sur GraphQL : quel bilan ? Le même que d'habitude : c'est un outil de plus dans la longue liste des outils possibles. Je vois bien les avantages par rapport à une API Rest mais je ne sais pas si la complexité induite est suffisante.

GraphQL – Partie 2

graphql-request /GitHub

Pour continuer ma compréhension du sujet, je reviens à une librairie « très » simple : graphql-request. Le but va être d’aller faire quelques requêtes sur l’API de GitHub.

C’est parti !

On commence simple :

  • npm init
  • npm install graphql-request

.
Ensuite, il suffit de copier/adapter l’exemple fourni :


import { GraphQLClient } from 'graphql-request'

// La requête
const query = `{
    viewer {
        login
    }
    rateLimit {
        limit
        cost
        remaining
        resetAt
    }
}`;

// Création d'un client
const client = new GraphQLClient('https://api.github.com/graphql', {
    headers: {
      Authorization: 'Bearer [MACLE]',
    },
  })
  
client.request(query)
    .then(data => console.debug(JSON.stringify(data)) )
    .catch((error) => { console.error(JSON.stringify(error)); })

« et voilà » :


{
	"viewer": {
		"login": "devbieres"
	},
	"rateLimit": {
		"limit": 5000,
		"cost": 1,
		"remaining": 4997,
		"resetAt": "2018-02-23T09:22:34Z"
	}
}

.

Fini !

Bon, ben c’était rapide 🙂

Un élément intéressant

Même si c’était très rapide, il y a un point intéressant dans cette petite requête: la gestion du coût. En effet, en REST, une requête effectuée est par définition limité ou limitable. Il suffit de mettre en dure des limites et des protections. Par dans GraphQL : c’est pas trop le principe. De plus, dans une seule requête, il est possible de ramener la base de données :).

GitHub l’explique assez bien ici. En V3, c’était 5 000 requêtes par heure. En V4, c’est 5 000 unités de calcul par heure. Et la requête que j’effectue permet justement de savoir le crédit qu’il me reste. Sachant que pour savoir, cela me coûte :(.

Cette logique de passe au coût de calcul est finalement assez logique et de plus va dans le sens du « Pay-as-you-go » en vogue sur de nombreuses plateformes.

Un deuxième élément intéressant

La combinaison de requête: je récupère deux types d’informations en même temps. Ce qui vient renforcer le point précédent.

Une deuxième requête

Comment perdre du temps …

Dans la série « comment perdre du temps », j’ai voulu récupérer les releases d’un projet. Le premier qui met venu : « angular ». Problème : pas moyen d’avoir les infos … je creuse, je change, rien … Par dépit, je tente un autre projet « material » et là, ça fonctionne. Je teste une dizaine d’autres et même résultat : cela fonctionne. Pourquoi cela ne fonctionne pas avec Angular … aucune idée.

La requête


const query = `query($owner: String!, $name: String!) {
    repository(owner:$owner, name:$name ) {
        releases(first:1, orderBy: { field: CREATED_AT, direction: DESC}) {
          edges {
            node {
              name
              tag {
                name
              }
            }
          }
        }
        tags:refs(refPrefix:"refs/tags/", first:1, orderBy:{ field: TAG_COMMIT_DATE, direction: DESC }) {
          edges {
            tag:node {
              name
            }
          }
        }
    }
    rateLimit {
           limit
           cost
           remaining
           resetAt
       }
}
`;

Pour rendre ma requête plus générique, je passe par des variables ce qui ne change pas grand chose dans l’absolu. Ensuite, le reste est assez explicite au niveau de la requête (avantage de GraphQL).

Résultats

Avec angular-cli :


{
  "data": {
    "repository": {
      "releases": {
        "edges": [
          {
            "node": {
              "name": "v6.0.0-beta.3",
              "tag": {
                "name": "v6.0.0-beta.3"
              }
            }
          }
        ]
      },
      "tags": {
        "edges": [
          {
            "tag": {
              "name": "v6.0.0-beta.3"
            }
          }
        ]
      }
    }
  }
}