Poglavje 7 P3.2 Načrtovanje s Swagger in OpenAPI

P3.2

7.1 Dokumentiranje REST API-ja z OpenAPI specifikacijo

Za lažje testiranje aplikacije bomo obstoječi REST API (do tega trenutka smo razvili zgolj metode GET) opisali s pomočjo ogrodja Swagger in v skladu z OpenAPI specifikacijo.

Za podporo dokumentiranju API-ja bomo uporabili knjižnici swagger-jsdoc, s katero lahko dokumentiramo izvorno kodo v OpenAPI specifikaciji, in swagger-ui-express, ki nam omogoča generiranje uporabniškega vmesnika za pregled API dokumentacije ter specifikacije v obliki datoteke swagger.json.

Najprej namestimo omenjeni knjižnici.

$ npm install swagger-jsdoc swagger-ui-express

V začetno datoteko aplikacije server.js vključimo reference na omenjeni knjižnici in poskrbimo za začetne nastavitve, kjer v seznam dostopnih točk dodamo razvojni in produkcijski strežnik. Pomembna je tudi množica poti v okviru nastavitve apis, s katero navedemo datoteke izvorne kode, kjer se nahajajo metapodatki za izdelavo Swagger UI uporabniškega vmesnika pregleda dokumentacije.

server.js

/**
 * Load environment variables
 */
require("dotenv").config();
const express = require("express");
const path = require("path");
/**
 * Swagger and OpenAPI
 */
const swaggerJsDoc = require("swagger-jsdoc");
const swaggerUi = require("swagger-ui-express");
const swaggerDocument = swaggerJsDoc({
  definition: {
    openapi: "3.0.3",
    info: {
      title: "Demo",
      version: "0.1.0",
      description:
        "Demo **REST API** used for [Web Programming](https://teaching.lavbic.net/SP) course at [Faculty of Computer and Information Science](https://www.fri.uni-lj.si/en), [University of Ljubljana](https://www.uni-lj.si/eng) given by [Associate Professor Dejan Lavbič](https://www.lavbic.net)!\n\nThe application supports:\n* **filtering nearby** cultural herritage **locations**,\n* **adding comments** to existing locations,\n* and more.",
    },
    servers: [
      {
        url: "http://localhost:3000/api",
        description: "Development server for testing",
      },
      {
        url: "https://web-dev.fly.dev/api",
        description: "Production server",
      },
    ],
  },
  apis: ["./api/models/locations.js", "./api/controllers/*.js"],
});
/**
 * Database connection
 */
require("./api/models/db.js");

Nato omogočimo dostop do izvorne kode specifikacije v obliki Swagger datoteke na /api/swagger.json in pregled REST API dokumentacije na naslovu /api/docs/.

Opomba: Bodite pozorni, da poti opredelujemo na usmerjevalniku indexApi, ki ima predpono /api, zato poti zapisujemo relativno, in sicer /docs za pot /api/docs.

server.js

/**
 * API routing
 */
app.use("/api", apiRouter);
/**
 * Swagger file and explorer
 */
apiRouter.get("/swagger.json", (req, res) =>
  res.status(200).json(swaggerDocument)
);
apiRouter.use(
  "/docs",
  swaggerUi.serve,
  swaggerUi.setup(swaggerDocument, {
    customCss: ".swagger-ui .topbar { display: none }",
  })
);
/**
 * Start server
 */
app.listen(port, () => {
  console.log(
    `Demo app started in '${
      process.env.NODE_ENV || "development"
    } mode' listening on port ${port}!`
  );
});

Pri dokumentiranju metod REST API vmesnika je dobra praksa opredelitev skupnih vhodnih in izhodnih podatkov v obliki shem. Primerno mesto za dodajanje takšnih metapodatkov je v okviru opredelitve modela, saj imamo na tem mestu opredeljeno shemo podatkovne baze, do katere imajo uporabniki dostop preko REST API-ja.

OpenAPI specifikacijo lahko v izvorno kodo preprosto vključimo kar s pomočjo večvrstičnega JavaScript komentarja, ki ga začnemo z /** in končamo z */. Da bo generiranje dokumentacije po OpenAPI specifikaciji potekalo nemoteno, JavaScript komentar začnemo z rezervirano besedo @openapi, nato v YAML obliki navedemo potrebne metapodatke. Pri opisovanju REST API dostopne točke si lahko pomagamo z obširno Swagger dokumentacijo.

Namig: Dokumentacijo lahko zajamemo tudi s pomočjo urejevalnika Swagger Editor, ki omogoča urejanje, uvoz in izvoz v JSON in YAML obliki.

Nato za obe shemi (Comment in Location) pripravimo potrebne vhodne in izhodne oblike podatkov v okviru okviru components/schemas. Za vsak podatek opredelimo tip type (npr. object, array, string, boolean, number, integer itd.), kateri izmed atributov so obvezni (required), navedemo pa lahko tudi testne primere. Kompleksne podatkovne tipe lahko enostavno medsebojno povezujemo s pomočjo $ref, podobno kot smo to implementirali tudi na ravni podatkovne baze MongoDB.

api/models/locations.js

const mongoose = require("mongoose");
/**
 * @openapi
 * components:
 *  schemas:
 *   Comment:
 *    type: object
 *    description: Comment about cultural heritage location.
 *    properties:
 *     _id:
 *      type: string
 *      description: <b>unique identifier</b> of comment
 *      example: 635a62f5dc5d7968e68464be
 *     author:
 *      type: string
 *      description: <b>name of the author</b> of the comment
 *      example: Dejan Lavbič
 *     rating:
 *      type: integer
 *      description: <b>rating</b> of the location
 *      minimum: 0
 *      maximum: 5
 *      example: 5
 *     comment:
 *      type: string
 *      description: <b>comment</b> about the location
 *      example: Interesting location with only few tourists.
 *     createdOn:
 *      type: string
 *      description: <b>date</b> of the comment <b>creation</b>
 *      format: date-time
 *      example: 2020-12-25T17:43:00.000Z
 *    required:
 *     - _id
 *     - author
 *     - rating
 *     - comment
 *     - createdOn
 */
const commentSchema = new mongoose.Schema({
  author: { type: String, required: [true, "Author is required!"] },
  rating: {
    type: Number,
    required: [true, "Rating is required!"],
    min: 0,
    max: 5,
  },
  comment: { type: String, required: [true, "Comment is required!"] },
  createdOn: { type: Date, default: Date.now },
});
/**
 * @openapi
 * components:
 *  schemas:
 *   Location:
 *    type: object
 *    description: Cultural heritage <b>location</b>.
 *    properties:
 *     _id:
 *      type: string
 *      description: unique identifier
 *     name:
 *      type: string
 *      description: name of the location
 *     category:
 *      type: string
 *      description: category of the location
 *     type:
 *      type: string
 *      description: type of the location
 *     keywords:
 *      type: array
 *      description: keywords describing the location
 *      items:
 *       type: string
 *      minItems: 1
 *     description:
 *      type: string
 *      description: description of the location
 *     location:
 *      type: string
 *      description: textual description of actual location
 *     institution:
 *      type: string
 *      description: institution responsible for the location
 *     heritage:
 *      type: string
 *      description: heritage of the location
 *     municipality:
 *      type: string
 *      description: municipality of the location
 *     coordinates:
 *      type: array
 *      items:
 *       type: number
 *      minItems: 2
 *      maxItems: 2
 *      description: GPS coordinates of the location
 *     synonyms:
 *      type: array
 *      items:
 *       type: string
 *      description: synonyms of the location
 *      minItems: 1
 *     datation:
 *      type: string
 *      description: datation of the location
 *     authors:
 *      type: string
 *      description: authors of the cultural heritage
 *     fields:
 *      type: array
 *      items:
 *       type: string
 *      description: fields of the cultural heritage
 *      minItems: 1
 *     distance:
 *      type: number
 *      description: distance from the given location
 *      minimum: 0
 *     rating:
 *      type: integer
 *      description: average rating of the location
 *      minimum: 0
 *      maximum: 5
 *     comments:
 *      type: array
 *      items:
 *       $ref: '#/components/schemas/Comment'
 *    required:
 *     - _id
 *     - name
 *     - category
 *     - type
 *     - keywords
 *     - description
 *     - location
 *     - institution
 *     - heritage
 *     - municipality
 *     - coordinates
 *     - fields
 */
const locationSchema = mongoose.Schema({
  id: {
    type: Number,
    required: [true, "Unique identifier is required!"],
  },
  name: { type: String, required: [true, "Name is required!"] },
  synonyms: {
    type: [String],
    validate: {
      validator: (v) => v == undefined || (Array.isArray(v) && v.length > 0),
      message: "If synonyms are provided, they must be an array of strings!",
    },
  },
  category: { type: String, required: [true, "Category is required!"] },
  type: { type: String, required: [true, "Type is required!"] },
  keywords: {
    type: [String],
    validate: {
      validator: (v) => Array.isArray(v) && v.length > 0,
      message: "At least one keyword is required!",
    },
  },
  description: {
    type: String,
    required: [true, "Description is required!"],
  },
  datation: { type: String, required: false },
  authors: { type: String, required: false },
  location: {
    type: String,
    required: [true, "Location description is required!"],
  },
  fields: {
    type: [String],
    validate: {
      validator: (v) => Array.isArray(v) && v.length > 0,
      message: "At least one field is required!",
    },
  },
  institution: { type: String, required: [true, "Institution is required!"] },
  heritage: { type: String, required: [true, "Heritage is required!"] },
  municipality: { type: String, required: [true, "Municipality is required!"] },
  coordinates: {
    type: [Number],
    validate: {
      validator: (v) => Array.isArray(v) && v.length == 2,
      message: "Coordinates must be an array of two numbers!",
    },
    index: "2dsphere",
  },
  rating: { type: Number, default: 0, min: 0, max: 5 },
  comments: {
    type: [commentSchema],
    validate: {
      validator: (v) => v == undefined || (Array.isArray(v) && v.length > 0),
      message: "If comments are provided, they must be an array of comments!",
    },
  },
});
mongoose.model("Location", locationSchema, "Locations");

Poleg shem Location in Comment pa bomo potrebovali še podporni shemi Codelist in ErrorMessage, kjer prva opredeljuje dovoljene vrednosti šifrantov, druga pa strukturo sporočila o napaki. Ti shemi bomo dodali kar v začetno opredelitev Swagger dokumenta.

server.js

const swaggerJsDoc = require("swagger-jsdoc");
const swaggerUi = require("swagger-ui-express");
const swaggerDocument = swaggerJsDoc({
  definition: {
    openapi: "3.1.0",
    info: {
      ...
    },
    servers: [
      ...
    ],
    components: {
      schemas: {
        Codelist: {
          type: "string",
          description:
            "Allowed values for the codelist used in filtering locations.",
          enum: [
            "category",
            "type",
            "keywords",
            "institution",
            "municipality",
            "fields",
          ],
        },
        ErrorMessage: {
          type: "object",
          properties: {
            message: {
              type: "string",
              description: "Message describing the error.",
            },
          },
          required: ["message"],
        },
      },
    },
  },
  apis: ["./api/models/locations.js", "./api/controllers/*.js"],
});

Zaradi večjega števila metod, ki jih ponuja REST API strežnik, je s pomočjo tags smiselno opredeliti kategorije oz. oznake, kamor lahko kasneje uvrstimo posamezne funkcionalnosti. To opredelitev naših vsebinskih sklopov lokacij in komentarjev bomo dodali v začetno opredelitev Swagger dokumenta v datoteki server.js

server.js

const swaggerJsDoc = require("swagger-jsdoc");
const swaggerUi = require("swagger-ui-express");
const swaggerDocument = swaggerJsDoc({
  definition: {
    openapi: "3.1.0",
    info: {
      ...
    },
    tags: [
      {
        name: "Locations",
        description: "Cultural heritage <b>locations</b> in Slovenia.",
      },
      {
        name: "Comments",
        description: "<b>Comments</b> for cultural heritage locations in Slovenia.",
      }
    ],
    servers: [
      ...
    ],
    components: {
      schemas: {
        ...
      },
    },
  },
  apis: ["./api/models/locations.js", "./api/controllers/*.js"],
});

Najbolj pomemben del je dokumentacija posameznih REST API metod, kjer za vsako izmed njih navedemo pot (npr. /locations), HTTP metodo (npr. GET, POST, PUT ali DELETE), parametre (npr. query (npr. /locations/distance?nResults=2), path (npr. /locations/{locationId}), header, cookie), statuse odgovorov (npr. 200, 204, 400, 404, 500 itd.) in formate odgovorov (npr. application/json).

Pri opisovanju metod bomo opredelili tudi omejitve, ki izhajajo iz sheme v podatkovni bazi MongoDB. Zaloge vrednosti lahko omejimo z intervalom, kjer opredelimo minimum in maximum ali enum.

api/controllers/locations.js

const mongoose = require("mongoose");
const Location = mongoose.model("Location");
const allowedCodelists = [
  "category",
  "type",
  "keywords",
  "institution",
  "municipality",
  "fields",
];
/**
 * @openapi
 *  /locations/distance:
 *   get:
 *    summary: Retrieve locations within a given distance.
 *    description: Retrieve **cultural heritages within a given distance** from a given location.
 *    tags: [Locations]
 *    parameters:
 *     - name: lat
 *       in: query
 *       required: true
 *       description: <b>latitude</b> of the location
 *       schema:
 *        type: number
 *        minimum: -180
 *        maximum: 180
 *       example: 46.050129
 *     - name: lng
 *       in: query
 *       required: true
 *       description: <b>longitude</b> of the location
 *       schema:
 *        type: number
 *        minimum: -90
 *        maximum: 90
 *       example: 14.469027
 *     - name: distance
 *       in: query
 *       schema:
 *        type: number
 *        minimum: 0
 *        default: 5
 *       description: maximum <b>distance</b> in kilometers
 *     - name: nResults
 *       in: query
 *       schema:
 *        type: integer
 *        minimum: 1
 *        default: 10
 *       description: maximum <b>number of results</b>
 *    responses:
 *     '200':
 *      description: <b>OK</b>, with list of locations.
 *      content:
 *       application/json:
 *        schema:
 *         $ref: '#/components/schemas/Location'
 *        example:
 *         - _id: 635a62f5dc5d7968e68467e3
 *           name: Ljubljana - Cankarjeva spominska soba na Rožniku
 *           category: spominski objekti in kraji
 *           type: memorialna dediščina
 *           keywords: spominska soba
 *           description: Spominska soba, posvečena Ivanu Cankarju, je bila urejena leta 1948 in prenovljena v letih 1965 in 1998. Pisatelj je na Rožniku živel v letih 1910 - 1917, muzejsko postavitev je pripravil Mestni muzej Ljubljana.
 *           location: Spominska soba je urejena v stavbi, ki stoji nasproti gostilne Rožnik, na Cankarjevem vrhu (Rožnik).
 *           institution: ZVKD Ljubljana
 *           heritage: neznano
 *           municipality: LJUBLJANA
 *           coordinates: [14.4765778196, 46.0557820208]
 *           synonyms: [Cankarjev vrh]
 *           datation: 20. stol., 1948, 1965, 1998
 *           authors: Mestni muzej Ljubljana (arhitekt; 1948)
 *           fields: [zgodovina]
 *           distance: 858.0692052049162
 *     '400':
 *      description: <b>Bad Request</b>, with error message.
 *      content:
 *       application/json:
 *        schema:
 *         $ref: '#/components/schemas/ErrorMessage'
 *        example:
 *         message: "Query parameters 'lng' and 'lat' are required"
 *     '404':
 *      description: <b>Not Found</b>, with error message.
 *      content:
 *       application/json:
 *        schema:
 *         $ref: '#/components/schemas/ErrorMessage'
 *        example:
 *         message: "No locations found."
 *     '500':
 *      description: <b>Internal Server Error</b>, with error message.
 *      content:
 *       application/json:
 *        schema:
 *         $ref: '#/components/schemas/ErrorMessage'
 *        example:
 *         message: "geo near accepts just one argument when querying for a GeoJSON point. Extra field found: $maxDistance: 5000.0"
 */
const locationsListByDistance = async (req, res) => {
  let lng = parseFloat(req.query.lng);
  let lat = parseFloat(req.query.lat);
  let distance = parseFloat(req.query.maxDistance);
  distance = 1000 * (isNaN(distance) ? 5 : distance);
  let nResults = parseInt(req.query.nResults);
  nResults = isNaN(nResults) ? 10 : nResults;
  if (!lng || !lat)
    res
      .status(400)
      .json({ message: "Query parameters 'lng' and 'lat' are required." });
  else {
    try {
      let locations = await Location.aggregate([
        {
          $geoNear: {
            near: {
              type: "Point",
              coordinates: [lng, lat],
            },
            distanceField: "distance",
            spherical: true,
            maxDistance: distance,
          },
        },
        { $project: { comments: false, id: false } },
        { $limit: nResults },
      ]);
      if (!locations || locations.length == 0)
        res.status(404).json({ message: "No locations found." });
      else res.status(200).json(locations);
    } catch (err) {
      res.status(500).json({ message: err.message });
    }
  }
};
/**
 * @openapi
 * /locations/search:
 *   get:
 *    summary: Retrieve locations filtered by codelist values.
 *    description: Retrieve **cultural heritages limited with** selected **codelist's values**.
 *    tags: [Locations]
 *    parameters:
 *     - name: category
 *       in: query
 *       description: <b>category</b> of the location
 *       schema:
 *        type: string
 *        enum:
 *         - arheološka najdišča
 *         - drugi objekti in naprave
 *         - kulturna krajina
 *         - naselja in njihovi deli
 *         - ostalo,parki in vrtovi
 *         - spominski objekti in kraji
 *         - stavbe,stavbe s parki ali z vrtovi
 *     - name: type
 *       in: query
 *       description: <b>type</b> of the location
 *       schema:
 *        type: string
 *        enum:
 *         - arheološka dediščina
 *         - kulturna krajina
 *         - memorialna dediščina
 *         - naselbinska dediščina
 *         - ostalo,profana stavbna dediščina
 *         - sakralna stavbna dediščina
 *         - sakralno profana stavbna ded.
 *         - vrtnoarhitekturna dediščina
 *         - zgodovinska krajina
 *     - name: keywords
 *       in: query
 *       description: <b>keywords</b> of the location
 *       schema:
 *        type: string
 *        enum:
 *         - Božji grob
 *         - Brezmadežno spočetje Device Marije
 *         - Devica Marija
 *         - Devica Marija na Pesku
 *         - Devica Marija pomočnica
 *         - Devica Marija v Britofu
 *         - Ecce homo
 *         - Gospodov vnebohod
 *         - Ime Marijino
 *         - Kancianila in Prot
 *         - Kancijan
 *         - Karmelska Mati božja
 *         - Kristusovo učlovečenje
 *         - Lurška Mati božja
 *         - Marija Alietska
 *         - Marija Devica Tolažnica žalostnih
 *         - Marija Pomočnica
 *         - Marija Snežna
 *         - Marija sedem žalosti
 *         - Marija vnebovzeta
 *         - Marijino Vnebovzetje
 *         - Marijino obiskanje
 *         - Marijino obiskovanje
 *         - Marijino oznanjenje
 *         - Marijino prikazovanje
 *         - Marijino rojstvo
 *         - Marijino vnebovzetje
 *         - Mati božja
 *         - Mati božja Tolažnica žalostnih
 *         - Mati božja dobrega sveta
 *         - Poveličanje sv. Križa
 *         - Rojstvo Device Marije
 *         - Rožnovenska Mati božja
 *         - Srce Jezusovo
 *         - Vsi svetniki
 *         - alpinetum
 *         - ambient
 *         - apnenica
 *         - arboretum
 *         - arheološki park
 *         - arheološko najdišče
 *         - arheološko območje
 *         - arhitekturna plastika
 *         - arkade
 *         - arkadno dvorišče
 *         - bajta
 *         - baldahin
 *         - banka
 *         - baraka
 *         - bazen
 *         - bencinska črpalka
 *         - bolnišnica
 *         - botanični vrt
 *         - brežina
 *         - brunarica
 *         - bunker
 *         - castra
 *         - cerkev
 *         - cesta
 *         - cisterna
 *         - deblak
 *         - delavska hiša
 *         - delavska kolonija
 *         - delavski dom
 *         - depandansa
 *         - dimnica
 *         - dimnik
 *         - divji kostanj
 *         - dom pomembne osebnosti
 *         - dom za onemogle
 *         - domačija
 *         - domačija na ključ
 *         - domačija na vogel
 *         - donžon
 *         - doprsje
 *         - drevo
 *         - drevored
 *         - drvarnica
 *         - dvigalo
 *         - dvojni kozolec
 *         - dvojni kozolec*
 *         - dvor
 *         - dvorana
 *         - dvorec
 *         - dvorec s parkom
 *         - dvorec z vrtom
 *         - dvorišče
 *         - dvorišče
 *         - elektrarna
 *         - enojni kozolec
 *         - evangeličanska cerkev
 *         - faladur
 *         - figuralno znamenje
 *         - fotoatelje
 *         - frančiškanski samostan
 *         - freska
 *         - furmanska gostilna
 *         - fužinarska hiša
 *         - fužinarsko naselje
 *         - gaj
 *         - galerija
 *         - galerija na prostem
 *         - gank
 *         - garaža
 *         - garderoba
 *         - gartlc
 *         - gledališče
 *         - golobnjak
 *         - gomila
 *         - gomilno grobišče
 *         - gospodarsko poslopje
 *         - gostilna
 *         - gostinski vrt
 *         - gozdarska žičnica
 *         - grad
 *         - grad s parkom
 *         - grad z vrtom
 *         - gradišče
 *         - grajska kapela
 *         - grajska pristava
 *         - grajski vrt
 *         - graščina
 *         - grob
 *         - grob pomembne osebnosti
 *         - grobišče
 *         - grobna kapela
 *         - grobni oltar
 *         - grobnica
 *         - gručasta domačija
 *         - gručasta vas
 *         - hidroelektrarna
 *         - hiša
 *         - hišno drevo
 *         - hlev
 *         - hospic
 *         - hotel
 *         - hram
 *         - internat
 *         - inštitut
 *         - izvir
 *         - jahalnica
 *         - jama
 *         - jamska postojanka
 *         - jamsko najdišče
 *         - javni spomenik
 *         - javni vodnjak
 *         - jašek
 *         - jez
 *         - kajža
 *         - kal
 *         - kalvarija
 *         - kamnit most
 *         - kamniti križ
 *         - kamniti most
 *         - kamnolom
 *         - kanoniška hiša
 *         - kapela
 *         - kapelica
 *         - kaplanija
 *         - kartuzijanski samostan
 *         - kastel
 *         - kavarna
 *         - kaverna
 *         - kašča
 *         - kegljišče
 *         - kenotaf
 *         - kino
 *         - kip
 *         - klavnica
 *         - klet
 *         - kmetija
 *         - kmečka hiša
 *         - kmečki dvorec
 *         - knjižnica
 *         - kobilarna
 *         - kolišče
 *         - komenda
 *         - komunska kašča
 *         - komunski vodnjak
 *         - koncentracijsko taborišče
 *         - kondenzator
 *         - konjušnica
 *         - kopališče
 *         - koruznjak
 *         - kostnica
 *         - kovačija
 *         - kovnica
 *         - kozolec
 *         - kozolec na psa
 *         - kozolec s plaščem
 *         - koča
 *         - kraj zgodovinskega dogodka
 *         - kripta
 *         - križev pot
 *         - krstilnica
 *         - kuhinja z ognjiščem
 *         - kulturna krajina
 *         - kulturni dom
 *         - kužno znamenje
 *         - laboratorij
 *         - ladja
 *         - lapidarij
 *         - ledenica
 *         - lekarna
 *         - leseni zvonik
 *         - letalo
 *         - letni dvorec
 *         - letno gledališče
 *         - lipa
 *         - logarnica
 *         - lokacija
 *         - lopa
 *         - lovska postaja
 *         - lovski dvorec
 *         - loža
 *         - marof
 *         - mavzolej
 *         - mejnik
 *         - mestna hiša
 *         - mestna kašča
 *         - mestna palača
 *         - mestna vrata
 *         - mestna četrt
 *         - mestni grad
 *         - mestni park
 *         - mestno jedro
 *         - mestno obzidje
 *         - meščanska hiša
 *         - meščanska palača
 *         - mežnarija
 *         - miljnik
 *         - minoritski samostan
 *         - mitnica
 *         - mitrej
 *         - mizarska delavnica
 *         - mlin
 *         - mlinska hiša
 *         - mlinščica
 *         - molilnica
 *         - most
 *         - mostovž
 *         - mozaik
 *         - mrliška vežica
 *         - mulatiera
 *         - muzej
 *         - muzej na prostem
 *         - nagrobna plošča
 *         - nagrobnik
 *         - napajalno korito
 *         - narodni dom
 *         - naselbina
 *         - naselje
 *         - nekropola
 *         - nizki kozolec
 *         - niša
 *         - njiva
 *         - obcestna postaja
 *         - obcestna vas
 *         - obloženi grob
 *         - obrambni stolp
 *         - obrambni zid
 *         - obredni depo
 *         - obrtniška hiša
 *         - obzidje
 *         - ograja
 *         - oktogon
 *         - oljarna
 *         - oltar
 *         - opekarna
 *         - orgle
 *         - ovčja planina
 *         - palača
 *         - park
 *         - parkovna plastika
 *         - partizanska bolnišnica
 *         - partizanska tiskarna
 *         - pastirska bajta
 *         - paviljon
 *         - pašnik
 *         - pepelnica
 *         - perišče
 *         - peč
 *         - pil
 *         - pivovarna
 *         - planina
 *         - planinski dom
 *         - plano grobišče
 *         - planšarsko naselje
 *         - plavž
 *         - pod
 *         - podružnična cerkev
 *         - pokopališka cerkev
 *         - pokopališka kapela
 *         - pokopališki zid
 *         - pokopališče
 *         - portal
 *         - posamična najdba
 *         - poslikava
 *         - poslovna stavba
 *         - postaja
 *         - pot
 *         - počivališče
 *         - poštna postaja
 *         - pravoslavna cerkev
 *         - predmestje
 *         - predmestna hiša
 *         - predor
 *         - preužitkarska hiša
 *         - preša
 *         - pristanišče
 *         - pristava
 *         - proštija
 *         - rake
 *         - rastlinjak
 *         - razgledni stolp
 *         - razložena vas
 *         - razpelo
 *         - razstavišče
 *         - razvalina
 *         - refugij
 *         - relief
 *         - repnica
 *         - restavracija
 *         - ribnik
 *         - romarska cerkev
 *         - romarska hiša
 *         - romarsko središče
 *         - rotunda
 *         - rudarska hiša
 *         - rudnik
 *         - rudniška kapela
 *         - ruševina
 *         - sadovnjak
 *         - samostan
 *         - samostanska cerkev
 *         - samostanski vrt
 *         - sarkofag
 *         - senik
 *         - shramba
 *         - silos
 *         - sinagoga
 *         - sirarna
 *         - skedenj
 *         - skeletni pokop
 *         - skladišče
 *         - skrivni bunker
 *         - slopno znamenje
 *         - sodišče
 *         - sokolski dom
 *         - soline
 *         - solinska hiša
 *         - sončna ura
 *         - spahnjenca
 *         - spahnjenica
 *         - spominska hiša
 *         - spominska plošča
 *         - spominska soba
 *         - spominska tabla
 *         - spominski park
 *         - spominsko znamenje
 *         - sprehajalna pot
 *         - sramotilni steber
 *         - srednjeveško mesto
 *         - stadion
 *         - staja
 *         - stan
 *         - stanovanjska četrt
 *         - stanovanjski blok
 *         - stavba
 *         - steber
 *         - stebrno znamenje
 *         - stegnjena domačija
 *         - steklarska peč
 *         - steljnik
 *         - stolnica
 *         - stolp
 *         - stolpna ura
 *         - stolpnica
 *         - stopnišče
 *         - stražni stolp
 *         - strelišče
 *         - strelski jarek
 *         - stroj
 *         - strojnica
 *         - struga
 *         - suho stranišče
 *         - sušilnica
 *         - sušilnica za sadje
 *         - sv. Ahac
 *         - sv. Alojzij
 *         - sv. Ambrož
 *         - sv. Ana
 *         - sv. Andrej
 *         - sv. Anton Padovanski
 *         - sv. Avguštin
 *         - sv. Barbara
 *         - sv. Benedikt
 *         - sv. Bolfenk
 *         - sv. Boštjan
 *         - sv. Brikcij
 *         - sv. Ciril in Metod
 *         - sv. Danijel
 *         - sv. Donat
 *         - sv. Doroteja
 *         - sv. Družina
 *         - sv. Duh
 *         - sv. Egidij
 *         - sv. Elizabeta
 *         - sv. Fabijan
 *         - sv. Fabijan in Sebastijan
 *         - sv. Filip in Jakob
 *         - sv. Florijan
 *         - sv. Frančišek Asiški
 *         - sv. Frančišek Ksaverij
 *         - sv. Gervazij in Protazij
 *         - sv. Helena
 *         - sv. Ingenuin in Albuin
 *         - sv. Jakob
 *         - sv. Jakob starejši
 *         - sv. Janez Krstnik
 *         - sv. Janez Nepomuk
 *         - sv. Jedrt
 *         - sv. Jernej
 *         - sv. Jošt
 *         - sv. Jožef
 *         - sv. Jurij
 *         - sv. Just
 *         - sv. Kancij
 *         - sv. Kancijan
 *         - sv. Katarina
 *         - sv. Kozma in Damijan
 *         - sv. Križ
 *         - sv. Ladislav
 *         - sv. Lenart
 *         - sv. Lenart in sv. Rok
 *         - sv. Lovrenc
 *         - sv. Lucija
 *         - sv. Luka
 *         - sv. Marija
 *         - sv. Marija Zvezda
 *         - sv. Marjeta
 *         - sv. Marko
 *         - sv. Martin
 *         - sv. Matevž
 *         - sv. Matija
 *         - sv. Maver
 *         - sv. Mavricij
 *         - sv. Mihael
 *         - sv. Miklavž
 *         - sv. Mohor in Fortunat
 *         - sv. Neža
 *         - sv. Nikolaj
 *         - sv. Ožbolt
 *         - sv. Pavel
 *         - sv. Peter
 *         - sv. Peter in Pavel
 *         - sv. Primož
 *         - sv. Primož in Felicijan
 *         - sv. Radegunda
 *         - sv. Rok
 *         - sv. Rok in Sebastijan
 *         - sv. Rozalija
 *         - sv. Rupert
 *         - sv. Silvester
 *         - sv. Simon in Juda
 *         - sv. Simon in Juda Tadej
 *         - sv. Tilen
 *         - sv. Tomaž
 *         - sv. Trije kralji
 *         - sv. Trojica
 *         - sv. Urban
 *         - sv. Urh
 *         - sv. Uršula
 *         - sv. Valentin
 *         - sv. Vid
 *         - sv. Vladimir
 *         - sv. Volbenk
 *         - sv. Štefan
 *         - svete stopnice
 *         - svetišče
 *         - svinjak
 *         - tabor
 *         - talilna peč
 *         - talilnica
 *         - telovadnica
 *         - tempelj
 *         - terase
 *         - terme
 *         - termoelektrarna
 *         - tiskarna
 *         - toplar
 *         - tovarna
 *         - trafika
 *         - transformatorska postaja
 *         - travnik
 *         - trg
 *         - trgovina
 *         - trška hiša
 *         - trško jedro
 *         - trško naselje
 *         - tržnica
 *         - ulica
 *         - ulični otok
 *         - uta
 *         - utrdba
 *         - vas
 *         - vaško jedro
 *         - veleblagovnica
 *         - vila
 *         - villa rustica
 *         - viničarija
 *         - vinogradniški dvorec
 *         - vinogradniški zaselek
 *         - vinska klet
 *         - vislice
 *         - viteški nagrobnik
 *         - vodna žaga
 *         - vodni grad
 *         - vodni mlin
 *         - vodni stolp
 *         - vodni zbiralnik
 *         - vodnjak
 *         - vodomet
 *         - vojaški tabor
 *         - vojaško pokopališče
 *         - vojašnica
 *         - vratarnica
 *         - vrhhlevna hiša
 *         - vrhkletna hiša
 *         - vrt
 *         - vrtec
 *         - vrtna plastika
 *         - vrtnarija
 *         - vrtni rondo
 *         - vzporedna domačija
 *         - vzporedni kozolec
 *         - zaledna postojanka
 *         - zapor
 *         - zaporni zid
 *         - zapornica
 *         - zaprta domačija
 *         - zaselek
 *         - zbirka predmetov
 *         - zdraviliški objekt
 *         - zdraviliški park
 *         - zdravilišče
 *         - zidanica
 *         - zlati oltar
 *         - znamenje
 *         - znamenje z nišo
 *         - zvonik
 *         - zvončnica
 *         - čebelnjak
 *         - čolnarna
 *         - črna kuhinja
 *         - čuvajnica
 *         - šola
 *         - špital
 *         - Žalostna Mati božja
 *         - žaga
 *         - žara
 *         - železarna
 *         - železniška postaja
 *         - železniška proga
 *         - železobetonski most
 *         - žgani pokop
 *         - žitnica
 *         - živica
 *         - žičnica
 *         - župnijska cerkev
 *         - župnijsko središče
 *         - župnišče
 *     - name: institution
 *       in: query
 *       description: <b>institution</b> of the location
 *       schema:
 *        type: string
 *        enum:
 *         - ZVKD Celje
 *         - ZVKD Kranj
 *         - ZVKD Ljubljana
 *         - ZVKD Maribor
 *         - ZVKD Nova Gorica
 *         - ZVKD Novo mesto
 *         - ZVKD Piran
 *     - name: municipality
 *       in: query
 *       description: <b>municipality</b> of the location
 *       schema:
 *        type: string
 *        enum:
 *         - AJDOVŠČINA
 *         - ANKARAN
 *         - APAČE
 *         - BELTINCI
 *         - BENEDIKT
 *         - BISTRICA OB SOTLI
 *         - BLED
 *         - BOHINJ
 *         - BOROVNICA
 *         - BOVEC
 *         - BRASLOVČE
 *         - BRDA
 *         - BREZOVICA
 *         - BREŽICE
 *         - CANKOVA
 *         - CELJE
 *         - CERKLJE NA GORENJSKEM
 *         - CERKNICA
 *         - CERKNO
 *         - CERKVENJAK
 *         - CIRKULANE
 *         - DESTRNIK
 *         - DIVAČA
 *         - DOBJE
 *         - DOBREPOLJE
 *         - DOBRNA
 *         - DOBROVA-POLHOV GRADEC
 *         - DOBROVNIK
 *         - DOL PRI LJUBLJANI
 *         - DOLENJSKE TOPLICE
 *         - DOMŽALE
 *         - DORNAVA
 *         - DRAVOGRAD
 *         - DUPLEK
 *         - GORENJA VAS-POLJANE
 *         - GORIŠNICA
 *         - GORJE
 *         - GORNJA RADGONA
 *         - GORNJI GRAD
 *         - GORNJI PETROVCI
 *         - GRAD
 *         - GROSUPLJE
 *         - HAJDINA
 *         - HORJUL
 *         - HOČE-SLIVNICA
 *         - HRASTNIK
 *         - HRPELJE-KOZINA
 *         - IDRIJA
 *         - IG
 *         - ILIRSKA BISTRICA
 *         - IVANČNA GORICA
 *         - IZOLA
 *         - JESENICE
 *         - JEZERSKO
 *         - JURŠINCI
 *         - KAMNIK
 *         - KANAL
 *         - KIDRIČEVO
 *         - KOBARID
 *         - KOBILJE
 *         - KOMEN
 *         - KOMENDA
 *         - KOPER
 *         - KOSTANJEVICA NA KRKI
 *         - KOSTEL
 *         - KOZJE
 *         - KOČEVJE
 *         - KRANJ
 *         - KRANJSKA GORA
 *         - KRIŽEVCI
 *         - KRŠKO
 *         - KUNGOTA
 *         - KUZMA
 *         - LAŠKO
 *         - LENART
 *         - LENDAVA
 *         - LITIJA
 *         - LJUBLJANA
 *         - LJUBNO
 *         - LJUTOMER
 *         - LOGATEC
 *         - LOVRENC NA POHORJU
 *         - LOŠKA DOLINA
 *         - LOŠKI POTOK
 *         - LUKOVICA
 *         - LUČE
 *         - MAJŠPERK
 *         - MAKOLE
 *         - MARIBOR
 *         - MEDVODE
 *         - MENGEŠ
 *         - METLIKA
 *         - MEŽICA
 *         - MIKLAVŽ NA DRAVSKEM POLJU
 *         - MIREN-KOSTANJEVICA
 *         - MIRNA
 *         - MIRNA PEČ
 *         - MISLINJA
 *         - MOKRONOG-TREBELNO
 *         - MORAVSKE TOPLICE
 *         - MORAVČE
 *         - MOZIRJE
 *         - MURSKA SOBOTA
 *         - MUTA
 *         - NAKLO
 *         - NAZARJE
 *         - NOVA GORICA
 *         - NOVO MESTO
 *         - OPLOTNICA
 *         - ORMOŽ
 *         - OSILNICA
 *         - PESNICA
 *         - PIRAN
 *         - PIVKA
 *         - PODLEHNIK
 *         - PODVELKA
 *         - PODČETRTEK
 *         - POLJČANE
 *         - POLZELA
 *         - POSTOJNA
 *         - PREBOLD
 *         - PREDDVOR
 *         - PREVALJE
 *         - PTUJ
 *         - PUCONCI
 *         - RADENCI
 *         - RADEČE
 *         - RADLJE OB DRAVI
 *         - RADOVLJICA
 *         - RAVNE NA KOROŠKEM
 *         - RAZKRIŽJE
 *         - RAČE-FRAM
 *         - RENČE-VOGRSKO
 *         - REČICA OB SAVINJI
 *         - RIBNICA
 *         - RIBNICA NA POHORJU
 *         - ROGATEC
 *         - ROGAŠKA SLATINA
 *         - ROGAŠOVCI
 *         - RUŠE
 *         - SELNICA OB DRAVI
 *         - SEMIČ
 *         - SEVNICA
 *         - SEŽANA
 *         - SLOVENJ GRADEC
 *         - SLOVENSKA BISTRICA
 *         - SLOVENSKE KONJICE
 *         - SOLČAVA
 *         - SREDIŠČE OB DRAVI
 *         - STRAŽA
 *         - SV. TROJICA V SLOV. GORICAH
 *         - SVETA ANA
 *         - SVETI ANDRAŽ V SLOV. GORICAH
 *         - SVETI JURIJ OB ŠČAVNICI
 *         - SVETI JURIJ V SLOV. GORICAH
 *         - SVETI TOMAŽ
 *         - TIŠINA
 *         - TOLMIN
 *         - TRBOVLJE
 *         - TREBNJE
 *         - TRŽIČ
 *         - TURNIŠČE
 *         - VELENJE
 *         - VELIKE LAŠČE
 *         - VERŽEJ
 *         - VIDEM
 *         - VIPAVA
 *         - VITANJE
 *         - VODICE
 *         - VOJNIK
 *         - VRANSKO
 *         - VRHNIKA
 *         - VUZENICA
 *         - ZAGORJE OB SAVI
 *         - ZREČE
 *         - ČRENŠOVCI
 *         - ČRNA NA KOROŠKEM
 *         - ČRNOMELJ
 *         - ŠALOVCI
 *         - ŠEMPETER-VRTOJBA
 *         - ŠENTILJ
 *         - ŠENTJERNEJ
 *         - ŠENTJUR
 *         - ŠENTRUPERT
 *         - ŠENČUR
 *         - ŠKOCJAN
 *         - ŠKOFJA LOKA
 *         - ŠMARJE PRI JELŠAH
 *         - ŠMARJEŠKE TOPLICE
 *         - ŠMARTNO OB PAKI
 *         - ŠMARTNO PRI LITIJI
 *         - ŠOŠTANJ
 *         - ŠTORE
 *         - ŽALEC
 *         - ŽELEZNIKI
 *         - ŽETALE
 *         - ŽIRI
 *         - ŽIROVNICA
 *         - ŽUŽEMBERK
 *     - name: fields
 *       in: query
 *       description: <b>fields</b> of the location
 *       schema:
 *        type: string
 *        enum:
 *         - arheologija
 *         - dendrologija
 *         - etnologija
 *         - geomorfologija
 *         - gozdarstvo
 *         - krajinska arhitektura
 *         - tehniška zgodovina
 *         - tehniška zgodovina,
 *         - umetnostna zgodovina
 *         - urbanistična zgodovina
 *         - zgodovina
 *     - name: nResults
 *       in: query
 *       schema:
 *        type: integer
 *        minimum: 1
 *        default: 10
 *       description: maximum <b>number of results</b>
 *    responses:
 *     '200':
 *      description: <b>OK</b>, with list of locations.
 *      content:
 *       application/json:
 *        schema:
 *         $ref: '#/components/schemas/Location'
 *        example:
 *         - _id: 635a62f5dc5d7968e68467e3
 *           name: Ljubljana - Cankarjeva spominska soba na Rožniku
 *           category: spominski objekti in kraji
 *           type: memorialna dediščina
 *           keywords: spominska soba
 *           description: Spominska soba, posvečena Ivanu Cankarju, je bila urejena leta 1948 in prenovljena v letih 1965 in 1998. Pisatelj je na Rožniku živel v letih 1910 - 1917, muzejsko postavitev je pripravil Mestni muzej Ljubljana.
 *           location: Spominska soba je urejena v stavbi, ki stoji nasproti gostilne Rožnik, na Cankarjevem vrhu (Rožnik).
 *           institution: ZVKD Ljubljana
 *           heritage: neznano
 *           municipality: LJUBLJANA
 *           coordinates: [14.4765778196, 46.0557820208]
 *           synonyms: [Cankarjev vrh]
 *           datation: 20. stol., 1948, 1965, 1998
 *           authors: Mestni muzej Ljubljana (arhitekt; 1948)
 *           fields: [zgodovina]
 *     '404':
 *      description: <b>Not Found</b>, with error message.
 *      content:
 *       application/json:
 *        schema:
 *         $ref: '#/components/schemas/ErrorMessage'
 *        example:
 *         message: "No locations found."
 *     '500':
 *      description: <b>Internal Server Error</b>, with error message.
 *      content:
 *       application/json:
 *        schema:
 *         $ref: '#/components/schemas/ErrorMessage'
 *        example:
 *         message: "geo near accepts just one argument when querying for a GeoJSON point. Extra field found: $maxDistance: 5000.0"
 */
const locationsListByMultiFilter = async (req, res) => {
  let filter = [];
  // Exclude fields
  filter.push({ $project: { comments: false, id: false } });
  // Filter by codelist if provided
  allowedCodelists.forEach((codelist) => {
    let value = req.query[codelist];
    if (value) filter.push({ $match: { [codelist]: value } });
  });
  // Maximum number of results
  let nResults = parseInt(req.query.nResults);
  nResults = isNaN(nResults) ? 10 : nResults;
  filter.push({ $limit: nResults });
  // Perform database search and return results
  try {
    let locations = await Location.aggregate(filter).exec();
    if (!locations || locations.length === 0)
      res.status(404).json({ message: "No locations found." });
    else res.status(200).json(locations);
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
};
/**
 * @openapi
 * /locations/codelist/{codelist}:
 *  get:
 *   summary: Retrieve codelist values.
 *   description: Allowed **codelist values** for given codelist name.
 *   tags: [Locations]
 *   parameters:
 *    - name: codelist
 *      in: path
 *      required: true
 *      description: codelist <b>name</b>
 *      schema:
 *       $ref: '#/components/schemas/Codelist'
 *      example: category
 *   responses:
 *    '200':
 *     description: <b>OK</b>, with codelist values.
 *     content:
 *      application/json:
 *       schema:
 *        type: array
 *        items:
 *         type: string
 *       example: ["arheološka najdišča","drugi objekti in naprave","kulturna krajina","naselja in njihovi deli","ostalo","parki in vrtovi","spominski objekti in kraji","stavbe","stavbe s parki ali z vrtovi"]
 *    '400':
 *     description: <b>Bad Request</b>, with error message.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/ErrorMessage'
 *       examples:
 *        codelist not found:
 *         value:
 *          message: "No codelist found for 'custom category'."
 *        codelist not allowed:
 *         value:
 *          message: "Parameter 'codelist must be one of: category, type, keywords, institution, municipality, fields.'"
 *    '500':
 *     description: <b>Internal Server Error</b>, with error message.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/ErrorMessage'
 *       example:
 *        message: Database not available.
 */
const locationsListCodelist = async (req, res) => {
  let codelist = req.params.codelist;
  if (!allowedCodelists.includes(codelist))
    res.status(400).json({
      message: `Parameter 'codelist' must be one of: ${allowedCodelists.join(
        ", "
      )}`,
    });
  else {
    try {
      let codeListValues = await Location.distinct(codelist).exec();
      if (!codeListValues || codeListValues.length === 0)
        res
          .status(404)
          .json({ message: `No codelist found for '${codelist}.'` });
      else res.status(200).json(codeListValues);
    } catch (err) {
      res.status(500).json({ message: err.message });
    }
  }
};
/**
 * @openapi
 * /locations/{locationId}:
 *   get:
 *    summary: Retrieve details of a given location.
 *    description: Retrieve **cultural heritage details** for a given location.
 *    tags: [Locations]
 *    parameters:
 *    - name: locationId
 *      in: path
 *      required: true
 *      description: <b>unique identifier</b> of location
 *      schema:
 *       type: string
 *       pattern: '^[a-fA-F\d]{24}$'
 *      example: 635a62f5dc5d7968e68464c1
 *    responses:
 *     '200':
 *      description: <b>OK</b>, with location details.
 *      content:
 *       application/json:
 *        schema:
 *         $ref: '#/components/schemas/Location'
 *        example:
 *         _id: 635a62f5dc5d7968e68464c1
 *         name: Šentrupert na Dolenjskem - Muzej na prostem
 *         category: stavbe
 *         type: profana stavbna dediščina
 *         keywords: [muzej na prostem,kozolec na psa,kozolec s plaščem,enojni kozolec,nizki kozolec,toplar,vzporedni kozolec]
 *         description: Muzej na prostem sestavljajo skupina 17 kozolcev različnih tipov in dve enostavni sušilni napravi (belokranjska ostrv in ribniški kozouček). Postavitev iz 2012 prikazuje genezo kozolca na Slovenskem in raznolikost kozolcev v Mirnski dolini.
 *         location: Muzej na prostem je urejen na južnem obrobju Šentruperta na Dolenjskem.
 *         institution: ZVKD Novo mesto
 *         heritage: spomenik lokalnega pomena
 *         municipality: ŠENTRUPERT
 *         coordinates: [15.0914351743,45.9748324693]
 *         datation: zadnja četrtina 18. stol., 19. stol., prva polovica 20. stol., prva četrtina 21. stol., 2012
 *         fields: [etnologija,krajinska arhitektura,tehniška zgodovina]
 *         comments: []
 *     '404':
 *      description: <b>Not Found</b>, with error message.
 *      content:
 *       application/json:
 *        schema:
 *         $ref: '#/components/schemas/ErrorMessage'
 *        example:
 *         message: "Location with id '735a62f5dc5d7968e68464c1' not found."
 *     '500':
 *      description: <b>Internal Server Error</b>, with error message.
 *      content:
 *       application/json:
 *        schema:
 *         $ref: '#/components/schemas/ErrorMessage'
 *        example:
 *         message: "Cast to ObjectId failed for value \"1\" (type string) at path \"_id\" for model \"Location\""
 */
const locationsReadOne = async (req, res) => {
  try {
    let location = await Location.findById(req.params.locationId)
      .select("-id")
      .exec();
    if (!location)
      res.status(404).json({
        message: `Location with id '${req.params.locationId}' not found`,
      });
    else res.status(200).json(location);
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
};
const locationsUpdateOne = (req, res) => {
  res.status(200).json({ status: "OK" });
};
const locationsDeleteOne = (req, res) => {
  res.status(204).json({ status: "OK" });
};
module.exports = {
  locationsListByDistance,
  locationsListByMultiFilter,
  locationsListCodelist,
  locationsReadOne,
  locationsUpdateOne,
  locationsDeleteOne,
};

api/controllers/comments.js

const mongoose = require("mongoose");
const Location = mongoose.model("Location");
/**
 * @openapi
 * /locations/{locationId}/comments/{commentId}:
 *  get:
 *   summary: Retrieve a specific comment of a given location.
 *   description: Retrieve details of a **comment** of a given cultural heritage.
 *   tags: [Comments]
 *   parameters:
 *    - name: locationId
 *      in: path
 *      schema:
 *       type: string
 *       pattern: '^[a-fA-F\d]{24}$'
 *      required: true
 *      description: <b>unique identifier</b> of location
 *      example: 635a62f5dc5d7968e6846574
 *    - name: commentId
 *      in: path
 *      schema:
 *       type: string
 *       pattern: '^[a-fA-F\d]{24}$'
 *      required: true
 *      description: <b>unique identifier</b> of comment
 *      example: 635a62f5dc5d7968e68464be
 *   responses:
 *    '200':
 *     description: <b>OK</b>, with location details.
 *     content:
 *      application/json:
 *       schema:
 *        type: object
 *        properties:
 *         location:
 *          type: object
 *          properties:
 *           _id:
 *            type: string
 *            description: <b>unique identifier</b> of location
 *            example: 635a62f5dc5d7968e6846574
 *           name:
 *            type: string
 *            description: <b>name</b> of the location
 *            example: Celje - Celjski grad
 *         comment:
 *          $ref: '#/components/schemas/Comment'
 *    '404':
 *     description: <b>Not Found</b>, with error message.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/ErrorMessage'
 *       examples:
 *        location not found:
 *         value:
 *          message: "Location with id '73512fc5022f6bd9e0dfe53e' not found."
 *        comment not found:
 *         value:
 *          message: "Comment with id '1' not found."
 *        no comments found:
 *         value:
 *          message: "No comments found."
 *    '500':
 *     description: <b>Internal Server Error</b>, with error message.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/ErrorMessage'
 *       example:
 *        message: Database not available.
 */
const commentsReadOne = async (req, res) => {
  try {
    let location = await Location.findById(req.params.locationId)
      .select("name comments")
      .exec();
    if (!location)
      res.status(404).json({
        message: `Location with id '${req.params.locationId}' not found`,
      });
    else if (!location.comments || location.comments.length == 0)
      res.status(404).json({ message: "No comments found." });
    else {
      let comment = location.comments.id(req.params.commentId);
      if (!comment)
        res.status(404).json({
          message: `Comment with id '${req.params.commentId}' not found.`,
        });
      else {
        res.status(200).json({
          location: {
            _id: req.params.locationId,
            name: location.name,
          },
          comment,
        });
      }
    }
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
};
const commentsCreate = (req, res) => {
  res.status(201).json({ status: "OK" });
};
const commentsUpdateOne = (req, res) => {
  res.status(200).json({ status: "OK" });
};
const commentsDeleteOne = (req, res) => {
  res.status(204).json({ status: "OK" });
};
module.exports = {
  commentsCreate,
  commentsReadOne,
  commentsUpdateOne,
  commentsDeleteOne,
};

Rezultat dodanih metapodatkov v okviru dokumentiranja REST API strežnika je prikazan na sliki 7.1, ki se generira samodejno in je na voljo na /api/docs. V spodnjem delu slike je prikazan seznam shem oz. opredelitev vhodov in izhodov metod, ki so na voljo uporabnikom. Nato so v okviru kategorij (Locations in Comments) na voljo posamezne metode.

Uporabniški vmesnik Swagger UI za testiranje REST API-ja

Slika 7.1: Uporabniški vmesnik Swagger UI za testiranje REST API-ja

Če izberemo metodo /locations/{locationId} iz kategorije Locations na sliki 7.1, se prikažejo podrobnosti, ki so prikazane na sliki 7.2. V spodnjem delu slike so na voljo vsi možni rezultati klica metode (t.j. 200, 404 in 500), skupaj s primeri odgovorov. Če želimo metodo poklicati, moramo zgolj vnesti vrednost parametra locationId ter potrditi s klikom na gumb Try it out in rezultat zahteve se prikaže v okviru testnega odjemalca na sliki 7.2.

Primer testiranja metode `/locations/{locationId}` v okviru uporabniškega vmesnika Swagger UI

Slika 7.2: Primer testiranja metode /locations/{locationId} v okviru uporabniškega vmesnika Swagger UI

7.2 POST metode za dodajanje podatkov v MongoDB

Za dodajanje podatkov v MongoDB podatkovno bazo bomo uporabljali POST metode, ki v odgovoru vrnejo dodan dokument. V našem usmerjevalniku za REST API imamo opredeljeni 2 metodi, ki sta podrobneje povzeti v tabeli 7.1.

Ker v okviru naše aplikacije seznam lokacij vedno uvozimo iz registra kulturne dediščine, dodajanja novih lokacij preko REST API-ja ne bomo podprli, zato omenjene reference odstranimo, obdržimo zgolj metodo za dodajanje komentarjev.

api/routes/api.js

/**
 * Locations
 */
router.get("/locations/distance", ctrlLocations.locationsListByDistance);
router.get("/locations/search", ctrlLocations.locationsListByMultiFilter);
router.get(
  "/locations/codelist/:codelist",
  ctrlLocations.locationsListCodelist
);
router.post("/locations", ctrlLocations.locationsCreate);
router.get("/locations/:locationId", ctrlLocations.locationsReadOne);
router.put("/locations/:locationId", ctrlLocations.locationsUpdateOne);
router.delete("/locations/:locationId", ctrlLocations.locationsDeleteOne);
/**
 * Comments
 */
router.post("/locations/:locationId/comments", ctrlComments.commentsCreate);
router.get(
  "/locations/:locationId/comments/:commentId",
  ctrlComments.commentsReadOne
);
router.put(
  "/locations/:locationId/comments/:commentId",
  ctrlComments.commentsUpdateOne
);
router.delete(
  "/locations/:locationId/comments/:commentId",
  ctrlComments.commentsDeleteOne
);

api/controllers/location.js

const locationsListCodelist = (req, res) => {
  ...
};
const locationsCreate = (req, res) => {
  res.status(201).json({ status: "OK" });
};
...
const locationsReadOne = (req, res) => {
  ...
};
const locationsUpdateOne = (req, res) => {
  res.status(200).json({ status: "OK" });
};
const locationsDeleteOne = (req, res) => {
  res.status(204).json({ status: "OK" });
};
module.exports = {
  locationsListByDistance,
  locationsListByMultiFilter,
  locationsListCodelist,
  locationsCreate,
  locationsReadOne,
  locationsUpdateOne,
  locationsDeleteOne,
};
Tabela 7.1: POST metode v okviru demo aplikacije
Akcija Metoda URL naslov Parametri Primer
Kreiranje nove lokacije POST /locations http://<server>/api/locations
Kreiranje novega komentarja
(glej poglavje 7.2.2)
POST /locations/:locationId/comments locationId http://<server>/api/
locations/123/comments

POST metoda pri svojem delovanju uporabi posredovane podatke iz obrazca in jih shrani v podatkovno bazo. V izvorni kodi podobno, kot dostopamo do URL parametrov z req.params in do parametrov poizvedbe z req.query, dostopamo do posredovanih podatkov z req.body.

Podporo obvladovanju podatkov v telesu zahteve, ki jih strežniku preko spletnega obrazca (application/x-www-form-urlencoded) posredujemo v req.body, moramo v našo aplikacijo dodatno vključiti. Za ta namen bomo uporabili knjižnico body-parser.

$ npm install body-parser

server.js

/**
 * Load environment variables
 */
require("dotenv").config();
const express = require("express");
const path = require("path");
const bodyParser = require("body-parser");
...
/**
 * Static pages
 */
app.use(express.static(path.join(__dirname, "public")));
/**
 * Body parser (application/x-www-form-urlencoded)
 */
app.use(bodyParser.urlencoded({ extended: false }));

7.2.1 Dodajanje novih dokumentov v MongoDB

Pri dodajanju novih dokumentov moramo najprej dostopati do posredovanih podatkov in jih pretvoriti v ustrezen JavaScript objekt, ki ustreza shemi dokumenta (glej opredelitev sheme v datoteki api/models/locations.js).

Za dodajanje uporabimo metodo create, ki je na voljo na izbranem modelu. Po uspešnem dodajanju dokumenta v podatkovno bazo pa odjemalcu v povratnem klicu vrnemo ravnokar dodan dokument oz. sporočilo o napaki, če dodajanje ni bilo uspešno.

V našem primeru ne bomo neposredno dodajali novih dokumentov, saj ne bomo omogočili kreiranja novih lokacij, kot je zapisano v začetku poglavja 7.2, ampak si bomo v naslednjem poglavju 7.2.2 podrobneje pogledali dodajanje odvisnega dokumenta že obstoječemu dokumentu.

7.2.2 Dodajanje odvisnih dokumentov v MongoDB

Pri naši demo aplikaciji so odvisni dokumenti komentarji, ki jih shranimo v okviru nadrejenega dokumenta, tj. lokacija. Postopek dodajanja odvisnih dokumentov je naslednji:

  1. iskanje ustreznega nadrejenega dokumenta, ki predstavlja lokacijo,
  2. dodajanje novega odvisnega dokumenta s komentarjem in
  3. shranjevanje nadrejenega dokumenta z lokacijo, ki vsebuje tudi komentar.

Iskanje ustreznega nadrejenega dokumenta je preprosto z uporabo metode findById, nato s select izberemo zgolj komentarje in v primeru uspešnega odgovora podatkovne baze pokličemo funkcijo dodajKomentar.

app_api/controllers/comments.js

const commentsCreate = async (req, res) => {
  const { locationId } = req.params;
  if (!locationId)
    res
      .status(400)
      .json({ message: "Path parameter 'locationId' is required." });
  else {
    try {
      let location = await Location.findById(locationId)
        .select("comments")
        .exec();
      doAddComment(req, res, location);
      res.status(201).json({ status: "OK" });
    } catch (err) {
      res.status(500).json({ message: err.message });
    }
  }
};

Pri dejanskem dodajanju komentarja za lokacijo uporabimo metodo doAddComment, ki jo moramo še implementirati glede na shemo podatkov komentarja v modelu api/models/locations.js in imena vnosnih polj na obrazcu, ki ga bomo uporabili za vnos komentarjev.

api/models/locations.js

const commentSchema = new mongoose.Schema({
  author: { type: String, required: [true, "Author is required!"] },
  rating: {
    type: Number,
    required: [true, "Rating is required!"],
    min: 0,
    max: 5,
  },
  comment: { type: String, required: [true, "Comment is required!"] },
  createdOn: { type: Date, default: Date.now },
});

7.2.2.1 Dodajanje komentarja

Pri dodajanju komentarja najprej pripravimo JavaScript objekt komentarja, ki je skladen s shemo, kjer podatki prihajajo iz vnosnega obrazca. Komentarje dodamo na konec polja comments obstoječemu dokumentu location z metodo push. Nato lokacijo shranimo v podatkovno bazo, kjer moramo še najprej poskrbeti za posodobitev povprečne ocene lokacije in vračanje ravno dodanega komentarja (slice(-1) vrne zadnji element in pop() element odstrani ter ga vrne) v odgovoru zahteve.

api/controllers/comments.js

const doAddComment = async (req, res, location) => {
  if (!location)
    res.status(404).json({
      message: `Location with id '${req.params.locationId}' not found.`,
    });
  else if (!req.body.author || !req.body.rating || !req.body.comment)
    res.status(400).json({
      message: "Body parameters 'author', 'rating' and 'comment' are required.",
    });
  else {
    location.comments.push({
      author: req.body.author,
      rating: req.body.rating,
      comment: req.body.comment,
    });
    try {
      await location.save();
      await updateAverageRating(location._id);
      res.status(201).json(location.comments.slice(-1).pop());
    } catch (err) {
      res.status(500).json({ message: err.message });
    }
  }
};

7.2.2.2 Posodobitev povprečne ocene lokacije

Po vsakem dodajanju novega komentarja je treba na ravni lokacije posodobiti tudi povprečno oceno lokacije v okviru metode updateAverageRating, kjer najprej poiščemo ustrezno lokacijo, izberemo potrebne podatke in v primeru uspešnega odgovora podatkovne baze kličemo metodo doSetAverageRating, kjer bomo dejansko izvedli izračun povprečne ocene in jo shranili v podatkovno bazo.

api/controllers/comments.js

const updateAverageRating = async (locationId) => {
  try {
    let location = await Location.findById(locationId)
      .select("rating comments")
      .exec();
    doSetAverageRating(location);
  } catch (err) {}
};

Pri izračunu povprečne ocene lokacije se sprehodimo čez vse komentarje, izračunamo povprečno oceno ter jo shranimo v nadrejeni dokument lokacije.

api/controllers/comments.js

const doSetAverageRating = async (location) => {
  if (!location.comments || location.comments.length == 0) location.rating = 0;
  else {
    const count = location.comments.length;
    const total = location.comments.reduce((acc, { rating }) => {
      return acc + rating;
    }, 0);
    location.rating = parseInt(total / count, 10);
  }
  try {
    await location.save();
    console.log(`Average rating updated to ${location.rating}.`);
  } catch (err) {
    console.log(err);
  }
};

Opazimo lahko, da naša metoda doSetAverageRating ne vrača nobenega JSON odgovora, ker smo ga predhodno že poslali.

7.2.2.3 OpenAPI dokumentacija

Podobno, kot smo dodali OpenAPI dokumentacijo pri GET metodah, jo bomo tudi pri tej POST metodi, kjer je treba dodatno opredeliti še telo zahteve, ki ga pri GET zahtevah ni bilo.

api/controllers/comments.js

/**
 * @openapi
 * /locations/{locationId}/comments:
 *  post:
 *   summary: Add a new comment to a given location.
 *   description: Add a new **comment** with author's name, rating and comment's content to a location with given unique identifier.
 *   tags: [Comments]
 *   parameters:
 *    - name: locationId
 *      in: path
 *      schema:
 *       type: string
 *       pattern: '^[a-fA-F\d]{24}$'
 *      description: <b>unique identifier</b> of location
 *      required: true
 *      example: 635a62f5dc5d7968e6846914
 *   requestBody:
 *    description: Comment's author, rating and content.
 *    required: true
 *    content:
 *     application/x-www-form-urlencoded:
 *      schema:
 *       type: object
 *       properties:
 *        author:
 *         type: string
 *         description: <b>author</b> of comment
 *         example: Dejan Lavbič
 *        rating:
 *         type: integer
 *         description: <b>rating</b> of comment
 *         minimum: 1
 *         maximum: 5
 *         enum: [1, 2, 3, 4, 5]
 *         example: 3
 *        comment:
 *         type: string
 *         description: <b>content</b> of comment
 *         example: Srednja žalost.
 *       required:
 *        - author
 *        - rating
 *        - comment
 *   responses:
 *    '201':
 *     description: <b>Created</b>, with comment details.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/Comment'
 *    '400':
 *     description: <b>Bad Request</b>, with error message.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/ErrorMessage'
 *       examples:
 *        locationId is required:
 *         value:
 *          message: Path parameter 'locationId' is required.
 *        author, rating and comment are required:
 *         value:
 *          message: Body parameters 'author', 'rating' and 'comment' are required.
 *    '404':
 *     description: <b>Not Found</b>, with error message.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/ErrorMessage'
 *       example:
 *        message: Location with id '735a62f5dc5d7968e68467e3' not found.
 *    '500':
 *     description: <b>Internal Server Error</b>, with error message.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/ErrorMessage'
 *       example:
 *        message: Database not available.
 */
const commentsCreate = (req, res) => {
  const locationId = req.params.locationId;
  if (locationId) {
    Location.findById(locationId)
      .select("comments")
      .exec((err, location) => {
        if (err) res.status(500).json({ message: err.message });
        else doAddComment(req, res, location);
      });
  } else {
    res.status(400).json({
      message: "Path parameter 'locationId' is required.",
    });
  }
};

7.2.2.4 Testiranje dodajanja novega komentarja

Komentar bomo dodali obstoječi lokaciji Ljubljana - Viško pokopališče. Pred začetkom testiranja izbrišemo morebitne obstoječe komentarje omenjene lokacije in povprečno oceno lokacije ter nato dodamo nov komentar preko REST API klica.

Zaradi enostavnejšega testiranja bomo pridobili enolični identifikator zgoraj omenjene lokacije in ga shranili v okoljsko spremenljivko LOCATION_ID, saj ga bomo v nadaljevanju večkrat potrebovali.

Opomba: Pri klicu uporabimo bash ukaze za pridobitev ID-ja lokacije, ki v takšni obliki v okolju Windows ne delujejo, če nimate nameščene podpore za grep in sed. V vsakem primeru lahko spremenljivko LOCATION_ID nastavite tudi ročno.

Odjemalec

$ LOCATION_ID=$(docker exec -it web-dev-mongo-db bash -c "mongosh Demo --eval \
    'db.Locations.findOne(\
      {\"name\": \"Ljubljana - Viško pokopališče\"}, { _id: 1 })'"\
    | grep 'ObjectId' \
    | sed 's/.*"\(.*\)".*/\1/')
$ echo $LOCATION_ID
635a62f5dc5d7968e6846914
$ docker exec -it web-dev-mongo-db bash -c "mongosh Demo --eval \
    'db.Locations.updateOne(\
      {\"name\": \"Ljubljana - Viško pokopališče\"}, \
      { \$set: { comments: [] }, \$unset: { rating: \"\" }} \
    )'"
$ curl -X POST \
       "http://localhost:3000/api/locations/${LOCATION_ID}/comments" \
       -H "Content-Type: application/x-www-form-urlencoded" \
       -d "author=Dejan%20Lavbi%C4%8D&rating=3&comment=Srednja%20%C5%BEalost."
{
  "author": "Dejan Lavbič",
  "rating": 3,
  "comment": "Srednja žalost.",
  "_id": "63629ef623624d18e30f3746",
  "createdOn": "2022-11-02T16:46:46.523Z"
}

Ko dodamo 1. komentar z oceno \(3\), kot je prikazano v zgornjem primeru, je izračun povprečne ocene enostaven, saj povprečna ocena preprosto postane ocena ravno dodanega komentarja. V našem primeru je povprečna ocena lokacije v 1. koraku \(3\), kar se izpiše v konzoli strežnika.

Strežnik

Average rating updated to 3.

Po dodajanju 2. komentarja z oceno \(5\), kot je prikazano v naslednjem primeru, se zopet izvede izračun povprečne ocene in rezultat \(4\) se izpiše v konzoli strežnika.

Odjemalec

$ curl -X POST \
       "http://localhost:3000/api/locations/${LOCATION_ID}/comments" \
       -H "Content-Type: application/x-www-form-urlencoded" \
       -d "author=Jack%20the%20Ripper&rating=5&comment=My%20kind%20of%20place."
{
  "author": "Jack the Ripper",
  "rating": 5,
  "comment": "My kind of place.",
  "_id": "6362a62623624d18e30f3765",
  "createdOn": "2022-11-02T17:17:26.473Z"
}

Strežnik

Average rating updated to 3.
Average rating updated to 4.

7.3 PUT metode za ažuriranje podatkov v MongoDB

Za ažuriranje podatkov v MongoDB podatkovni bazi bomo uporabljali PUT metode, ki v odgovoru vrnejo spremenjen dokument. V našem usmerjevalniku za REST API imamo opredeljeni 2 metodi, ki sta podrobneje povzeti v tabeli 7.2.

Ker v okviru naše aplikacije seznam lokacij vedno uvozimo iz registra kulturne dediščine, spreminjanja obstoječih lokacij preko REST API-ja ne bomo podprli, zato omenjene reference odstranimo, obdržimo zgolj metodo za spreminjanje komentarjev:

api/routes/api.js

/**
 * Locations
 */
router.get("/locations/distance", ctrlLocations.locationsListByDistance);
router.get("/locations/search", ctrlLocations.locationsListByMultiFilter);
router.get(
  "/locations/codelist/:codelist",
  ctrlLocations.locationsListCodelist
);
router.get("/locations/:locationId", ctrlLocations.locationsReadOne);
router.put("/locations/:locationId", ctrlLocations.locationsUpdateOne);
router.delete("/locations/:locationId", ctrlLocations.locationsDeleteOne);
/**
 * Comments
 */
router.post("/locations/:locationId/comments", ctrlComments.commentsCreate);
router.get(
  "/locations/:locationId/comments/:commentId",
  ctrlComments.commentsReadOne
);
router.put(
  "/locations/:locationId/comments/:commentId",
  ctrlComments.commentsUpdateOne
);
router.delete(
  "/locations/:locationId/comments/:commentId",
  ctrlComments.commentsDeleteOne
);

api/controllers/locations.js

const locationsListByDistance = async (req, res) => {
  ...
};
...
const locationsListByMultiFilter = async (req, res) => {
  ...
};
...
const locationsListCodelist = async (req, res) => {
  ...
};
...
const locationsReadOne = async (req, res) => {
  ...
};
const locationsUpdateOne = (req, res) => {
  res.status(200).json({ status: "OK" });
};
const locationsDeleteOne = (req, res) => {
  res.status(204).json({ status: "OK" });
};
module.exports = {
  locationsListByDistance,
  locationsListByMultiFilter,
  locationsListCodelist,
  locationsReadOne,
  locationsUpdateOne,
  locationsDeleteOne,
};
Tabela 7.2: PUT metode v okviru demo aplikacije
Akcija Metoda URL naslov Parametri Primer
Posodobitev določene lokacije</i PUT /locations/:locationId locationId http://<server>/api/
locations/123
Posodobitev določenega komentarja
(glej poglavje 7.3.2)
PUT /locations/:locationId/
comments/:commentId
locationId, commentId http://<server>/api/
locations/123/comments/abc

7.3.1 Posodobitev obstoječega dokumenta v MongoDB

Postopek za posodobitev obstoječega dokumenta je naslednji:

  1. iskanje ustreznega dokumenta,
  2. posodobitev dokumenta,
  3. shranjevanje dokumenta in
  4. posredovanje JSON odgovora.

Pri posodabljanju dokumenta moramo podobno kot pri kreiranju novega dokumenta poskrbeti za skladnost s shemo dokumentov.

V našem primeru ne bomo neposredno spreminjali obstoječih dokumentov, ampak si bomo v naslednjem poglavju 7.3.2 podrobneje pogledali spreminjanje odvisnega dokumenta v okviru obstoječega dokumenta.

7.3.2 Posodobitev obstoječega odvisnega dokumenta v MongoDB

Pri posodobitvi obstoječega odvisnega dokumenta moramo tako kot pri kreiranju novega dokumenta upoštevati opredeljeno shemo dokumenta v modelu api/models/locations.js.

api/models/locations.js

const commentSchema = new mongoose.Schema({
  author: { type: String, required: [true, "Author is required!"] },
  rating: {
    type: Number,
    required: [true, "Rating is required!"],
    min: 0,
    max: 5,
  },
  comment: { type: String, required: [true, "Comment is required!"] },
  createdOn: { type: Date, default: Date.now },
});

Najprej poiščemo nadrejeni dokument, nato posodobimo izbrane podatke, shranimo posodobljeno lokacijo, kjer v povratnem klicu poskrbimo tudi za ponoven izračun povprečne ocene lokacije, če je slučajno prišlo do spremembe ocene komentarja.

api/controllers/comments.js

const commentsUpdateOne = async (req, res) => {
  if (!req.params.locationId || !req.params.commentId)
    res.status(400).json({
      message: "Path parameters 'locationId' and 'commentId' are required.",
    });
  else {
    try {
      let location = await Location.findById(req.params.locationId)
        .select("comments")
        .exec();
      if (!location)
        res.status(404).json({
          message: `Location with id '${req.params.locationId}' not found.`,
        });
      else {
        const comment = location.comments.id(req.params.commentId);
        if (!comment)
          res.status(404).json({
            message:
              "Comment with id '" + req.params.commentId + "' not found.",
          });
        else if (!req.body.rating && !req.body.comment) {
          res.status(400).json({
            message:
              "At least on of body parameters 'rating' or 'comment' is required.",
          });
        } else {
          if (req.body.rating) comment.rating = req.body.rating;
          if (req.body.comment) comment.comment = req.body.comment;
          await location.save();
          await updateAverageRating(location._id);
          res.status(200).json(comment { status: "OK" });
        }
      }
    } catch (err) {
      res.status(500).json({ message: err.message });
    }
  }
};

7.3.2.1 OpenAPI dokumentacija

Podobno, kot smo dodali OpenAPI dokumentacijo pri GET in POST metodah, jo bomo tudi pri tej PUT metodi.

api/controllers/comments.js

/**
 * @openapi
 * /locations/{locationId}/comments/{commentId}:
 *  put:
 *   summary: Change existing comment to a given location.
 *   description: Change existing **comment** with author's name, rating and comment's content (at least 1 value) to a location with given unique identifier.
 *   tags: [Comments]
 *   parameters:
 *    - name: locationId
 *      in: path
 *      schema:
 *       type: string
 *       pattern: '^[a-fA-F\d]{24}$'
 *      description: <b>unique identifier</b> of location
 *      required: true
 *      example: 635a62f5dc5d7968e6846914
 *    - name: commentId
 *      in: path
 *      schema:
 *       type: string
 *       pattern: '^[a-fA-F\d]{24}$'
 *      description: <b>unique identifier</b> of comment
 *      required: true
 *      example: 636346e7171e084ff4e4bbf9
 *   requestBody:
 *    description: Comment's author, rating and content.
 *    required: true
 *    content:
 *     application/x-www-form-urlencoded:
 *      schema:
 *       type: object
 *       properties:
 *        rating:
 *         type: integer
 *         description: <b>rating</b> of comment
 *         minimum: 1
 *         maximum: 5
 *         enum: [1, 2, 3, 4, 5]
 *         example: 3
 *        comment:
 *         type: string
 *         description: <b>content</b> of comment
 *         example: Srednja žalost.
 *   responses:
 *    '200':
 *     description: <b>OK</b>, with updated comment details.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/Comment'
 *    '400':
 *     description: <b>Bad Request</b>, with error message.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/ErrorMessage'
 *       examples:
 *        locationId and commentId are required:
 *         value:
 *          message: Path parameters 'locationId' and 'commentId' are required.
 *        rating or comment is required:
 *         value:
 *          message: At least on of body parameters 'rating' or 'comment' is required.
 *    '404':
 *     description: <b>Not Found</b>, with error message.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/ErrorMessage'
 *       examples:
 *        location not found:
 *         value:
 *          message: Location with id '735a62f5dc5d7968e6846914' not found.
 *        comment not found:
 *         value:
 *          message: Comment with id '736346e7171e084ff4e4bbf9' not found.
 *    '500':
 *     description: <b>Internal Server Error</b>, with error message.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/ErrorMessage'
 *       example:
 *        message: Database not available.
 */
const commentsUpdateOne = async (req, res) => {
  if (!req.params.locationId || !req.params.commentId)
    res.status(400).json({
      message: "Path parameters 'locationId' and 'commentId' are required.",
    });
  else {
    try {
      let location = await Location.findById(req.params.locationId)
        .select("comments")
        .exec();
      if (!location)
        res.status(404).json({
          message: `Location with id '${req.params.locationId}' not found.`,
        });
      else {
        const comment = location.comments.id(req.params.commentId);
        if (!comment)
          res.status(404).json({
            message:
              "Comment with id '" + req.params.commentId + "' not found.",
          });
        else if (!req.body.rating && !req.body.comment) {
          res.status(400).json({
            message:
              "At least on of body parameters 'rating' or 'comment' is required.",
          });
        } else {
          if (req.body.rating) comment.rating = req.body.rating;
          if (req.body.comment) comment.comment = req.body.comment;
          await location.save();
          await updateAverageRating(location._id);
          res.status(200).json(comment);
        }
      }
    } catch (err) {
      res.status(500).json({ message: err.message });
    }
  }
};

7.3.2.2 Testiranje ažuriranja obstoječega komentarja

Poleg LOCATION_ID sedaj potrebujemo še COMMENT_ID, ki ga pridobimo s poizvedbo v podatkovno bazo MongoDB.

$ COMMENT_ID=$(docker exec -it web-dev-mongo-db bash -c "mongosh Demo --eval \
    'db.Locations.findOne(\
      {\"name\": \"Ljubljana - Viško pokopališče\" }, \
      { _id: 0, comments: { \$elemMatch: { \"author\": \"Jack the Ripper\" }} })'" \
    | grep 'ObjectId' \
    | sed 's/.*"\(.*\)".*/\1/')
$ echo $COMMENT_ID
635a62f5dc5d7968e6846914

Primer posodobitve obstoječega komentarja avtorja Jack the Ripper je na spodnjem primeru, kjer spremenimo vrednost ocene s \(5\) na \(1\).

Odjemalec

$ curl -X PUT \
       "http://localhost:3000/api/locations/${LOCATION_ID}/comments/${COMMENT_ID}" \
       -H 'Content-Type: application/x-www-form-urlencoded' \
       -d "rating=1"
{
  "author": "Jack the Ripper",
  "rating": 1,
  "comment": "My kind of place.",
  "_id": "636346ff171e084ff4e4bc00",
  "createdOn": "2022-11-03T04:43:43.625Z"
}

Ta sprememba povzroči tudi posodobitev povprečne ocene lokacije, kar je prikazano v konzoli strežnika.

Strežnik

Average rating updated to 2.

7.4 DELETE metode za brisanje podatkov iz MongoDB

Za brisanje podatkov iz MongoDB podatkovne baze bomo uporabljali DELETE metode. V našem usmerjevalniku za REST API imamo opredeljeni 2 metodi, ki sta podrobneje povzeti v tabeli 7.3.

Ker v okviru naše aplikacije senzma lokacij vedno uvozimo iz registra kulturne dediščine, brisanja obstoječih lokacij preko REST API-ja ne bomo podprli, zato omenjene reference odstranimo, obdržimo zgolj metodo za brisanje komentarjev:

api/routes/api.js

/**
 * Locations
 */
router.get("/locations/distance", ctrlLocations.locationsListByDistance);
router.get("/locations/search", ctrlLocations.locationsListByMultiFilter);
router.get(
  "/locations/codelist/:codelist",
  ctrlLocations.locationsListCodelist
);
router.get("/locations/:locationId", ctrlLocations.locationsReadOne);
router.delete("/locations/:locationId", ctrlLocations.locationsDeleteOne);
/**
 * Comments
 */
router.post("/locations/:locationId/comments", ctrlComments.commentsCreate);
router.get(
  "/locations/:locationId/comments/:commentId",
  ctrlComments.commentsReadOne
);
router.put(
  "/locations/:locationId/comments/:commentId",
  ctrlComments.commentsUpdateOne
);
router.delete(
  "/locations/:locationId/comments/:commentId",
  ctrlComments.commentsDeleteOne
);

api/controllers/locations.js

const locationsListByDistance = (req, res) => {
  ...
};
...
const locationsListByMultiFilter = (req, res) => {
  ...
};
...
const locationsListCodelist = (req, res) => {
  ...
};
...
const locationsReadOne = (req, res) => {
  ...
};
const locationsDeleteOne = (req, res) => {
  res.status(204).json({ status: "OK" });
};
module.exports = {
  locationsListByDistance,
  locationsListByMultiFilter,
  locationsListCodelist,
  locationsReadOne,
  locationsDeleteOne,
};
Tabela 7.3: DELETE metode v okviru demo aplikacije
Akcija Metoda URL naslov Parametri Primer
Brisanje določene lokacije DELETE /locations/:locationId locationId http://<server>/api/
locations/123
Brisanje določenega komentarja
(glej poglavje 7.4.1)
DELETE /location/:locationId/
comments/:commentId
locationId, commentId http://<server>/api/
locations/123/comments/abc

7.4.1 Brisanje obstoječega komentarja

Pri brisanju odvisnega dokumenta je postopek nekoliko drugačen kot pri brisanju samostojnega dokumenta, saj vse operacije izvajamo z vidika nadrejenega dokumenta, tj. lokacije. Najprej poiščemo ustrezen nadrejeni element lokacije, nato poiščemo izbrani odvisni dokument komentarja in ga hkrati izbrišemo. Na koncu je potrebno še posodobiti nadrejeni element lokacije, tako da jo shranimo, kjer je potrebno poskrbeti tudi za posodobitev povprečne ocene lokacije.

api/controllers/comments.js

const commentsDeleteOne = async (req, res) => {
  const { locationId, commentId } = req.params;
  if (!locationId || !commentId)
    res.status(400).json({
      message: "Path parameters 'locationId' and 'commentId' are required.",
    });
  else {
    try {
      let location = await Location.findById(locationId)
        .select("comments")
        .exec();
      if (!location)
        res.status(404).json({
          message: `Location with id '${locationId}' not found.`,
        });
      else if (location.comments && location.comments.length > 0) {
        const comment = location.comments.id(commentId);
        if (!comment)
          res.status(404).json({
            message: `Comment with id '${commentId}' not found.`,
          });
        else {
          comment.deleteOne();
          await location.save();
          await updateAverageRating(location._id);
          res.status(204).send().json({ status: "OK" });
        }
      } else res.status(404).json({ message: "No comments found." });
    } catch (err) {
      res.status(500).json({ message: err.message });
    }
  }
};

7.4.1.1 OpenAPI dokumentacija

Podobno, kot smo dodali OpenAPI dokumentacijo pri GET, POST in PUT metodah, jo bomo tudi pri tej DELETE metodi.

api/controllers/comments.js

/**
 * @openapi
 * /locations/{locationId}/comments/{commentId}:
 *  delete:
 *   summary: Delete existing comment from a given location.
 *   description: Delete existing **comment** with a given unique identifier from a location with given unique identifier.
 *   tags: [Comments]
 *   parameters:
 *    - name: locationId
 *      in: path
 *      schema:
 *       type: string
 *       pattern: '^[a-fA-F\d]{24}$'
 *      description: <b>unique identifier</b> of location
 *      required: true
 *      example: 635a62f5dc5d7968e6846914
 *    - name: commentId
 *      in: path
 *      schema:
 *       type: string
 *       pattern: '^[a-fA-F\d]{24}$'
 *      description: <b>unique identifier</b> of comment
 *      required: true
 *      example: 636346e7171e084ff4e4bbf9
 *   responses:
 *    '204':
 *     description: <b>No Content</b>, with no content.
 *    '400':
 *     description: <b>Bad Request</b>, with error message.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/ErrorMessage'
 *       example:
 *        message: Path parameters 'locationId' and 'commentId' are required.
 *    '404':
 *     description: <b>Not Found</b>, with error message.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/ErrorMessage'
 *       examples:
 *        location not found:
 *         value:
 *          message: Location with id '735a62f5dc5d7968e6846914' not found.
 *        comment not found:
 *         value:
 *          message: Comment with id '736346e7171e084ff4e4bbf9' not found.
 *        no comments found:
 *         value:
 *          message: No comments found.
 *    '500':
 *     description: <b>Internal Server Error</b>, with error message.
 *     content:
 *      application/json:
 *       schema:
 *        $ref: '#/components/schemas/ErrorMessage'
 *       example:
 *        message: Database not available.
 */
const commentsDeleteOne = async (req, res) => {
  const { locationId, commentId } = req.params;
  if (!locationId || !commentId)
    res.status(400).json({
      message: "Path parameters 'locationId' and 'commentId' are required.",
    });
  else {
    try {
      let location = await Location.findById(locationId)
        .select("comments")
        .exec();
      if (!location)
        res.status(404).json({
          message: `Location with id '${locationId}' not found.`,
        });
      else if (location.comments && location.comments.length > 0) {
        const comment = location.comments.id(commentId);
        if (!comment)
          res.status(404).json({
            message: `Comment with id '${commentId}' not found.`,
          });
        else {
          comment.deleteOne();
          await location.save();
          await updateAverageRating(location._id);
          res.status(204).send() /*.json({ status: "OK" })*/;
        }
      } else res.status(404).json({ message: "No comments found." });
    } catch (err) {
      res.status(500).json({ message: err.message });
    }
  }
};

7.4.1.2 Testiranje brisanja obstoječega komentarja

V nadaljevanju je prikazano brisanje obstoječega komentarja avtorja Jack the Riper.

Odjemalec

$ curl -X DELETE \
       "http://localhost:3000/api/locations/${LOCATION_ID}/comments/${COMMENT_ID}"

Ta sprememba povzroči tudi spremembo povprečne ocene lokacije, kar je prikazano v konzoli strežnika.

Strežnik

Average rating updated to 3.

7.4.2 Brisanje dokumentov

Zaradi načina delovanje naše aplikacije brisanja lokacij ne bomo podprli. V okviru MongoDB podatkovne baze imamo za to funkcionalnost na voljo metodo findByIdAndRemove, ki najprej poišče dokument s podanim enoličnim identifikatorjem in ga nato izbriše.

7.5 Uporaba REST API-ja v Express aplikaciji

Do podatkovne baze imamo pripravljen REST API vmesnik, ki ga sedaj lahko vključimo v našo aplikacijo, kot to prikazuje slika 7.3.

V nadaljevanju bomo s pomočjo Express in Node.js dostopali do REST API za dostop do MongoDB podatkovne baze

Slika 7.3: V nadaljevanju bomo s pomočjo Express in Node.js dostopali do REST API za dostop do MongoDB podatkovne baze

Aplikacije na strani strežnika s pomočjo predlog Handlebars ne bomo gradili, saj bomo v nadaljevanju začeli z izdelavo aplikacije Angular na strani odjemalca. Na tem mestu bomo zgolj demonstrirali omejeno funkcionalnost prikaza lokacij, kjer bomo na strani strežnika dostopali do podatkov razvitih REST API metod.

7.5.1 Priprava na API klic iz Express aplikacije

Pri dostopu do REST storitve si bomo pomagali s knjižnico axios, ki jo namestimo in njeno odvisnost vključimo v package.json.

$ npm install --save axios

Nato bomo implementirali spremembe v krmilniku locations.js na strani Express strežnika. Najprej poskrbimo za ustrezen naslov REST API strežnika, kjer upoštevamo razvojno oz. produkcijsko okolje. V nadaljevanju pa nastavimo še privzeti URL naslov knjižnici axios ter nastavimo privzeti časovni potek zahteve, ki je v našem primeru 5s.

hbs/controllers/locations.js

const apiServer =
  process.env.NODE_ENV === "production"
    ? "https://web-dev.fly.dev"
    : "http://localhost:" + (process.env.PORT || 3000);
const axios = require("axios").create({ baseURL: apiServer, timeout: 5000 });
const list = (req, res) => {
  res.render("index", { title: "List of cultural heritages!" });
}

7.5.2 Pridobitev seznama lokacij preko API-ja

Naš trenutni cilj je, da podatke pridobimo s klicem API-ja, ki dostopa do podatkovne baze in nam jih podatke vrne. Uporabimo lahko REST API metodo /locations/distance, ki nam vrne najbližje lokacije glede na podano izhodišče, ki ga podamo v obliki parametrov GPS koordinat.

hbs/controllers/locations.js

const list = (req, res) => {
  let location = { lng: 14.469027, lat: 46.050129 }; // Ljubljana
  let data = [];
  axios
    .get("/api/locations/distance", {
      params: { lng: location.lng, lat: location.lat, nResults: 5 },
    })
    .then((apiRes) => (data = apiRes.data))
    .catch((err) => (title = err.message + "!"))
    .finally(() => res.render("index", { title: "List of cultoral heritages" }));
};

7.5.3 Priprava novega pogleda seznama

Podatki so preneseni iz modela v krmilnik in so na voljo v spremenljivki data, vendar jih pri generiranju pogleda še ne uporabimo. V nadaljevanju bomo tako pripravili nov pogled list.hbs, ki bo pogleg parametra title, pričakoval še vhodni parameter locations, kjer bodo v obliki tabele shranjene pridobljene lokacije iz podatkovne baze.

Pogled je HTML predloga, ki s pomočjo Bootstrap elementov prikaže lokacije v obliki seznama z določenimi izbranimi podatki, ki jih pridobimo v vhodnem parametru locations.

hbs/views/list.hbs

<h1>{{title}}</h1>
<div class="row">
  {{#each locations as |location|}}
    <div class="col-12 col-xxl-6">
      <div class="card mt-4">
        <div class="card-header">
          <div class="d-flex justify-content-between align-items-center">
            <div>
              <h3 class="card-title mt-1 mb-1">{{location.name}}</h3>
              {{#each synonyms as |synonym|}}
                {{synonym}}{{#unless @last}},{{/unless}}
              {{/each}}
            </div>
            <div class="text-nowrap ps-3">{{location.rating}}</div>
          </div>
        </div>
        <div class="card-body">
          <div class="d-flex justify-content-between align-items-center">
            <div><h6
                class="card-subtitle mb-1 text-muted"
              >{{location.type}}</h6>
              <p class="card-text">{{location.description}}</p></div>
            <div class="text-nowrap ps-3"><span
                class="badge rounded-pill text-bg-dark"
              >{{location.distance}}</span></div>
          </div>
        </div>
        <div class="card-footer text-muted">
          {{#each fields as |field|}}
            <span class="badge text-bg-secondary">{{field}}</span>
          {{/each}}
          {{#each keywords as |keyword|}}
            <span class="badge text-bg-dark">{{keyword}}</span>
          {{/each}}
        </div>
      </div>
    </div>
  {{/each}}
</div>

Sedaj mora metoda krmilnika omenjen pogled poklicati in mu posredovati pridobljene podatke iz podatkovne baze.

hbs/controllers/location.js

const list= (req, res) => {
  let location = { lng: 14.469027, lat: 46.050129 }; // Ljubljana
  let data = [];
  let title = "List of cultural heritages";
  axios
    .get("/api/locations/distance", {
      params: { lng: location.lng, lat: location.lat, nResults: 5 },
    })
    .then((apiRes) => (data = apiRes.data))
    .catch((err) => (title = err.message + "!"))
    .finally(() => res.render("list index", { title: "List of cultural heritages", locations: data }));
};

Slika 7.4 prikazuje začetni seznam lokacij, ki vsebuje vse željene podatke:

  • naslov, sinonimi in povprečna ocena lokacije (če je na voljo),
  • tip in opis lokacije,
  • področja in ključne besede, povezane z lokacijo.
Začetni seznam lokacij

Slika 7.4: Začetni seznam lokacij

Z izgledom nismo v celoti zadovoljni, saj bi želeli bolj uporabniško prijazen prikaz razdalje in povprečne ocene.

Spletna stran deluje z manjšo težavo pri prikazu razdalje od trenutne lokacije (glej sliko 7.4), kar bomo v nadaljevanju popravili. Najprej se sprehodimo čez vse lokacije in za vsako razdaljo pokličemo funkcijo formatDistance, ki bo poskrbela za pretvorbo v ustrezne enote in zaokroževanje vrednosti. Privzeto so vhodni podatki v metrih in se prikažejo v preveliki natančnosti, kar bomo spremenili in v primeru večjih razdalj poskrbeli za pretvorbo v kilometre.

hbs/controllers/location.js

const apiServer =
  process.env.NODE_ENV === "production"
    ? "https://sp-2023-2024.fly.dev"
    : "http://localhost:" + (process.env.PORT || 3000);
const axios = require("axios").create({ baseURL: apiServer, timeout: 5000 });
const formatDistance = (distance) => {
  let unit = "m";
  let nDigits = 0;
  if (distance > 1000) {
    distance = distance / 1000;
    unit = "km";
    nDigits = 2;
  }
  return (
    distance.toLocaleString("sl-SI", { maximumFractionDigits: nDigits }) +
    " " +
    unit
  );
};
const list = (req, res) => {
  let location = { lng: 14.469027, lat: 46.050129 }; // Ljubljana
  let data = [];
  let title = "List of cultural heritages";
  axios
    .get("/api/locations/distance", {
      params: { lng: location.lng, lat: location.lat, nResults: 5 },
    })
    .then(
      (apiRes) =>
        (data = apiRes.data.map((loc) => {
          loc.distance = formatDistance(loc.distance);
          return loc;
        }))
    )
    .catch((err) => (title = err.message + "!"))
    .finally(() => res.render("list", { title, locations: data }));
};

Prav tako bomo številčno vrednost povprečne ocene lokacije prikazali z zvezdicami. To najlažje dosežemo s pomočjo Handlebars helper funkcije, ki jo opredelimo enkrat na enem mestu in uporabimo večkrat. Če se npr. v prihodnje odločimo, da bomo oceno komentarja prikazovali na drugačen način (ne z zvezdicami), bomo to lahko spremenili na enem mestu. Kodo za prikaz ocene shranimo v novo datoteko hbsh.js.

hbs/views/helpers/hbsh.js

const hbs = require("hbs");

hbs.registerHelper("stars", (rating) => {
  let stars = "";
  for (let i = 1; i <= 5; i++)
    stars +=
      '<i class="fa-' + (rating >= i ? "solid" : "regular") + ' fa-star"></i>';
  return stars;
});

Novo opredeljeno helper funkcijo moramo v aplikaciji tudi registrirati, zato jo vključimo v začetno datoteko naše aplikacije server.js.

server.js

/**
 * View engine (HBS) setup
 */
app.set("views", path.join(__dirname, "hbs", "views"));
app.set("view engine", "hbs");
require("./hbs/views/helpers/hbsh.js");

V obstoječem pogledu list.hbs pa dodamo klic helper funkcije. Pomembno je, da sedaj uporabimo trojne oklepaje {{{...}}}, namesto dvojnih {{..}}, kar pomeni, da v pogled neposredno vstavljamo HTML kodo (zvezdice s pomočjo Font Awesome ikon v okviru helper funkcije stars).

hbs/views/list.hbs

<div class="row">
  {{#each locations as |location|}}
    <div class="col-12 col-xxl-6">
      <div class="card mt-4">
        <div class="card-header">
          <div class="d-flex justify-content-between align-items-center">
            ...
            <div class="text-nowrap ps-3">{{{ stars location.rating }}}</div>
          </div>
        </div>
        ...
      </div>
    </div>
  {{/each}}
</div>

Posodobljen prikaz razdalje in povprečne ocene v obliki zvezdic prikazuje slika 7.5.

Končni seznam lokacij

Slika 7.5: Končni seznam lokacij

7.5.4 Vključitev ostalih podatkov iz zunanjih virov

Poleg podatkov našega lastnega REST API strežnika lahko v aplikacijo vključimo tudi ostale podatke iz zunanjih virov. V našem primeru bomo uporabili storitev /nominatim/reverse, ki je na voljo na strežniku Teaching API. Omenjena storitev izvede proces obratnega geokodiranja, ki na podlagi vhodnih GPS koordinat vrne najbližji naslov. To funkcionalnost bomo uporabili za naslov strani s seznamom lokacij, kjer bomo v naslov dodali kratek opis lokacije.

hbs/controllers/locations.js

const list = async (req, res) => {
  let location = { lng: 14.469027, lat: 46.050129 }; // Ljubljana
  //let location = { lng: 15.260505, lat: 46.231193 }; // Celje
  let data = [];
  let title = "List of cultural heritages near ";
  let address;
  try {
    address = (
      await axios.get(
        "https://teaching.lavbic.net/api/nominatim/reverse?lat=" +
          location.lat +
          "&lon=" +
          location.lng +
          "&format=geojson&addressdetails=1&zoom=16"
      )
    ).data.features[0].properties.address;
    if (address.road && address.city)
      title += address.road + ", " + address.city;
    else if (address.city) title += address.city;
    else title += "unknown location";
  } catch (err) {}
  axios
    .get("/api/locations/distance", {
      params: { lng: location.lng, lat: location.lat, nResults: 5 },
    })
    .then(
      (apiRes) =>
        (data = apiRes.data.map((loc) => {
          loc.distance = formatDistance(loc.distance);
          return loc;
        }))
    )
    .catch((err) => (title = err.message + "!"))
    .finally(() => res.render("list", { title, locations: data }));
};

Sliki 7.6 in 7.7 prikazuje rezultat obratnega geokodiranja, kjer se na podlagi podanih vhodnih GPS koordinat prilagodi naslov spletne strani s seznamom kulturnih dediščin z opisom trenutne lokacije na ravni ulice in kraja.

Naslov seznama lokacij **v Ljubljani** s pomočjo obratnega geokodiranja

Slika 7.6: Naslov seznama lokacij v Ljubljani s pomočjo obratnega geokodiranja

Naslov seznama lokacij **v Celju** s pomočjo obratnega geokodiranja

Slika 7.7: Naslov seznama lokacij v Celju s pomočjo obratnega geokodiranja

7.6 Izboljšanje zmogljivosti API-ja

7.6.1 Porazdelitev po straneh

Porazdelitev po straneh (angl. pagination), kot ga prikazuje slika 7.8, je običajen postopek optimizacije pri večjih rezultatih, ki se prenašajo nazaj odjemalcu in na ta način izboljšamo odzivnost storitve.

Porazdelitev po straneh

Slika 7.8: Porazdelitev po straneh

Vplivi ukrepa:

  • zaporedno številčenje strani,
  • obravnava večjega števila rezultatov.

V okviru razvojnega okolja Node.js lahko nadgradimo obstoječo ORM knjižnico mongoose s knjižnico mongoose-paginate-v2, ki nam omogoča poenostavljeno porazdelitev po straneh pri dostopu do MongoDB podatkovne baze.

7.6.2 Asinhrono beleženje

Pri sinhronem beleženju disk obravnava vsak klic in lahko upočasni sistem. Pri asinhronem beleženju, kot ga prikazuje slika 7.9, se dnevniški zapisi najprej brez zaklepanja pošljejo v medpomnilnik (angl. buffer). Dnevniški zapisi se nato občasno zapišejo na disk, kar znatno zmanjša stroške V/I.

Asinhrono beleženje

Slika 7.9: Asinhrono beleženje

Vplivi ukrepa:

  • posredovanje dnevniških zapisov v medpomnilnik brez zaklepanja,
  • občasno zapisovanje na disk,
  • večja prepustnost in manjša zakasnitev.

V okviru razvojnega okolja Node.js imamo na voljo številne knjižnice za beleženje dnevniških zapisov, in sicer: pino, winston, morgan, bunyan, roarr, signale, tracer.

7.6.3 Predpomnjenje

Podatke, do katerih pogosto dostopamo, lahko shranimo v predpomnilnik (angl. cache). Odjemalec lahko najprej izvede poizvedbo v predpomnilniku (angl. cache), če pa pride do zgrešenega zadetka v predpomnilniku, odjemalec šele takrat dostopa do podatkovne baze (glej sliko 7.10). Predpomnilniki, kot je npr. Redis, shranjuje podatke v pomnilnik, zato je dostop do podatkov veliko hitrejši kot do podatkovne baze.

Predpomnjenje

Slika 7.10: Predpomnjenje

Vplivi ukrepa:

  • shranjevanje pogosto uporabljenih podatkov v predpomnilnik, namesto v podatkovno bazo,
  • poizvoedovanje po podatkovni baze le ob zgrešenem zadetku v predpomnilniku.

V okviru razvojnega okolja Node.js imamo na voljo številne knjižnice za predpomnjenje, in sicer node-cache, memcached, memory-cache, redis.

7.6.4 Stiskanje koristne vsebine

Zahteve in odgovore lahko stisnemo z npr. uporabo gzip (glej sliko 7.11), kar posledično zmanjša količino prenesenih podatkov in pospeši branje in nalaganje podatkov.

Stiskanje koristne vsebine

Slika 7.11: Stiskanje koristne vsebine

Vpliv ukrepa:

  • zmanjšanje velikosti podatkov in pospešitev prenosa ter nalaganja.

V okviru razvojnega okolja Node.js in uporabljenega strežnika express lahko uporabimo knjižnico compression in jo enostavno vključimo, kot prikazuje spodnji izsek kode:

const compression = require("compression");
...
const app = express();
app.use(compression());
...

7.6.5 Bazen povezav

Pri dostopu do virov moramo pogosto naložiti podatke iz podatkovne baze. Odpiranje in zapiranje povezav traja veliko časa, zato je smiselno pri dostopu do podatkovne baze uporabiti že odprte povezave v bazenu povezav (angl. connection pool) (glej sliko 7.12), ki je odgovoren za upravljanje življenjskega cikla povezave.

Bazen povezav

Slika 7.12: Bazen povezav

Vplivi ukrepa:

  • odpiranje in zapiranje povezav do podatkovne baze povroča nepotrebne stroške,
  • bazen povezav vzdržuje odprtih več povezav, ki jih lahko aplikacija ponovno uporabi.

V okviru razvojnega okolja Node.js in uporabljeno ORM knjižnico mongoose je ta funkcionalnost že privzeto na voljo.

Dodatno gradivo

  • Getting MEAN with Mongo, Express, Angular, and Node, 2nd edition (Holmes and Herber 2019)
    • Part 2: Building a Node web application
      • Chapter 6: Writing a REST API: Exposing the MongoDB database to the application
      • Chapter 7: Consuming a REST API: Using an API from inside Express