Siirry sisältöön

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,
  })

  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:

Luotaessa muistiinpano jonka kenttä content on liian lyhyt, seurauksena on virheilmoituksen sisältävä JSON

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:thepasswordishere@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 @eslint/js --save-dev

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

npx eslint --init

Vastaillaan kysymyksiin:

Vastataan kysymyksiin koodin luonteen mukaan, erityisesti että kyse ei ole TypeSriptistä, käytetään ' merkkijonoissa, ei käytetä ; rivien lopussa

Konfiguraatiot tallentuvat tiedostoon eslint.config.mjs:

import globals from 'globals'

export default [
  { files: ["**/*.js"], languageOptions: {sourceType: "commonjs"} },
  { languageOptions: { globals: globals.browser } },
]

Formatoidaan tiedostoa hieman:

// ...
export default [
  {
    files: ["**/*.js"],
    languageOptions: {
      sourceType: "commonjs",
      globals: {
        ...globals.node,
      },
      ecmaVersion: "latest",
    },
  },
]

files määrittelee, että ESLint tarkkailee projektin JavaScript-tiedostoja. languageOptions alla sourceType kertoo, että projektissa on käytössä commonjs-moduulijärjestelmä, ja globals.node taas määrittelee että projektissa käytetään NodeJS-ympäristön globaaleja muuttujia kuten process. Jos kyseessä olisi selaimessa suoritettava koodi, tulisi määritellä globals.browser sallimaan selainkohtaiset globaalit muuttujat, kuten window ja document.

Lopuksi ecmaVersion-ominaisuuden arvoksi asetetaan viimeisin JavaScriptin versio.

Haluamme käyttää ESLintin suosittelemia asetuksia omien asetustemme ohella. Asentamamme @eslint/js tarjoaa meille ennalta määritetyt asetukset ESLintille. Otetaan nämä käyttöön:

// ...
import js from '@eslint/js'
// ...

export default [
  js.configs.recommended,
  {
    // ...
  }
]

Rivi js.configs.recommended kannattaa laittaa konfiguraation alkuun ennen mahdollisia itse tehtäviä lisäkonfiguraatioita.

Asennetaan seuraavaksi liitännäinen @stylistic/eslint-plugin-js jonka avulla saamme käyttöömme joukon valmiiksi määriteltyjä ESlint-säntöjä:

npm install --save-dev @stylistic/eslint-plugin-js

Otetaan plugin käyttöön ja määritellään projektiin neljä sääntöä:

// ...
import stylisticJs from '@stylistic/eslint-plugin-js'

export default [
  {
    // ...
    plugins: {
      '@stylistic/js': stylisticJs
    },
    rules: {
      '@stylistic/js/indent': [
        'error',
        2
      ],
      '@stylistic/js/linebreak-style': [
        'error',
        'unix'
      ],
      '@stylistic/js/quotes': [
        'error',
        'single'
      ],
      '@stylistic/js/semi': [
        'error',
        'never'
      ],
    },
  },
]

Pluginit tarjoavat tavan laajentaa ESLintin toiminnallisuutta lisäämällä määrittelyjä jotka eivät ole mukana ESLint-ydinkirjastossa. Otimme nyt käyttöön pluginin @stylistic/eslint-plugin-js, joka tuo käyttöömme joukon JavaScriptin tyylisääntöjä joista otimme käyttöön sisennystä, rivinvaihtoa, lainausmerkkejä ja puolipisteitä koskevat säännöt.

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 dist oleva frontendin tuotantoversio tulee näin tarkastettua. Sitä emme kuitenkaan halua. Määritelläänkin konfiguraatioon hakemiston sisältö ignoroitavaksi:

// ...
export default [
  // ...
  { 
    ignores: ["dist/**"],
  },
  //...
]

Kun nyt suoritamme linttauksen, löytyy koodistamme jonkin verran huomautettavaa:

Lint kertoo kolmesta virheestä, kaikki muuttujia joille ei ole käyttöä

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:

Havainnollistus siitä miten VS code merkkaa rivit, joilla on eslint-tyylirike

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 eslint.config.mjs.

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.

export default [
  // ...
  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:

export default [
  // ...
  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ä:

// ...

export default [
  js.configs.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': 'off',
    },
  },
]

Kokonaisuudessaan konfiguraatiotiedosto näyttää seuraavalta:

import globals from "globals";
import stylisticJs from '@stylistic/eslint-plugin-js'
import js from '@eslint/js'

export default [
  js.configs.recommended,
  {
    files: ["**/*.js"],
    languageOptions: {
      sourceType: "commonjs",
      globals: {
        ...globals.node,
      },
      ecmaVersion: "latest",
    },
    plugins: {
      '@stylistic/js': stylisticJs
    },
    rules: {
      '@stylistic/js/indent': [
        'error',
        2
      ],
      '@stylistic/js/linebreak-style': [
        'error',
        'unix'
      ],
      '@stylistic/js/quotes': [
        'error',
        'single'
      ],
      '@stylistic/js/semi': [
        'error',
        'never'
      ],
      'eqeqeq': 'error',
      'no-trailing-spaces': 'error',
      'object-curly-spacing': [
        'error', 'always'
      ],
      'arrow-spacing': [
        'error', { 'before': true, 'after': true },
      ],
      'no-console': 'off',
    },
  },
  { 
    ignores: ["dist/**", "build/**"],
  },
]

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

Suoritetaan npm run lint...

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.