d

Validointi ja ESLint

Sovelluksen tietokantaan tallettamalle datan muodolle on usein tarve asettaa joitain ehtoja. Sovelluksemme ei hyväksy esim. muistiinpanoja, joiden sisältö eli content-kenttä puuttuu. Muistiinpanon oikeellisuus tarkastetaan sen luovassa metodissa:

app.post('/api/notes', (request, response) => {
  const body = request.body
  if (body.content === undefined) {    return response.status(400).json({ error: 'content missing' })  }
  // ...
})

Eli jos muistiinpanolla ei ole kenttää content, vastataan pyyntöön statuskoodilla 400 Bad Request.

Routejen tapahtumakäsittelijöissä tehtävää tarkastusta järkevämpi tapa tietokantaan talletettavan tiedon oikean muodon määrittelylle ja tarkastamiselle on Mongoosen validointitoiminnallisuuden käyttö.

Kullekin talletettavan datan kentälle voidaan määritellä validointisääntöjä skeemassa:

const noteSchema = new mongoose.Schema({
  content: {    type: String,    minlength: 5,    required: true  },  important: Boolean
})

Kentän content pituuden vaaditaan nyt olevan vähintään viisi merkkiä ja kentän arvo ei saa olla tyhjä. Kentälle important ei ole asetettu mitään ehtoa, joten se on määritelty edelleen yksinkertaisemmassa muodossa.

Esimerkissä käytetyt validaattorit minlength ja required ovat Mongooseen sisäänrakennettuja validointisääntöjä. Mongoosen custom validator -ominaisuus mahdollistaa mielivaltaisten validaattorien toteuttamisen, jos valmiiden joukosta ei löydy tarkoitukseen sopivaa.

Jos tietokantaan yritetään tallettaa validointisäännön rikkova olio, heittää tallennusoperaatio poikkeuksen. Muutetaan uuden muistiinpanon luomisesta huolehtivaa käsittelijää siten, että se välittää mahdollisen poikkeuksen virheenkäsittelijämiddlewaren huolehdittavaksi:

app.post('/api/notes', (request, response, next) => {  const body = request.body

  const note = new Note({
    content: body.content,
    important: body.important || false,
    date: new Date(),
  })

  note.save()
    .then(savedNote => {
      response.json(savedNote)
    })
    .catch(error => next(error))})

Laajennetaan virheenkäsittelijää huomioimaan validointivirheet:

const errorHandler = (error, request, response, next) => {
  console.error(error.message)

  if (error.name === 'CastError') {
    return response.status(400).send({ error: 'malformatted id' })
  } else if (error.name === 'ValidationError') {    return response.status(400).json({ error: error.message })  }

  next(error)
}

Validoinnin epäonnistuessa palautetaan validaattorin oletusarvoinen virheviesti:

fullstack content

Huomaamme kuitenkin että sovelluksessa on pieni ongelma, validaatiota ei suoriteta muistiinpanojen päivityksen yhteydessä. Dokumentaatio kertoo mistä on kyse, validaatiota ei suoriteta oletusarvoisesti metodin findOneAndUpdate suorituksen yhteydessä.

Korjaus on onneksi helppo. Muotoillaan routea muutenkin hieman siistimmäksi:

app.put('/api/notes/:id', (request, response, next) => {
  const { content, important } = request.body
  Note.findByIdAndUpdate(
    request.params.id, 
    { content, important },    { new: true, runValidators: true, context: 'query' }  ) 
    .then(updatedNote => {
      response.json(updatedNote)
    })
    .catch(error => next(error))
})

Tietokantaa käyttävän version vieminen tuotantoon

Sovelluksen pitäisi toimia tuotannossa eli Fly.io:ssa tai Renderissä lähes sellaisenaan. Frontendin muutosten takia on tehtävä siitä uusi tuotantoversio ja kopioitava se backendiin.

Huomaa, että vaikka määrittelimme sovelluskehitystä varten ympäristömuuttujille arvot tiedostossa .env, tietokantaurlin kertovan ympäristömuuttujan täytyy asettaa Fly.io:n tai Render vielä erikseen.

Fly.io:ssa komennolla fly secrets set:

fly secrets set MONGODB_URI='mongodb+srv://fullstack:<password>@cluster0.o1opl.mongodb.net/noteApp?retryWrites=true&w=majority'

Kun sovellus viedään tuotantoon, on hyvin tavanomaista että kaikki ei toimi odotusten mukaan. Esim. ensimmäinen tuotantoonvientiyritykseni päätyi seuraavaan:

fullstack content

Sovelluksessa ei toimi mikään.

Selaimen konsolin network-välilehti paljastaa että yritys muistiinpanojen hakemiseksi ei onnistu, pyyntö jää pitkäksi aikaa tilaan pending ja lopulta epäonnistuu HTTP statuskoodilla 502.

Selaimen konsolia on siis tarkasteltava koko ajan!

Myös palvelimen lokien seuraaminen on elintärkeää. Ongelman syy selviääkin heti kun katsomme komennolla fly logs mitä palvelimella tapahtuu:

fullstack content

Tietokannan osoite on siis undefined, eli komento fly secrets set MONGODB_URI oli unohtunut.

Renderiä käytettäessä tietokannan osoitteen kertova ympäristömuuttuja määritellään dashboardista käsin:

fullstack content

Renderiä käytettäessä sovelluksen lokia on mahdollista tarkastella Dashboardin kautta:

fullstack content

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan GitHubissa, branchissä part3-6.

Lint

Ennen osan lopetusta katsomme vielä nopeasti paitsioon jäänyttä tärkeää työkalua lintiä. Wikipedian sanoin:

Generically, lint or a linter is any tool that detects and flags errors in programming languages, including stylistic errors. The term lint-like behavior is sometimes applied to the process of flagging suspicious language usage. Lint-like tools generally perform static analysis of source code.

Staattisesti tyypitetyissä, käännettävissä kielissä (esim. Javassa) ohjelmointiympäristöt, kuten NetBeans osaavat huomautella monista koodiin liittyvistä asioista, sellaisistakin, jotka eivät ole välttämättä käännösvirheitä. Erilaisten staattisen analyysin lisätyökalujen, kuten checkstylen avulla voidaan vielä laajentaa Javassa huomautettavien asioiden määrää koskemaan koodin tyylillisiä seikkoja, esim. sisentämistä.

JavaScript-maailmassa tämän hetken johtava työkalu staattiseen analyysiin eli "linttaukseen" on ESLint.

Asennetaan ESLint backendiin kehitysaikaiseksi riippuvuudeksi:

npm install eslint --save-dev

Tämän jälkeen voidaan muodostaa alustava ESLint-konfiguraatio:

npx eslint --init

Vastaillaan kysymyksiin:

fullstack content

Konfiguraatiot tallentuvat tiedostoon .eslintrc.js:

module.exports = {
    'env': {
        'commonjs': true,
        'es2021': true,
        'node': true    },
    'extends': 'eslint:recommended',
    'parserOptions': {
        'ecmaVersion': 'latest'
    },
    'rules': {
        'indent': [
            'error',
            4
        ],
        'linebreak-style': [
            'error',
            'unix'
        ],
        'quotes': [
            'error',
            'single'
        ],
        'semi': [
            'error',
            'never'
        ]
    }
}

Tarkista, että tiedostossa on rivi 'node': true kuvanmukaisesti ja lisää se tarvittaessa.

Muutetaan heti konfiguraatioista sisennystä määrittelevä sääntö siten, että sisennystaso on kaksi välilyöntiä:

"indent": [
    "error",
    2
],

Esim tiedoston index.js tarkastus tapahtuu komennolla:

npx eslint index.js

Kannattaa ehkä tehdä linttaustakin varten npm-skripti:

{
  // ...
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    // ...
    "lint": "eslint ."  },
  // ...
}

Nyt komento npm run lint suorittaa tarkastukset koko projektille.

Myös hakemistossa build oleva frontendin tuotantoversio tulee näin tarkastettua. Sitä emme kuitenkaan halua, eli tehdään projektin juureen tiedosto .eslintignore ja sille seuraava sisältö:

build

Näin koko hakemiston build sisältö jätetään huomioimatta linttauksessa.

Lintillä on jonkin verran huomautettavaa koodistamme:

fullstack content

Ei kuitenkaan korjata ongelmia vielä.

Parempi vaihtoehto linttauksen suorittamiselle komentoriviltä on konfiguroida editorille eslint-plugin, joka suorittaa linttausta koko ajan. Näin pääset korjaamaan pienet virheet välittömästi. Tietoja esim. Visual Studion ESLint-pluginista on täällä.

VS Coden ESLint-plugin alleviivaa tyylisääntöjä rikkovat kohdat punaisella:

fullstack content

Näin ongelmat on helppo korjata koodiin heti.

Komento npm run lint -- --fix voi olla avuksi, jos koodissa on esim. useampia syntaksivirheitä.

ESLintille on määritelty suuri määrä sääntöjä, joita on helppo ottaa käyttöön muokkaamalla tiedostoa .eslintrc.js.

Otetaan käyttöön sääntö eqeqeq joka varoittaa, jos koodissa yhtäsuuruutta verrataan muuten kuin käyttämällä kolmea = -merkkiä. Sääntö lisätään konfiguraatiotiedostoon kentän rules alle.

{
  // ...
  'rules': {
    // ...
   'eqeqeq': 'error',
  },
}

Tehdään samalla muutama muukin muutos tarkastettaviin sääntöihin.

Estetään rivien lopussa olevat turhat välilyönnit, vaaditaan että aaltosulkeiden edessä/jälkeen on aina välilyönti ja vaaditaan myös konsistenttia välilyöntien käyttöä nuolifunktioiden parametrien suhteen:

{
  // ...
'rules': {
    // ...
    'eqeqeq': 'error',
    'no-trailing-spaces': 'error',
    'object-curly-spacing': [
        'error', 'always'
    ],
    'arrow-spacing': [
        'error', { 'before': true, 'after': true }
    ]
  },
}

Oletusarvoinen konfiguraatiomme ottaa käyttöön joukon valmiiksi määriteltyjä sääntöjä (eslint:recommended):

'extends': 'eslint:recommended',

Mukana on myös console.log-komennoista varoittava sääntö.

Yksittäinen sääntö on helppo kytkeä pois päältä määrittelemällä sen "arvoksi" konfiguraatiossa 0. Tehdään toistaiseksi näin säännölle no-console:

{
  // ...
  'rules': {
      // ...
      'eqeqeq': 'error',
      'no-trailing-spaces': 'error',
      'object-curly-spacing': [
          'error', 'always'
      ],
      'arrow-spacing': [
          'error', { 'before': true, 'after': true }
      ],
      'no-console': 0,    },
}

HUOM: Kun teet muutoksia tiedostoon .eslintrc.js, kannattaa muutosten jälkeen suorittaa linttaus komentoriviltä ja varmistaa, että konfiguraatio ei ole viallinen:

fullstack content

Jos konfiguraatiossa on jotain vikaa, voi editorin lint-plugin näyttää mitä sattuu.

Monissa yrityksissä on tapana määritellä yrityksen laajuiset koodausstandardit ja näiden käyttöä valvova ESLint-konfiguraatio. Pyörää ei kannata välttämättä keksiä uudelleen, ja voi olla hyvä idea ottaa omaan projektiin käyttöön joku jossain muualla hyväksi havaittu konfiguraatio. Viime aikoina monissa projekteissa on omaksuttu AirBnB:n JavaScript-tyyliohjeet ottamalla käyttöön firman määrittelemä ESLint-konfiguraatio.

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan GitHubissa, branchissa part3-7.