Headless Blog mit dem JavaScript Framework Sails.js

Sep 24, 2019 | Academy Day, Headless Blog, Wissen

SailsJS – Ohne Patenthalse zur API

Sails ist ein Node.js MVC Framework dessen Focus mehr darauf liegt eine gesunde Grund­struktur zu schaffen als einen kompletten Satz an Tools auszu­liefern. Dies bedeuted jedoch nicht nur eine sinnvolle Trennung zwischen Daten, Controllern und Ansichten sondern auch einen Satz an ( tatsächlich sinnvoll ) nutzbaren Genera­toren, ORMs und Plugins.
Wer einen schnellen Einstieg in die Entwicklung von Node-basierten APIs sucht und eine saubere MVC-Struktur ebenso mag wie ich, dem lege ich diese Framework ans Herz.

Vorbe­reitung unseres Turns

Im Rahmen des Projektes sollte nun ein Headless Blog entwi­ckelt werden, dass heist keine Views, nur API Routen mit Controllern. Wir werden uns hierbei mithilfe der Genera­toren in wenigen minuten eine komplette Struktur für unsere Daten schaffen. Deswei­teren muss unsere Anwendung in einem Docker Container laufen um sie korrekt mit der CI Umgebung zu verbinden.

Vorräte kontrol­lieren

Instal­lation von Sails

Wir starten mit einem leeren Ordner, instal­lierten Node.js und einem Webbrowser der uns die Seite https://sailsjs.com/get-started anzeigt.

In (relativ) schneller Abfolge führen wir 

npm install sails -g
sails new FullSails

aus und beant­worten die Fragen des Installers. Wir wählen für unsere Zwecken das leere Sails Projekt aus und starten anschließend mit 

cd FullSails
sails lift
die Anwendung. Bei Erfolg bekommen wir in der Konsole ein nettes Schiffchen auf großer Fahrt zu sehen.

Der Ordner api beinhaltet alles was wir für unsere Anwendung benötigen. Ansonsten ist noch der Ordner config von Interesse, da er die Konfi­gu­ra­ti­ons­da­teien beinhaltet um z.B. im nächsten Schritt unsere Datenbank anzubinden. Neben den genannten gibt es noch

assets: Beinhaltet die stati­schen Assets für die Auslie­ferung mittels des Webservers ( Bilder, js, etc )

tasks: Umfasst eine Sammlung von Tasks für das Entwi­ckeln wie Linting und Kompi­lieren von js und scss

views: Dort werden generierte Ansichten abgelegt. ( Standart­mäsig im ejs Format )

Da wir im Rahmen des Projektes nur eine API Entwi­ckeln, können wir im Prinzip alles ignorieren außer api und config.

Deswei­teren sollte unter localhost:1337 die Start­seite der Anwendung zu sehen sein. Ein kurzer Blick in die Ordner­struktur zeigt uns direkt wo sich später was befindet.

Instal­lation von PostgreSQL

Nun müssen wir noch unsere Datenbank verbinden. Die Entscheidung ist hierbei auf eine PostgreSQL Datenbank gefallen.

Es ist nicht nötig an dieser Stelle eine lokale Datenbank zu instal­lieren, da später sowieso auf Docker umgestellt wird. Wer auf Docker verzichten will, der sollte sich für sein Betriebs­system nun eine PostgreSQL Datenbank zulegen. Am besten richtet ihr dann gleich eine neue Datenbank mit Nutzer ein. Dieser Artikel beinhaltet alles was ihr dafür benötigt.

Ist das erledigt, öffnen wir einfach mal die Datei config/datastores.js.

default: {
 
/***************************************************************************
* *
* Want to use a different database during development? *
* *
* 1. Choose an adapter: *
* https://sailsjs.com/plugins/databases *
* *
* 2. Install it as a dependency of your Sails app. *
* (For example: npm install sails-mysql --save) *
* *
* 3. Then pass it in, along with a connection URL. *
* (See https://sailsjs.com/config/datastores for help.) *
* *
***************************************************************************/
// adapter: 'sails-mysql',
// url: 'mysql://user:password@host:port/database',
},

Im Prinzip sagt uns der Kommentar genau was getan werden muss, also fix auf https://sailsjs.com/plugins/databases und die Anwei­sungen befolgen:

npm install sails-postgresql --save
Und anschließend
...
// adapter: 'sails-mysql',
// url: 'mysql://user:password@host:port/database',
adapter: 'sails-postgresql',
url: 'localhost://sails:fullsailsahead@host:port/fullsails',
...

An dieser Stelle benutze ich nun die Datenbank fullsails mit dem Nutzer sails und dem Password fullsailsahead.  ( Anm.: Die Datenbank ist hier für eine lokale Verbindung einge­richtet )

Leinen Los

Nun, da das Projekt lokal läuft, sollten wir uns um die Docker Container kümmern. Dieser wird für die CI Pipeline benötigt und erlaubt es uns später ein Rechner­un­ab­hän­giges deployment einfacher umzusetzen.

Um die Anwendung mit Docker zu versehen, benötigen wir 2 Dockerfile und 1 docker-compose.yml. Es bietet sich herbei die Docker Container als Ordner­struktur unter der docker-compose.yml anzulegen. Konkret sollten wir unsere Anwendung, die momentan so aussieht:

  • Fullsails
    • api .…
    • assets .…
    • .…

umbauen, sodass die eigentlich Sails App gekapselt wird:

 

  • FullSails
    • backend
      • api .…
      • assets .…
      • Dockerfile
      • .…
    • postgresql
      • Dockerfile 
      • .….
    • docker-compose.yml

PostgreSQL mit Docker

Um eine PostgreSQL Datenbank mit Docker zu versehen, bietet sich das Dockerimage postgres:latest an. Somit lautet die Datei postgresql/Dockerfile

FROM postgres:latest

Und das wärs auch gewesen, aber wir benötigen ja noch die Datenbank mit User und Password. Dafür bietet das PostgreSQL Image die Möglichkeit eine init.sql Datei anzulegen, die beim Contai­ner­start alles anlegt.

CREATE USER sails with PASSWORD 'fullsailahead';
CREATE DATABASE fullsails;
GRANT ALL PRIVILEGES ON DATABASE fullsails TO sails;
Nun muss die Datei nur noch in den Container kopiert werden. Dafür erweitern wir unsere Dockerfile:
FROM postgres:latest
 
COPY init.sql /docker-entrypoint-initdb.d/
Das wars für PostgreSQL. Als nächsten verpacken wir unsere Sails Anwendung in Docker

Sails mit Docker

Sails ist nicht ganz so komfor­tabel wie die Datenbank, aber immer noch sehr simpel. Die backend/Dockerfile lautet
FROM node:latest
 
# add sources
COPY . /app
 
WORKDIR /app
 
RUN npm install
 
CMD ["npm","run", "start"]

Kurz umrissen wird hier einfach das neueste Node.js Image geladen, unser Quellcode wird in /app kopiert und anschließend wechseln wir dorthin und instal­lieren mit npm install unsere Abhän­gig­keiten.

Wichtig ist hier das wir die Anwendung mit npm run start hochfahren um sie in der Produktion Environment laufen zu lassen.

Kompo­sition der beiden Docker Container

Nun fehlt noch die Verknüpfung der beiden Container. Hierfür legen wir uns eine docker-compose.yml an.

version: '3.1'
services:
  api:
    build: ./backend
    ports:
      - "80:80"
      - "9229:9229"
    volumes:
      - ./backend:/app
    command: bash -c "npm install && npm start"
    environment:
      - NODE_ENV=development
    networks:
      - test
 
 
  postgresql:
      build: ./postgresql
      ports:
        - "5432:5432"
      networks:
        - test
 
networks:
  test:
    external:
      name: test

Von unten nach oben passiert nun folgendes:

  • Anlegen eines Netzwerkes zum kapseln der Container Verbindung
  • Definition des postgresql services. Hier wird die Dockerfile in ./postgresql gebaut, der Standartport nach außen geöffnet und der Container in das Netzwerk test gelegt.
  • Definition des api services. Analog wird der Inhalt von ./backend  gebaut. Anschließend werden die ports 80 (Webserver) und 9229 (Node Debug, Optional) geöffnet.
    Anschließend wird noch das Volume einge­bunden um die lokalen Änderungen ohne Neubau des Containers zu übernehmen. Wir setzen nun noch die NODE_ENV variable und fügen den Service ebenfalls zum Netzwerk hinzu.
    Das command wird hier zum starten des Containers benutzt und überschreibt den Befehl in der backend/Dockerfile. Dies erlaubt es uns mittels docker-compose up die Abhän­gig­keiten zu aktua­li­sieren.

Nun müssen wir noch die Verbindung zur Datenbank mittels Docker anpassen. Hierfür ersetzen wir in backend/config/datastore.js die hostadresse mit dem Namen des Postrg­reSQL Services

...
 
    adapter: 'sails-postgresql',
    url: 'postgresql://sails:fullsailahead@postgresql:5432/fullsails',
...
Jetzt sind wir fast fertig mit dem Setup. Wenn wir das Setup nun starten würden…
docker-compose up
…so wirft sails einen Fehler:

Also öffnen wir die beschriebene Datei in backend/config/env/production.js und ersetzen die werte für onlyAl­lo­wOr­igins sowie port. Dadurch wird der production server auch auf port 80 gestartet werden, was unser gewünschter Port für die API ist.

...
  sockets: {
 
    /***************************************************************************
    *                                                                          *
    * Uncomment the `onlyAllowOrigins` whitelist below to configure which      *
    * "origins" are allowed to open socket connections to your Sails app.      *
    *                                                                          *
    * > Replace "https://example.com" etc. with the URL(s) of your app.        *
    * > Be sure to use the right protocol!  ("http://" vs. "https://")         *
    *                                                                          *
    ***************************************************************************/
    onlyAllowOrigins: [
        "http://0.0.0.0"
    ],
...
 
  /**************************************************************************
  *                                                                         *
  * Lift the server on port 80.                                             *
  * (if deploying behind a proxy, or to a PaaS like Heroku or Deis, you     *
  * probably don't need to set a port here, because it is oftentimes        *
  * handled for you automatically.  If you are not sure if you need to set  *
  * this, just try deploying without setting it and see if it works.)       *
  *                                                                         *
  ***************************************************************************/
  port: 80,
...
And that should do it. Another
docker-compose up
and we are good to go.

Anker Lichten

Nun da unser Projekt in Docker läuft, können wir beginnen die API zu Program­mieren. Sails bietet dafür mehrere Optionen:
  • Blueprints
  • Actions
  • Controller
Blueprints
Blueprints bieten die Möglichkeit ohne jegliche Imple­men­tierung gewisse Standar­tak­tionen an unseren Daten­bank­models durch­zu­führen. Diese automa­ti­sierten Routen werden aus unseren Models generiert und bieten ein umfang­reiche REST API mit erwei­terten Konfi­gu­ra­tionen an. Die Verfüg­baren Routen sind unter Sails Blueprint actions zu finden. Im Rahmen unseres Projektes benötigen wir jedoch Routen die mittels POST / PUT / DELETE funktio­nieren sowie in mehreren Fällen andere Routen. Deshalb wurde hier gegen die Benutzung der Blueprints entschieden. Theore­tisch ist es jedoch möglich die Blueprint Routen und Aktionen anzupassen.
Actions
Actions sind prinzi­piell in einzelne Dateien aufge­teilte Routen. Der Gedanke hierbei ist, das manche Controller im Verlauf der Entwicklung zu groß und unüber­sichtlich werden. Durch die Action Dateien werden diese aufge­trennt und in unabhängige Funktionen geteilt. Da wir hier eine sehr überschaubare API bauen, werden wir auf die Actions verzichten und den klassi­schen Controller Weg gehen
Controller
Der klassische Controller. Eine Datei die alle Routen zu einem Model/Bereich beinhaltet.

Genera­toren

Egal für was wir uns entscheiden, sails liefert einen ganzen Satz an möglichen Genera­toren mit. Falls du einen Generator willst der dir nicht nur normale Sails Kompo­nenten genierert finden sich auch ein paar nützliche Community Genera­toren ( z.b. um Views nicht in .ejs sonder als React Compo­nente zu generieren ).

Da wir uns für die Benutzung von Controllern entschieden haben, bleiben uns zwei Optionen:

  • sails generate controller sails generate model: Bietet uns die Option direkt in der Console Aktionen im Controller und Felder im Model anzugeben.
  • sails generate api: Generiert ein Controller/Model paar

Der zweite Ansatz erlaubt es uns zwar beide Einträge mit einer Zeile zu definieren, jedoch nicht die Angabe von Feldern und Aktionen vorzu­nehmen. Wir verwenden daher die genera­toren für model + controller getrennt.

Starten wir mit dem User

cd backend/
sails generate controller User list add get edit delete
sails generate model User name email token

Werfen wir nun einen Blick in api/controllers so finden wir dort den UserController.js

/**
 * UserController
 *
 * @description :: Server-side actions for handling incoming requests.
 * @help        :: See https://sailsjs.com/docs/concepts/actions
 */
 
module.exports = {
   
 
  /**
   * `UserController.list()`
   */
  list: async function (req, res) {
    return res.json({
      todo: 'list() is not implemented yet!'
    });
  },
 
  /**
   * `UserController.add()`
   */
  add: async function (req, res) {
    return res.json({
      todo: 'add() is not implemented yet!'
    });
  },
 
  /**
   * `UserController.get()`
   */
  get: async function (req, res) {
    return res.json({
      todo: 'get() is not implemented yet!'
    });
  },
 
  /**
   * `UserController.edit()`
   */
  edit: async function (req, res) {
    return res.json({
      todo: 'edit() is not implemented yet!'
    });
  },
 
  /**
   * `UserController.delete()`
   */
  delete: async function (req, res) {
    return res.json({
      todo: 'delete() is not implemented yet!'
    });
  }
 
};

und unter api/models  das model User.js

/**
 * User.js
 *
 * @description :: A model definition represents a database table/collection.
 * @docs        :: https://sailsjs.com/docs/concepts/models-and-orm/models
 */
 
module.exports = {
 
  attributes: {
 
    //  ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦  ╦╔═╗╔═╗
    //  ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
    //  ╩  ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
 
    name: { type: 'string' },
 
    email: { type: 'string' },
 
    token: { type: 'string' }
 
    //  ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
    //  ║╣ ║║║╠╩╗║╣  ║║╚═╗
    //  ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
 
 
    //  ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
    //  ╠═╣╚═╗╚═╗║ ║║  ║╠═╣ ║ ║║ ║║║║╚═╗
    //  ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
 
  },
 
};

Durch die Angabe der benötigten Aktionen für den Controller und Felder für das Model müssen wir nur noch die Leerstellen auffüllen.

Analog generieren wir nun noch die restlichen Models und Controller

sails generate controller Comment add get edit delete list
sails generate model Comment message
sails generate controller Post add get edit delete list
sails generate model Post message


Damit erhalten wir alle benötigten JS Filtes für unsere API. Jetzt müssen wir nur noch unsere Routen auf die korrekte Aktion zeigen lassen. Dafür öffnen wir die Datei config/routes.js und tragen unter dem Block „API ENDPOINTS“ alle benötigten Routen ein, Das Schema lautet dabei ‚<request type> <route>’: ‚<controller>.<function>’

...
    //  ╔═╗╔═╗╦  ╔═╗╔╗╔╔╦╗╔═╗╔═╗╦╔╗╔╔╦╗╔═╗
    //  ╠═╣╠═╝║  ║╣ ║║║ ║║╠═╝║ ║║║║║ ║ ╚═╗
    //  ╩ ╩╩  ╩  ╚═╝╝╚╝═╩╝╩  ╚═╝╩╝╚╝ ╩ ╚═╝
 
    // User
    'post /api/users': 'UserController.add',
    'get /api/users/:id': 'UserController.get',
    'put /api/users/:id': 'UserController.edit',
    'delete /api/users/:id': 'UserController.delete',
    'get /api/users': 'UserController.list',
 
    // Post
    'post /api/posts': 'PostController.add',
    'get /api/posts/:id': 'PostController.get',
    'put /api/posts/:id': 'PostController.edit',
    'delete /api/posts/:id': 'PostController.delete',
    'get /api/posts': 'PostController.list',
 
    // Comments
    'post /api/posts/:postId/comments': 'CommentController.add',
    'get /api/posts/:postId/comments/:id': 'CommentController.get',
    'put /api/posts/:postId/comments/:id': 'CommentController.edit',
    'delete /api/posts/:postId/comments/:id': 'CommentController.delete',
    'get /api/posts/:postId/comments': 'CommentController.list',
 
    //  ╦ ╦╔═╗╔╗ ╦ ╦╔═╗╔═╗╦╔═╔═╗
    //  ║║║║╣ ╠╩╗╠═╣║ ║║ ║╠╩╗╚═╗
    //  ╚╩╝╚═╝╚═╝╩ ╩╚═╝╚═╝╩ ╩╚═╝
...
Anschließend sollten wir die Anwendung neu starten mittels
docker-compose restart
Öffnen wir nun die URL http://localhost/api/users im Browser/Postman/Curl so erhalten wir eine gültige Antwort vom Server und die API ist damit live. Nun können wir anfangen die Leerstellen zu füllen.

Segel setzen

Um die API zu vervoll­stän­digen sind nun noch eine Handvoll Schritte von Nöten. Wir müssen unsere Models um nicht-primitive Typen und Bezie­hungen erweitern, unsere Business Logik in den Controller eintragen und unsere Daten initia­li­sieren.

Model befüllen

Wir fügen nun zu unseren Models noch die benötigten Verknüp­fungen hinzu.

  • User: Keine Beziehung zu anderen Models
  • Post: 1 zu 1 Beziehung mit User für die Author Infor­mation
    Wir erweitern das Model um das Attribut author
    ...
        attributes: {
     
            //  ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦  ╦╔═╗╔═╗
            //  ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
            //  ╩  ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
            message: {type: 'string'},
     
            //  ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
            //  ║╣ ║║║╠╩╗║╣  ║║╚═╗
            //  ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
             
            //  ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
            //  ╠═╣╚═╗╚═╗║ ║║  ║╠═╣ ║ ║║ ║║║║╚═╗
            //  ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
            author: {
                model: 'User'
            }
    ...
    
  • Comments: 1 zu 1 Beziehung mit Post
    Analog zu User erweitern wir um das Attribut postId
    ...
      attributes: {
     
        //  ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦  ╦╔═╗╔═╗
        //  ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
        //  ╩  ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
     
        message: { type: 'string' },
     
        //  ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
        //  ║╣ ║║║╠╩╗║╣  ║║╚═╗
        //  ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
             
        //  ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
        //  ╠═╣╚═╗╚═╗║ ║║  ║╠═╣ ║ ║║ ║║║║╚═╗
        //  ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
        postId: {
          model: 'Post'
        },
    ...
    

Controller befüllen

Durch die Vorge­gebene Struktur füllen wir nun noch schnell die benötigten Aktionen.

api/controllers/UserController.js:

...
module.exports = {
 
    /**
     * `UserController.index()`
     */
    list: async function (req, res) {
        const users = await User.find({
            where: {},
            select: ['name', 'email']
        })
 
        return res.json({
            users: users
        })
    },
 
    /**
     * `UserController.create()`
     */
    add: async function (req, res) {
 
        const token = uuid();
 
        const createdUser = await User.create({
            name: req.body.name,
            email: req.body.email,
            token: token
        }).fetch();
 
        return res.json({
            id: createdUser.id,
            token: createdUser.token,
            name: createdUser.name,
            email: createdUser.email
        });
    },
 
    /**
     * `UserController.show()`
     */
    get: async function (req, res) {
        const foundUser = await User.findOne({
              id: req.params.id
        });
 
        return res.json({
            id: foundUser.id,
            name: foundUser.name,
            email: foundUser.email
        });
    },
 
    /**
     * `UserController.edit()`
     */
    edit: async function (req, res) {
        const updateUser = await User
            .update({id: req.params.id})
            .set({
                name: req.body.name,
                email: req.body.email
            })
            .fetch()
            .then( (users) =&amp;gt; users[0]);
 
        return res.json({
            id: updateUser.id,
            name: updateUser.name,
            email: updateUser.email
        });
    },
 
    /**
     * `UserController.delete()`
     */
    delete: async function (req, res) {
        await User.destroy({id: req.params.id});
 
        return res.ok();
    }
};

api/controllers/PostController.js:

...
module.exports = {
 
 
  /**
   * `PostController.add()`
   */
  add: async function (req, res) {
    const foundUser = await User.findOne({token: req.headers.token});
 
    if (!foundUser) return res.forbidden();
 
    const createdPost = await Post.create({
        message: req.body.message,
        author: foundUser.id,
    })
    .fetch()
    .then((post) =&amp;gt; Post.findOne({id: post.id}).populate('author'));
 
    return res.json(Post.toResJson(createdPost));
  },
 
  /**
   * `PostController.get()`
   */
  get: async function (req, res) {
    const foundPost = await Post.findOne({id: req.params.id})
        .populate('author');
 
 
      return res.json(Post.toResJson(foundPost));
  },
 
  /**
   * `PostController.edit()`
   */
  edit: async function (req, res) {
    const updatedPost = await Post.update({id: req.params.id})
        .set({
            message: req.body.message
        })
        .fetch()
        .then((post) =&amp;gt; Post.findOne({id: post[0].id}).populate('author'));
 
 
      return res.json(Post.toResJson(updatedPost));
  },
 
  /**
   * `PostController.delete()`
   */
  delete: async function (req, res) {
    await Post.destroy({id: req.params.id});
 
    return res.ok();
  },
 
  /**
   * `PostController.show()`
   */
  list: async function (req, res) {
    const listPosts = await Post.find()
        .populate('author')
        .then( (results) =&amp;gt; results.map((post) =&amp;gt; {return Post.toResJson(post)}));
 
    return res.json({
        posts: listPosts
    });
  }
 
};

api/controllers/CommentController.js

...
module.exports = {
 
 
    /**
     * `CommentController.add()`
     */
    add: async function (req, res) {
        const createdComment = await Comment.create({
            postId: req.params.postId,
            message: req.body.message,
        })
            .fetch()
 
        return res.json(Comment.toResJson(createdComment));
    },
 
    /**
     * `CommentController.get()`
     */
    get: async function (req, res) {
        const foundComment = await Comment.findOne({id: req.params.id});
 
        return res.json(Comment.toResJson(foundComment));
    },
 
    /**
     * `CommentController.edit()`
     */
    edit: async function (req, res) {
        const updatedComment = await Comment.update({id: req.params.id})
            .set({message: req.body.message})
            .fetch()
            .then( (results) =&amp;gt; results[0]);
 
        return res.json(Comment.toResJson(updatedComment));
    },
 
    /**
     * `CommentController.delete()`
     */
    delete: async function (req, res) {
        await Comment.destroy({id: req.params.id})
 
        return res.ok();
    },
 
    /**
     * `CommentController.list()`
     */
    list: async function (req, res) {
        const listComment = await Comment.find({postId: req.params.postId})
            .then( (results) =&amp;gt; results.map((comment) =&amp;gt; {return Comment.toResJson(comment)}));
 
        return res.json({
            postId: req.params.postId,
            comments: listComment
        })
    }
 
};
Bei genauerem Hinsehen fällt auf das im Post und Comment wir die Funktion Comment.toResJson(…)  benutzt haben. Diese existiert jedoch nicht. Wir können mit sails ORM Modellen jedoch einfach Funktionen auf den Models definieren. Um die Funktio­na­lität zu imple­men­tieren erweitern wir die Models api/models/Comment.js und api/models/Post.js und fügen am Ende folgendes Feld ein. Comment.js
...
    toResJson: function (comment) {
        return {
          id: comment.id,
          message: comment.message,
          postId: comment.postId.id ? comment.postId.id : comment.postId,
        }
    }
};
Post.js
...
    toResJson: function (post) {
        return {
            id: post.id,
            message: post.message,
            author: {
                id: post.author.id,
                name: post.author.name
            }
        }
    }
};

Daten initia­li­sieren

Da wir unsere Anwendung noch nie mit Models gestartet haben, befinden sich auch noch keine Daten in der Datenbank. Sails bietet eine automa­ti­sierte Migration an, solange die Anwendung nicht mit NODE_ENV=production läuft.

Schalten wir diese migration für die dev Umgebung ein und setzen das migrate flag:

config/model.js

...
  /***************************************************************************
  *                                                                          *
  * How and whether Sails will attempt to automatically rebuild the          *
  * tables/collections/etc. in your schema.                                  *
  *                                                                          *
  * &amp;amp;gt; Note that, when running in a production environment, this will be      *
  * &amp;amp;gt; automatically set to `migrate: 'safe'`, no matter what you configure   *
  * &amp;amp;gt; here.  This is a failsafe to prevent Sails from accidentally running   *
  * &amp;amp;gt; auto-migrations on your production database.                           *
  * &amp;amp;gt;                                                                        *
  * &amp;amp;gt; For more info, see:                                                    *
  * &amp;amp;gt; https://sailsjs.com/docs/concepts/orm/model-settings#?migrate         *
  *                                                                          *
  ***************************************************************************/
 
  migrate: 'alter',
...

Nun müssen wir unsere Anwendung stoppen

docker-compose down

und anschließend einmalig nicht mit NODE_ENV=production starten. Hierfür passen wir kurzerhand die package.json Datei an.

...
    &amp;quot;start&amp;quot;: &amp;quot;NODE_ENV=dev node app.js&amp;quot;,
...
und führen danach
docker-compose up
aus. Wir sollten in der Ausgabe folgende Zeilen sehen

Nun müssen wir die Änderung in package.json wieder rückgängig machen

...
    &amp;quot;start&amp;quot;: &amp;quot;NODE_ENV=production node app.js&amp;quot;,
...
und die Anwendung mit
docker-compose restart
neu starten.

Volle Fahrt voraus

Das wars. Nun können wir unsere Tests gegen die API laufen lassen. Hierfür benutzten wir die vorbe­reitete Postman collection um den Stand der Anwendung lokal zu verifi­zieren.

Im letzten Schritt müssen wir die Anwendung noch an die CI Umgebung anbinden um die Tests beim push mitlaufen zu lassen.

Nützliche Hinweise

Was hier nicht gemacht wurde, ist die Einrichtung eines hot-reload Systems. Dies lässt sich jedoch schmerzfrei mittel sails-hook-autoreload umsetzen, zumindest bis weitere plugins hinzu­kommen. Wer kein Interesse an einem inoffi­zi­ellem Plugin hat, der kann mittels nodemon/gulp-watch oder ähnlichem bei einer Dateiän­derung sails neu laden lassen.

Inter­essant ist auch die Möglichkeit sich direkt Views mittels Genera­toren zu erzeugen, e.g. ReactJs

 

Happy Sailing,

Maggistro