Uni Webdev Backend Summary

Summary for the webdev backend course at HdM Stuttgart

Felicitas Pojtinger

2023-01-28

1 Meta

1.1 Contributing

These study materials are heavily based on professor Toenniessen’s “Web Development Backend” lecture at HdM Stuttgart and prior work of fellow students.

Found an error or have a suggestion? Please open an issue on GitHub (github.com/pojntfx/uni-webdev-backend-notes):

QR code to source repository
QR code to source repository

If you like the study materials, a GitHub star is always appreciated :)

1.2 License

AGPL-3.0 license badge
AGPL-3.0 license badge

Uni Webdev Backend Notes (c) 2023 Felicitas Pojtinger and contributors

SPDX-License-Identifier: AGPL-3.0

2 Themen der Vorlesung

  1. Einführung in Node.js und einfache HTML-Fileserver
  2. RESTful Endpoints mit Express.js
  3. Die Template-Engine EJS und Express-Sessions
  4. Datenbanken mit MongoDB und Mongoose

3 Einführung in Node.js

3.1 Warum Node.js

Vorteile:

Sehr einfache APIs, schnell zu lernen

Nachteile:

3.2 Module

Node.js hat ein Modul-Konzept, das es ermöglicht, Funktionen und Variablen in eigene Dateien auszulagern und sie in anderen Dateien zu importieren.

Ein Beispiel dafür ist die Funktion add(x,y), die in eine separate Datei namens 01d_Export.js ausgelagert wird:

module.exports = function add(x, y) {
  return x + y;
};

In einer anderen Datei, z.B. 01d_AddiererFctModule.js, wird das Modul importiert und verwendet:

const add = require("./01d_Export");
const a = 5,
  b = 7;
const s = add(a, b);
console.log(`${a} + ${b} = ${s}`);

3.3 Import & Export mit require/module

Export-Varianten:

  1. Einzelne Methode oder Variable:
module.exports = function add(x, y) {
  return x + y;
};
  1. Mehrere Methoden oder Variablen über ein Objekt:
module.exports = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
};
  1. Mehrere einzelne Exports (mit der Convenience-Variable exports):
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;

Wenn Sie jedoch module.exports direkt zuweisen, werden alle vorherigen Exporte überschrieben.

Import-Varianten:

  1. Gesamtes Modul importieren:
const fs = require("fs");
fs.readFile();

const readFile = require("fs").readFile;
readFile();
  1. Destrukturierende Zuweisung:
const { readFile } = require("fs");
readFile();

const { readFile, ...fs } = require("fs");
readFile();
fs.writeFile();

3.4 Import & Export mit ES6

Export:

export function add(x, y) {
  return x + y;
}

export function subtract(x, y) {
  return x - y;
}

Default-Export:

export default (x, y) {
  return x - y;
}

Import eines gesamten Moduls:

import * as math from "./math.js";

console.log(math.add(5, 2)); // Ausgabe: 7
console.log(math.subtract(5, 2)); // Ausgabe: 3

Import mit destrukturierenden Zuweisung:

import { add as addition, subtract } from "./math.js";

console.log(addition(5, 2)); // Ausgabe: 7
console.log(subtract(5, 2)); // Ausgabe: 3

3.5 Callbacks vs. Promises vs. Async/Await

Callbacks:

const fs = require("fs");

fs.readFile("file.txt", function (err, data) {
  if (err) throw err;
  console.log(data);
});

Promises:

const fs = require("fs").promises;

fs.readFile("file.txt")
  .then((data) => console.log(data))
  .catch((err) => console.error(err));

async/await:

const fs = require("fs").promises;

async function readFileExample() {
  try {
    const data = await fs.readFile("file.txt");
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

readFileExample();

3.6 Statischer Webserver

Mit Support für ein paar wenige MIME-Types:

const http = require("http");
const fs = require("fs");
const { extname } = require("path");

const app = http.createServer((request, response) => {
  fs.readFile(__dirname + request.url, (err, data) => {
    const status = err ? 400 : 200;

    if (extname(request.url) == ".html")
      response.writeHead(200, { status, "Content-Type": "text/html" });
    if (extname(request.url) == ".js")
      response.writeHead(200, { status, "Content-Type": "text/javascript" });
    if (extname(request.url) == ".css")
      response.writeHead(200, { status, "Content-Type": "text/css" });

    response.write(data);
    response.end();
  });
});

app.listen(3000);

console.log("Listening on :3000");

Mit Support für ein alle MIME-Types:

$ npm install node-static
const http = require("http");
const fileserver = new (require("node-static").Server)();

const app = http.createServer((request, response) => {
  fileserver.serve(request, response);
});

app.listen(3000);

console.log("Listening on :3000");

Mit Support für ein alle MIME-Types & Express:

$ npm install express
const express = require("express");
const app = express();

app.use("/WDBackend", express.static(__dirname + "/public"));
app.listen(3000);

console.log("Listening on :3000");

3.7 NPM: Pakete Installieren

3.8 NPM: package.json

3.9 NPM: Paketauflösung

  1. In einem relativen Pfad zur Datei, bis eine “package.json” Datei gefunden wird (npm Projekt Definition) und dort im “node_modules” Ordner
  2. In den global installierten Paketen

Best Practice: Pakete sollten immer im Projekt installiert werden, damit dort alle Abhängigkeiten definiert sind. CLI-Tools können auch global, z.B. zur Projektinitialisierung, installiert werden.

4 RESTful Endpoints mit Express.js

4.1 Warum REST?

Jahr 2000: Überlastung der Web-Backends (Server)

Heute: Zustandslose Web-Backends (Server)

4.2 Was ist REST?

4.3 Merkmale einer REST-Architektur

4.4 Idempotente Schnittstellen

Sichere und idempotente Schnittstellen:

Unsichere und nicht-idempotente Schnittstelle: POST (Create auf eine Ressource). Im Gegensatz zu DELETE können hier nach einem erneuten Anruf ohne Checks weitere Objekte erstellt werden.

4.5 Einheitliche Schnittstellen

4.6 Einheitliche Schnittstellen mit REST

Pfad:

http://127.0.0.1:3000/fruits
const DATA = [
  { id: 1, name: "Apfel", color: "gelb,rot" },
  { id: 2, name: "Birne", color: "gelb,grün" },
  { id: 3, name: "Banane", color: "gelb" },
];

app.get("/fruits", (req, res) => {
  res.send(DATA);
});

URL-Parameter:

http://127.0.0.1:3000/fruits/2
const DATA = [
  { id: 1, name: "Apfel", color: "gelb,rot" },
  { id: 2, name: "Birne", color: "gelb,grün" },
  { id: 3, name: "Banane", color: "gelb" },
];

app.get("/fruits/:id", (req, res) => {
  const id = parseInt(req.params.id);

  const item = DATA.find((o) => o.id === id);

  res.send(item);
});

Query-Parameter:

http://127.0.0.1:3000/fruits/2
const DATA = [
  { id: 1, name: "Apfel", color: "gelb,rot" },
  { id: 2, name: "Birne", color: "gelb,grün" },
  { id: 3, name: "Banane", color: "gelb" },
];

app.get("/fruits", (req, res) => {
  const id = parseInt(req.query.id);

  const item = DATA.find((o) => o.id === id);

  res.send(item);
});

HTTP-Body (URL-Encoded)

Nutze express.urlencoded

app.use(express.urlencoded({ extended: true }));

const DATA = [{ id: 1, name: "Apfel", color: "gelb,rot" }];

app.post("/fruits", (req, res) => {
  const { name, color } = req.body;

  if (DATA.find((o) => o.name === name)) {
    res.send("Duplicate name");
  } else {
    const id = Math.max(...DATA.map((o) => o.id)) + 1;
    const fruit = { id, name, color };
    DATA.push(fruit);
    res.send(fruit);
  }
});

HTTP-Body (JSON)

Nutze express.json

app.use(express.json());

const DATA = [{ id: 1, name: "Apfel", color: "gelb,rot" }];

app.post("/fruits", (req, res) => {
  const { name, color } = req.body;

  if (DATA.find((o) => o.name === name)) {
    res.send("Duplicate name");
  } else {
    const id = Math.max(...DATA.map((o) => o.id)) + 1;
    const fruit = { id, name, color };
    DATA.push(fruit);
    res.send(fruit);
  }
});

4.7 Routenpfade in Express

app.get("/ab?cd", function (req, res) {
  res.send("ab?cd");
}); // acdabcd

app.get("/ab+cd", function (req, res) {
  res.send("ab+cd");
}); // abcdabbbbcd

app.get("/ab.*cd", function (req, res) {
  res.send("ab.*cd");
}); // abcdabxcd

app.get("/ab(cd)?e", function (req, res) {
  res.send("ab(cd)?e");
}); // /abe und /abcde

app.get(/a/, function (req, res) {
  res.send("/a/");
}); // alles mit 'a' drin

app.get(/.*fly$/, function (req, res) {
  res.send("/.*fly$/");
});

4.8 Middleware in Express

Wildcard-Route:

app.all(/.*/, (req, res, next) => {
  console.log(`wildcard-route: ${req.method} ${req.url}`);
  next();
});

Middleware (empfohlen):

app.use((req, res, next) => {
  console.log(`middleware: ${req.method} ${req.url}`);
  next();
});

Die next()-Methode führt immer den nächsten passenden Routen-Handler aus.

4.9 Mehrere Callback-Handler

let cb0 = function (req, res, next) {
  console.log("CB0");
  next();
};

let cb1 = function (req, res, next) {
  console.log("CB1");
  next();
};

let cb2 = function (req, res) {
  res.send("Hello from CB2!");
};

app.get("/example/c", [cb0, cb1, cb2]);

Wichtig: next nicht vergessen!

4.10 Chaining Routes

Mehrere HTTP-Verben für eine Route können mithilfe von Chaining Routes zusammengefasst werden.

app
  .route("/books")
  .get(function (req, res) {
    res.send("Get all books");
  })
  .post(function (req, res) {
    res.send("Add a book");
  });

app
  .route("/books/:id")
  .put(function (req, res) {
    res.send("Update the book");
  })
  .delete(function (req, res) {
    res.send("Delete the book");
  });

Vorteile: weniger fehleranfällig, leichter zu pflegen (da man die Route nur einmal schreibt)

4.11 Modularisierung

Modularisierung von Routen in Express kann mithilfe von express.Router erreicht werden.

Erstellung einer Router-Datei birds.js:

const express = require("express");
const router = express.Router();

// Middleware
router.use(function timeLog(req, res, next) {
  console.log("Time: ", Date.now());
  next();
});

// Routen
router.get("/", function (req, res) {
  res.send("Birds home page");
});

router.get("/about", function (req, res) {
  res.send("About birds");
});

module.exports = router;

Einbindung des Routers in die Anwendung app.js:

const express = require("express");
const app = express();
const birds = require("./birds");

app.use("/birds", birds);

app.listen(3000);

console.log("Listening on :3000");

4.12 Weitere Methoden von Express

Result:

Request:

4.13 Fehlerhandling

404 als JSON zurückgeben:

app.use("/users", require("./routes/users"));
app.use("/products", require("./routes/products"));

// Middleware nach allen Routes
app.use((req, res) => {
  res.status(404);
  res.json({ message: "Not found" });
});

Exceptions:

try {
  throw new Error("Something went wrong");
} catch (err) {
  res.status(500).json({ message: "InternalServerError" });
}
app.get("/", async (req, res, next) => {
  try {
    throw new Error("Something went wrong");
  } catch (err) {
    next(err);
  }
});

app.use((err, req, res, next) => {
  res.status(500);
  res.json({ message: "InternalServerError" });
  console.error(err);
});

4.14 HTTP-Verben und HTML-Forms

const express = require("express");
const methodOverride = require("method-override");
const app = express();

app.use(methodOverride("_method"));

Jetzt kann man eine PATCH-Route definieren, die dann auch über ein Formular angesprochen werden kann:

app.patch("/fruits", (req, res) => {
  // some code ...
});

In dem Formular muss dann der URL-Parameter _method=patch hinzugefügt werden:

<form action="/fruits?_method=patch" method="post">...</form>

Jetzt wird die PATCH-Route aufgerufen, wenn das Formular abgeschickt wird.

5 Die Template-Engine EJS und Express-Sessions

5.1 Einführung in EJS

5.2 Verwendung von EJS

Der Code auf dem Server, der die EJS-Template-Engine verwendet, sieht wie folgt aus:

const express = require("express");
const app = express();

app.set("view engine", "ejs");

app.get("/user", (req, res) => {
  const user = {
    name: "John Doe",
    email: "johndoe@example.com",
    phone: "555-555-5555",
  };
  res.render("user-template", { user });
});

Das Template template.ejs im Unterverzeichnis views, welcher ein JavaScript-Objekt {vorname, adresse, telefon} übergeben wird:

<html>
  <body>
    <h1>User Information</h1>
    <table>
      <tr>
        <td>Name:</td>
        <td><%= user.name %></td>
      </tr>
      <tr>
        <td>Email:</td>
        <td><%= user.email %></td>
      </tr>
      <tr>
        <td>Phone:</td>
        <td><%= user.phone %></td>
      </tr>
    </table>
  </body>
</html>

5.3 Schleifen in EJS

Server:

const express = require("express");
const app = express();

app.set("view engine", "ejs");

const DATA = [
  { id: 1, name: "Apfel", color: "gelb,rot" },
  { id: 2, name: "Birne", color: "gelb,grün" },
  { id: 3, name: "Banane", color: "gelb" },
];

app.get("/fruits", (req, res) => {
  res.render("all", { fruits: DATA }); // all.ejs Template
});

app.get("/fruits/:id", (req, res) => {
  const id = parseInt(req.params.id);
  const fruit = DATA.find((o) => o.id === id);
  res.render("fruit", fruit); // fruit.ejs Template
});
app.listen(3000);

console.log("EJS server running on localhost:3000");

Template:

<html>
  <body>
    <table>
      <tr>
        <th>Name</th>
        <th>Farbe</th>
      </tr>
      <% fruits.forEach( o => { %>
      <tr>
        <td><%= o.name %></td>
        <td><%= o.color %></td>
      </tr>
      <% }) %>
    </table>
  </body>
</html>

So können Cookies gesetzt werden:

const cookieParser = require("cookie-parser");

app.use(cookieParser());

response.cookie("userID", "xyz12345"); // Einzelner Cookie

response
  .cookie("userID", "xyz12345")
  .cookie("verein", "VfB Stuttgart", { maxAge: 90000 }); // Mehrere Cookies, der zweite mit 90000 milli secs Lebensdauer

So können Cookies ausgelesen werden:

const cookieParser = require("cookie-parser");

app.use(cookieParser());

const cookies = request.cookies;

let userID = cookies.userID;
let verein = cookies.verein;

5.5 State mit Cookies durch express-session

Mit dem npm-Package express-session kann man zustandsbehaftete Server bauen:

const express = require("express");
const session = require("express-session");

const app = express();

app.use(
  session({
    secret: "mykey", // Für Encoding und Decoding des Cookies
    resave: false, // Nur speichern nach Änderung
    saveUninitialized: true, // Anfangs immer speichern
    cookie: { maxAge: 5000 }, // Ablaufzeit in Millisekunden
  })
);

app.get("/", function (req, res) {
  if (req.session.count) {
    // Eine Session kann beliebige Attribute bekommen
    req.session.count++;
    res.setHeader("Content-Type", "text/html");
    res.write("<p>count: " + req.session.count + "</p>");
    res.end();
  } else {
    req.session.count = 1;
    res.end("Willkommen zu der Sitzung. Refresh!");
  }
});

6 Datenbanken mit MongoDB und Mongoose

6.1 Datenbanken in Webanwendungen

6.2 Grundlagen zu MongoDB

6.3 Verwendung von MongoDB in Express

Zuerst mongodb installieren: npm i -s mongodb

Dann mit DB verbinden:

let db = null;
const url = `mongodb://localhost:27017`;

MongoClient.connect(url, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
}).then((connection) => {
  db = connection.db("food");
  console.log("connected to database food ...");
});

6.4 Queries in MongoDB

Erstellen einer Collection:

app.post("/example-create-collection-fruits", async (req, res) => {
  await db.createCollection("fruits");

  res.send("Collection fruits created ...");
});

Löschen einer Datenbank:

app.post("/example-drop-db-food", async (req, res) => {
  await db.dropDatabase("food");
  res.send("Database food dropped!");
});

db.dropCollection für das Löschen einer Collection

Importieren von Dokumenten:

$ mongoimport --db tools-test --collection restaurants --file restaurants.json

Exportieren von Dokumenten:

$ mongoexport --db tools-test --collection restaurants --out new-restaurants.json

Einfügen eines Dokument:

app.post("/example-create/fruits", async (req, res) => {
  const { name, color } = req.body;
  const fruit = { name, color };

  await db.collection("fruits").insertOne(fruit);

  res.send(`${name} inserted ...`);
});

Auslesen eines Dokuments:

app.get("/example-find-one/fruits/:name", async (req, res) => {
  const { name } = req.params;

  const fruit = await db.collection("fruits").findOne({ name });

  if (fruit) {
    res.send(fruit);
  } else {
    res.status(400).send("not found ...");
  }
});

Einfügen mehrerer Dokumente:

app.post("/example-insert-many/fruits", async (req, res) => {
  const fruits = [
    { name: "Apfel", color: "gelb,rot" },
    { name: "Birne", color: "gelb,grün" },
    { name: "Kiwi", color: "grün" },
    { name: "Banane", color: "gelb" },
    { name: "Pfirsich", color: "gelb,rot" },
  ];

  await db.collection("fruits").insertMany(fruits);

  res.send("Fruits inserted");
});

Auslesen aller Dokumente:

app.get("/example-list/fruits", async (req, res) => {
  const fruits = await db.collection("fruits").find().toArray();

  res.send(fruits);
});

Löschen eines Dokuments:

app.delete("/example-delete/fruits/:name", async (req, res) => {
  const { name } = req.params;

  const result = await db.collection("fruits").deleteOne({ name });

  if (result.deletedCount > 0) {
    res.send(`${name} deleted ...`);
  } else {
    res.status(400).send("fruit not found, nothing to delete ...");
  }
});

Löschen mehrerer Dokumente:

db.collection("fruits").deleteMany({ name: { $regex: name } });

Aktualisierung von Dokumenten:

app.patch("/example-update-cuisine/:name", async (req, res) => {
  const { name } = req.params;
  const { cuisine } = req.body;

  const result = await db.collection("restaurants").updateOne(
    { name },
    {
      $set: { cuisine },
      $currentDate: { lastModified: true }, // Änderungsdatum
    }
  );

  res.send(result);
});

Hinzufügen von Arrayelementen in Dokumenten:

app.post("/example-push-grade-score/restaurants/:name", async (req, res) => {
  const { name } = req.params;
  const { grade, score } = req.body;
  const newGrade = { date: new Date(), grade, score };

  const result = await db.collection("restaurants").updateOne(
    { name },
    (update = {
      $push: { grades: newGrade },
      $currentDate: { lastModified: true },
    })
  );

  res.send(result);
});

Löschen von Arrayelementen in Dokumenten:

app.post("/example-pop-grade-score/restaurants/:name", async (req, res) => {
  const { name } = req.params;
  const query = { name };

  const result = await db.collection("restaurants").updateOne(query, {
    $pop: { grades: 1 },
    $currentDate: { lastModified: true },
  });

  res.send(result);
});

Tiefe Queries:

app.get("/example-zip/restaurants", async (req, res) => {
  const { cuisine, zip } = req.query;

  const restaurants = await db
    .collection("restaurants")
    .find({ "address.zipcode": zip, cuisine })
    .toArray();

  res.send(restaurants.map((o) => ({ name: o.name, zip: o.address.zipcode })));
});

BSON für Vergleichsoperatoren:

app.get("/example-zip-range/restaurants", async (req, res) => {
  const { zipMin, zipMax, cuisine } = req.query;

  const restaurants = await db
    .collection("restaurants")
    .find({
      cuisine,
      "address.zipcode": { $gte: zipMin, $lt: zipMax }, // es gibt auch $eq, $in, $neq, $nin ...
    })
    .toArray();

  res.send(restaurants.map((o) => ({ name: o.name, zip: o.address.zipcode })));
});

BSON für Oder-Verknüpfung:

app.get("/example-zip-or-cuisine/restaurants", async (req, res) => {
  const { zip, cuisine } = req.query;

  const restaurants = await db
    .collection("restaurants")
    .find(
      { $or: [{ "address.zipcode": zip }, { cuisine }] } // es gibt auch noch $and, $not und $nor
    )
    .toArray();

  res.send(
    restaurants.map((o) => ({
      name: o.name,
      cuisine: o.cuisine,
      zip: o.address.zipcode,
    }))
  );
});

6.5 Projektionen in MongoDB

Ein/Auschluss von Attributen:

app.get("/example-fields/restaurants", async (req, res) => {
  const { borough, cuisine } = req.query;

  const restaurants = await db
    .collection("restaurants")
    .find(
      { borough, cuisine },
      { projection: { name: 1, address: 1, _id: 0 } } // 1 bedeuted Einschluss eines Attributs, 0 den Ausschluss
    )
    .toArray();

  res.send(restaurants);
});

Sortieren:

app.get("/example-sort/restaurants", async (req, res) => {
  const { borough, cuisine } = req.query;

  const restaurants = await db
    .collection("restaurants")
    .find({ borough, cuisine }, { projection: { name: 1, address: 1, _id: 0 } })
    .sort({ name: 1 }) // Mit 1 wird aufsteigend sortiert, mit 0 absteigend.
    .toArray();
  res.send(restaurants);
});

Aggregation:

app.get("/example-avg-score/restaurants", async (req, res) => {
  const { borough, cuisine } = req.query;

  const restaurants = await db
    .collection("restaurants")
    .aggregate([
      {
        // Wie `WHERE` in SQL
        $match: { borough, cuisine },
      },
      {
        // Wie `SELECT` in SQL
        $project: {
          name: "$name", // auch name: 1 möglich
          avg_score: { $avg: "$grades.score" },
        }, // auch $min, $max, $sum
      },
      { $sort: { name: 1 } },
    ])
    .toArray();

  res.send(restaurants);
});

Gruppierung:

app.get("/example-group/restaurants", async (req, res) => {
  const { borough, cuisine } = req.query;

  const restaurants = await db
    .collection("restaurants")
    .aggregate([
      {
        $match: { borough, cuisine },
      },
      {
        // Wie `GROUP BY` in SQL
        $group: {
          _id: "$address.zipcode",
          count: { $sum: 1 }, // auch $min, $max, $avg
        },
      },
      { $sort: { _id: 1 } },
    ])
    .toArray();

  res.send(restaurants);
});

6.6 Indizes in MongoDB

Datenbank-Indizes beschleunigen die Zugriffe für Queries und Updates, wenn nicht konkret mit der _id gesucht wird.

Anlegen eines Index:

await db.collection("restaurants").createIndex({ cuisine: 1 }); // 1: Aufsteigend, -1: Absteigend

Form: {name: 'cuisine_1'}

Anlegen eines kombinierten Index:

await db
  .collection("restaurants")
  .createIndex({ cuisine: 1, "address.zipcode": -1 });

Form: {name: 'cuisine_1_address.zipcode_-1'}

Abfragen aller Indizes:

const indexes = await db.collection("restaurants").getIndexes();

Löschen eines Index:

const result = await db.collection("restaurants").dropIndex("cuisine_1"); // 0: not ok, 1: success

Löschen aller Indizes:

const result = await db.collection("restaurants").dropIndexes();

6.7 Bewertung von MongoDB

Vorteile:

Nachteile:

6.8 Grundlagen zu Mongoose

6.9 Datenbankverbindung in Mongoose herstellen

Ist sehr ähnlich zu MongoDB:

const url = "mongodb://localhost:27017/food_mongoose";

mongoose
  .connect(url, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => {
    console.log("connected to database food_mongoose ...");
  });

6.10 Schematas in Mongoose

Definition von Schemata:

const mongoose = require("mongoose");

const fruitSchema = new mongoose.Schema({
  name: { type: String, required: true },
  color: { type: String, required: true },
  img: { data: Buffer, contentType: String },
});

const Fruit = mongoose.model("Fruit", fruitSchema); // `Fruit` maps to a collection `fruits`

Schema Types aus ES6:

const schemaExample = new mongoose.Schema({
  bool: Boolean,
  updated: Date,
  age: Number,
  array: [],
  arrayofString: [String],
  arrayofArrays: [[]],
  arrayofArrayOfNumbers: [[Number]],
  map: Map,
  mapOfString: { type: Map, of: String },
});

Weitere Schema Types aus MongoDB:

const schemaExample = new mongoose.Schema({
  mixed: mongoose.Mixed, // Kann alles sein (`any`)
  _someId: mongoose.ObjectId, // Explizite MongoDB-Id
  decimal: mongoose.Decimal128, // 128-bit Floating-Point; `mongoose.Types.Decimal128.fromString('3.1415')`
});

Indizes in Schemas:

const schemaExample = new mongoose.Schema({
  name: {
    type: String,
    required: true, // Feld zwingend notwendig
    index: true, // Index wird automatisch angelegt
  },
});

Options-Objekt:

const schemaExample = new mongoose.Schema(
  {
    name: String,
    age: Number,
  },
  {
    // Das Options-Objekt
    timestamps: true, // Automatisch `createdAt` und `modifiedAt` verwalten
  }
);

Verschachtelte Schemata mit Kopien:

const ratingSchema = new mongoose.Schema({
  grade: {
    type: Number,
    min: 1,
    max: 6,
  },
  comment: String,
  date: Date,
});

const productSchema = new mongoose.Schema({
  name: { type: String, required: true },
  price: { type: String, required: true },
  ratings: [ratingSchema],
});

const Rating = mongoose.model("Rating", ratingSchema);
const Product = mongoose.model("Product", productSchema);

Verschachtelte Schemata mit Referenzen:

const productSchema = new mongoose.Schema({
  name: String,
  category: {
    // 1:1-Beziehung
    type: mongoose.ObjectId,
    ref: "Category",
  },
});

const Product = mongoose.model("Product", productSchema);

const categorySchema = new mongoose.Schema({
  name: String,
  products: [
    // 1:n-Beziehung
    {
      type: mongoose.ObjectId,
      ref: "Product",
    },
  ],
});

const Category = mongoose.model("Category", categorySchema);

Auslesen von Referenzen mit populate (wie SQL JOIN): await Product.find().populate("category")

6.11 Queries in Mongoose

Auslesen aller Dokumente:

app.get("/example-list/fruits", async (req, res) => {
  const fruits = await Fruit.find();

  res.send(fruits);
});

Hinzufügen eines Dokuments:

app.post("/example-create/fruits", async (req, res) => {
  const { name, color } = req.body;
  const doc = await Fruit.findOne({ name });
  if (doc) {
    res.status(400).send("fruit found, delete first ...");

    return;
  }

  const fruit = new Fruit({ name, color });

  const imgPath = path.join(IMAGE_PATH, `${name}.png`);
  try {
    fruit.img.data = await fs.readFile(imgPath);
    fruit.img.contentType = "image/png";
  } catch (err) {
    console.log(`No image for ${name} found.`);
  }

  await fruit.save();

  res.send(`${name} inserted ...`);
});

Umwandlung von MongoDB-Queries in Mongoose-Queries:

app.get("/example-group-by-color/fruits/:color", async (req, res) => {
  const { color } = req.params;
  const result = await Fruit.aggregate([
    {
      $match: { color: { $regex: color } },
    },
    {
      $group: {
        _id: "$color",
        count: { $sum: 1 },
      },
    },
    { $sort: { _id: 1 } },
  ]);
  res.send(result);
});

Ersetze db.collection("fruits") durch Fruit

6.12 Validation in Mongoose

Eingebaute Validatoren:

const breakfastSchema = new Schema({
  eggs: {
    type: Number,
    min: [6, "Too few eggs"],
    max: 12,
  },
  bacon: {
    type: Number,
    required: [true, "Why no bacon?"],
  },
  drink: {
    type: String,
    enum: ["Coffee", "Tea"],
    required: function () {
      return this.bacon > 3;
    },
  },
});

Eigene Validatoren:

function myValidator(val) {
  return val === "something";
}

new mongoose.Schema({ name: { type: String, validate: myValidator } });

// Hinzufügen einer Error-Message, {PATH} ist der fehlerhafte Pfad im Schema:
const customValidator = [
  myValidator,
  'Ups, {PATH} does not equal "something".',
];

new mongoose.Schema({ name: { type: String, validate: customValidator } });