Framework

Installation

Deepkit Framework basiert auf Runtime Types in Deepkit Type. Stelle sicher, dass @deepkit/type korrekt installiert ist. Siehe dazu Runtime Type Installation.

npm install ts-node @deepkit/framework

Stellen Sie sicher, dass alle Peer-Abhängigkeiten installiert sind. Standardmäßig werden sie von NPM 7+ automatisch installiert.

Um Ihre Anwendung zu kompilieren, benötigen wir den TypeScript-Compiler und empfehlen ts-node, um die App einfach auszuführen.

Eine Alternative zur Verwendung von ts-node besteht darin, den Quellcode mit dem TypeScript-Compiler zu kompilieren und den JavaScript-Quellcode direkt auszuführen. Dies hat den Vorteil, dass sich die Ausführungsgeschwindigkeit für kurze Befehle drastisch erhöht. Allerdings wird dadurch auch zusätzlicher Workflow-Overhead erzeugt, indem der Compiler entweder manuell ausgeführt oder ein Watcher eingerichtet wird. Aus diesem Grund wird in dieser Dokumentation in allen Beispielen ts-node verwendet.

Erste Applikation

Da das Deepkit Framework keine Konfigurationsdateien oder eine spezielle Ordnerstruktur verwendet, können Sie Ihr Projekt so strukturieren, wie Sie es wünschen. Die einzigen beiden Dateien, die Sie für den Start benötigen, sind die TypeScript-Datei app.ts und die TypeScript-Konfiguration tsconfig.json.

Unser Ziel ist es, die folgenden Dateien in unserem Projektordner zu haben:

.
├── app.ts
├── node_modules
├── package-lock.json
└── tsconfig.json

Datei: tsconfig.json

{
  "compilerOptions": {
    "outDir": "./dist",
    "experimentalDecorators": true,
    "strict": true,
    "esModuleInterop": true,
    "target": "ES2020",
    "module": "CommonJS",
    "moduleResolution": "node"
  },
  "reflection": true,
  "files": [
    "app.ts"
  ]
}

Datei: app.ts

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { Logger } from '@deepkit/logger';
import { cli, Command } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

@cli.controller('test')
export class TestCommand implements Command {
    constructor(protected logger: Logger) {
    }

    async execute() {
        this.logger.log('Hello World!');
    }
}

new App({
    controllers: [TestCommand],
    imports: [new FrameworkModule]
}).run();

In diesem Code sehen Sie, dass wir einen Testbefehl über die Klasse TestCommand definiert und eine neue Anwendung erstellt haben, die wir direkt mit run() ausführen. Durch das Ausführen dieses Skripts starten wir die App.

Mit dem Shebang in der ersten Zeile (#!…​) können wir unser Skript mit dem folgenden Befehl ausführbar machen.

chmod +x app.ts

Und dann ausführen:

$ ./app.ts
VERSION
  Node

USAGE
  $ ts-node-script app.ts [COMMAND]

TOPICS
  debug
  migration  Executes pending migration files. Use migration:pending to see which are pending.
  server     Starts the HTTP server

COMMANDS
  test

Um nun unseren Testbefehl auszuführen, führen wir folgenden Befehl aus.

$ ./app.ts test
Hello World

In Deepkit Framework geschieht nun alles über diese app.ts. Sie können die Datei beliebig umbennen oder weitere anlegen. Eigene CLI commands, HTTP/RPC server, Migration commands, usw werden alle über diesen Einstiegspunkt gestartet.

Um den HTTP/RPC-Server zu starten, führen Sie folgendes aus:

./app.ts server:start

Um Anfragen bedienen zu können, lesen Sie bitte das Kapitel HTTP oder RPC. Im Kapitel CLI kann mehr über CLI commands erfahren werden.

App

Über das App-Objekt startet wie Applikation.

Die run()-Methode list dabei die Argumente aus und führt den entsprechenden CLI-Controller aus. Da FrameworkModule eigene CLI-Controller bereitstellt, die zum Beispiel für das Starten des HTTP-Servers verantwortlich sind, können diese darüber aufgerufen werden.

Über das App-Objekt kann auch der Dependency Injection Container angesprochen werden, ohne dass ein CLI-Controller ausgeführt wird.

const app = new App({
    controllers: [TestCommand],
    imports: [new FrameworkModule]
});

//get access to all registered services
const eventDispatcher = app.get(EventDispatcher);

//then run the app, or do something else
app.run();

Module

Deepkit Framework ist hochgradig modular und ermöglicht es Ihnen, Ihre Anwendung in mehrere praktische Module aufzuteilen. Jedes Modul hat seine eigene Dependency Injektion Sub-Container, Konfiguration, Befehle und vieles mehr. Im Kapitel "Erste Applikation" haben Sie bereits ein Modul erstellt - das Root-Modul. new App benötigt fast die gleichen Argumente wie ein Modul, denn es erstellt das Root-Modul im Hintergrund für Sie automatisch.

Sie können dieses Kapitel überspringen, wenn Sie nicht vorhaben, Ihre Anwendung in Untermodule aufzuteilen, oder wenn Sie nicht vorhaben, ein Modul als Paket für andere zur Verfügung zu stellen.

Ein Modul ist eine einfache Klasse:

import { createModule } from '@deepkit/app';

export class MyModule extends createModule({}) {
}

Es hat zu diesem Zeitpunkt im Grunde keine Funktionalität, da seine Moduldefinition ein leeres Objekt ist und es keine Methoden hat, aber dies demonstriert die Beziehung zwischen Modulen und Ihrer Anwendung (Ihrem Stammmodul). Dieses Modul MyModule kann dann in Ihrer Anwendung oder in anderen Modulen importiert werden.

import { MyModule } from './module.ts'

new App({
    imports: [
        new MyModule(),
    ]
}).run();

Sie können nun diesem Modul Features hinzufügen, wie Sie es mit App tun würden. Die Argumente sind die gleichen, nur dass Importe in einer Moduldefinition nicht verfügbar sind. Fügen Sie HTTP/RPC/CLI-Controller, Dienste, eine Konfiguration, Event-Listener sowie verschiedene Modul-Hooks hinzu, um Module dynamischer zu gestalten.

Controllers

Module können Controller definieren, die von anderen Modulen verarbeitet werden. Wenn Sie zum Beispiel einen Controller mit Dekoratoren aus dem @deepkit/http-Paket hinzufügen, wird sein Modul HttpModule dies aufgreifen und die gefundenen Routen in seinem Router registrieren. Ein einzelner Controller kann mehrere solcher Dekoratoren enthalten. Es liegt an dem Modulautor, der Ihnen diese Dekoratoren gibt, wie er die Controller verarbeitet.

In Deepkit gibt es drei Pakete, die solche Controller verarbeitet: HTTP, RPC, und CLI. Siehe jeweils deren Kapitel, um mehr zu erfahren. Nachfolgend ist ein Beispiel eines HTTP-Controllers:

import { createModule } from '@deepkit/app';
import { http } from '@deepkit/http';
import { injectable } from '@deepkit/injector';

class MyHttpController {
    @http.GET('/hello)
    hello() {
        return 'Hello world!';
    }
}

export class MyModule extends createModule({
    controllers: [MyHttpController]
}) {}

//same is possible for App
new App({
    controllers: [MyHttpController]
}).run();

Provider

Wenn Sie einen Provider im providers-Bereich Ihrer Anwendung definieren, ist dieser in Ihrer gesamten Anwendung zugänglich. Bei Modulen hingegen werden diese Provider automatisch in den Subcontainer für die Injektion von Abhängigkeiten dieses Moduls gekapselt. Sie müssen jeden Provider manuell exportieren, um ihn für ein anderes Modul bzw. ihrer Anwendung verfügbar zu machen.

Um mehr darüber zu erfahren, wie Provider funktionieren, lesen Sie bitte das Kapitel Dependency Injection.

import { createModule } from '@deepkit/app';
import { http } from '@deepkit/http';
import { injectable } from '@deepkit/injector';

export class HelloWorldService {
    helloWorld() {
        return 'Hello there!';
    }
}

class MyHttpController {
    constructor(private helloService: HelloWorldService) {}

    @http.GET('/hello)
    hello() {
        return this.helloService.helloWorld();
    }
}

export class MyModule extends createModule({
    controllers: [MyHttpController],
    providers: [HelloWorldService],
}) {}

//same is possible for App
new App({
    controllers: [MyHttpController],
    providers: [HelloWorldService],
}).run();

Wenn ein Benutzer dieses Modul importiert, hat er keinen Zugriff auf HelloWorldService, da dieser im Subdependency-Injection-Container von MyModule gekapselt ist.

Exports

Um Provider im Modul des Importeurs verfügbar zu machen, können Sie den Token des Providers in exports aufnehmen. Dadurch wird der Provider im Wesentlichen eine Ebene nach oben in den Dependency-Injection-Container des übergeordneten Moduls - des Importeurs - verschoben.

import { createModule } from '@deepkit/app';

export class MyModule extends createModule({
    controllers: [MyHttpController]
    providers: [HelloWorldService],
    exports: [HelloWorldService],
}) {}

Wenn Sie andere Provider wie FactoryProvider, UseClassProvider usw. haben, sollten Sie trotzdem nur den Klassentyp in den Exporten verwenden.

import { createModule } from '@deepkit/app';

export class MyModule extends createModule({
    controllers: [MyHttpController]
    providers: [
        {provide: HelloWorldService, useValue: new HelloWorldService}
    ],
    exports: [HelloWorldService],
}) {}

We can now import that module and use its exported service in our application code.

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { cli, Command } from '@deepkit/app';
import { HelloWorldService, MyModule } from './my-module';

@cli.controller('test')
export class TestCommand implements Command {
    constructor(protected helloWorld: HelloWorldService) {
    }

    async execute() {
        this.helloWorld.helloWorld();
    }
}

new App({
    controllers: [TestCommand],
    imports: [
        new MyModule(),
    ]
}).run();

Lesen Sie das Kapitel Dependency Injection um mehr darüber zu erfahren.

Konfiguration

Im Deepkit Framework können Module und Ihre Anwendung über Konfigurationsoptionen verfügen. Eine Konfiguration kann z.B. aus Datenbank-URLs, Passwörtern, IPs usw. bestehen. Services, HTTP/RPC/CLI Controller sowie Template Funktionen können diese Konfigurationsoptionen über Dependency Injection auslesen.

Eine Konfiguration kann durch die Definition einer Klasse mit Eigenschaften definiert werden. Dies ist ein typsicherer Weg, um eine Konfiguration für Ihre gesamte Anwendung zu definieren, und ihre Werte werden automatisch serialisiert und validiert.

Beispiel

import { MinLength } from '@deepkit/type';
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http } from '@deepkit/http';

class Config {
    pageTitle: string & MinLength<2> = 'Cool site';
    domain: string = 'example.com';
    debug: boolean = false;
}

class MyWebsite {
    constructor(protected allSettings: Config) {
    }

    @http.GET()
    helloWorld() {
        return 'Hello from ' + this.allSettings.pageTitle + ' via ' + this.allSettings.domain;
    }
}

new App({
    config: Config,
    controllers: [MyWebsite],
    imports: [new FrameworkModule]
}).run();
$ curl http://localhost:8080/
Hello from Cool site via example.com

Konfigurationsklasse

import { MinLength } from '@deepkit/type';

export class Config {
    title!: string & MinLength<2>; //this makes it required and needs to be provided
    host?: string;

    debug: boolean = false; //default values are supported as well
}
import { createModule } from '@deepkit/app';
import { Config } from './module.config.ts';

export class MyModule extends createModule({
   config: Config
}) {}

Die Werte für die Konfigurationsoptionen können entweder im Konstruktor des Moduls, mit der Methode .configure() oder über Konfigurationslader (z.B. Umgebungsvariablenlader) bereitgestellt werden.

import { MyModule } from './module.ts';

new App({
   imports: [new MyModule({title: 'Hello World'}],
}).run();

Um die Konfigurationsoptionen eines importierten Moduls dynamisch zu ändern, können Sie den process Hook verwenden. Dies ist ein guter Ort, um entweder Konfigurationsoptionen umzuleiten oder ein importiertes Modul abhängig von der aktuellen Modulkonfiguration oder anderen Modulinstanzinformationen einzurichten.

import { MyModule } from './module.ts';

export class MainModule extends createModule({
}) {
    process() {
        this.getImportedModuleByClass(MyModule).configure({title: 'Changed'});
    }
}

Auf der Anwendungsebene funktioniert es etwas anders:

new App({
    imports: [new MyModule({title: 'Hello World'}],
})
    .setup((module, config) => {
        module.getImportedModuleByClass(MyModule).configure({title: 'Changed'});
    })
    .run();

Wenn das Root-Anwendungsmodul aus einem regulären Modul erstellt wird, funktioniert es ähnlich wie reguläre Module.

class AppModule extends createModule({
}) {
    process() {
        this.getImportedModuleByClass(MyModule).configure({title: 'Changed'});
    }
}

App.fromModule(new AppModule()).run();

Konfigurationsoptionen Auslesen

Um eine Konfigurationsoption in einem Dienst zu verwenden, können Sie die normale Dependency Injection verwenden. Es ist möglich, entweder das gesamte Konfigurationsobjekt, einen einzelnen Wert oder einen Teil der Konfiguration zu injizieren.

Partial

Um nur einen Teilbereich der Konfigurationswerte zu injizieren, verwenden Sie den Typ Pick.

import { Config } from './module.config';

export class MyService {
     constructor(private config: Pick<Config, 'title' | 'host'}) {
     }

     getTitle() {
         return this.config.title;
     }
}


//In unit tests, it can be instantiated via
new MyService({title: 'Hello', host: '0.0.0.0'});

//or you can use type aliases
type MyServiceConfig = Pick<Config, 'title' | 'host'};
export class MyService {
     constructor(private config: MyServiceConfig) {
     }
}

Single value

Um nur einen einzigen Wert zu injizieren, verwenden Sie den Indexzugriffsoperator.

import { Config } from './module.config';

export class MyService {
     constructor(private title: Config['title']) {
     }

     getTitle() {
         return this.title;
     }
}

All

To inject all config values, use the class as dependency.

import { Config } from './module.config';

export class MyService {
     constructor(private config: Config) {
     }

     getTitle() {
         return this.config.title;
     }
}

Debugger

Die Konfigurationswerte Ihrer Anwendung und aller Module können im Debugger angezeigt werden. Aktivieren Sie die Debug-Option im FrameworkModul und öffnen Sie http://localhost:8080/_debug/configuration.

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

new App({
    config: Config,
    controllers: [MyWebsite],
    imports: [
        new FrameworkModule({
            debug: true,
        })
    ]
}).run();
debugger configuration

Sie können auch ts-node app.ts app:config verwenden, um alle verfügbaren Konfigurationsoptionen, den aktiven Wert, ihren Standardwert, die Beschreibung und den Datentyp anzuzeigen.

$ ts-node app.ts app:config
Application config
┌─────────┬───────────────┬────────────────────────┬────────────────────────┬─────────────┬───────────┐
│ (index) │     name      │         value          │      defaultValue      │ description │   type    │
├─────────┼───────────────┼────────────────────────┼────────────────────────┼─────────────┼───────────┤
│    0    │  'pageTitle'  │     'Other title'      │      'Cool site'       │     ''      │ 'string'  │
│    1    │   'domain'    │     'example.com'      │     'example.com'      │     ''      │ 'string'  │
│    2    │    'port'     │          8080          │          8080          │     ''      │ 'number'  │
│    3    │ 'databaseUrl' │ 'mongodb://localhost/' │ 'mongodb://localhost/' │     ''      │ 'string'  │
│    4    │    'email'    │         false          │         false          │     ''      │ 'boolean' │
│    5    │ 'emailSender' │       undefined        │       undefined        │     ''      │ 'string?' │
└─────────┴───────────────┴────────────────────────┴────────────────────────┴─────────────┴───────────┘
Modules config
┌─────────┬──────────────────────────────┬─────────────────┬─────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────┐
│ (index) │           name               │      value      │  defaultValue   │                                            description                                             │    type    │
├─────────┼──────────────────────────────┼─────────────────┼─────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────┤
│    0    │       'framework.host'       │   'localhost'   │   'localhost'   │                                                 ''                                                 │  'string'  │
│    1    │       'framework.port'       │      8080       │      8080       │                                                 ''                                                 │  'number'  │
│    2    │    'framework.httpsPort'     │    undefined    │    undefined    │ 'If httpsPort and ssl is defined, then the https server is started additional to the http-server.' │ 'number?'  │
│    3    │    'framework.selfSigned'    │    undefined    │    undefined    │           'If for ssl: true the certificate and key should be automatically generated.'            │ 'boolean?' │
│    4    │ 'framework.keepAliveTimeout' │    undefined    │    undefined    │                                                 ''                                                 │ 'number?'  │
│    5    │       'framework.path'       │       '/'       │       '/'       │                                                 ''                                                 │  'string'  │
│    6    │     'framework.workers'      │        1        │        1        │                                                 ''                                                 │  'number'  │
│    7    │       'framework.ssl'        │      false      │      false      │                                       'Enables HTTPS server'                                       │ 'boolean'  │
│    8    │    'framework.sslOptions'    │    undefined    │    undefined    │                   'Same interface as tls.SecureContextOptions & tls.TlsOptions.'                   │   'any'    │
...

Konfigurationswerte setzen

Standardmäßig werden keine Werte überschrieben, es werden also Standardwerte verwendet. Es gibt mehrere Möglichkeiten, Konfigurationswerte zu setzen.

  • Umgebungsvariablen für jede Option

  • Umgebungsvariable über JSON

  • dotenv-Dateien

Sie können mehrere Methoden zum Laden der Konfiguration gleichzeitig verwenden. Die Reihenfolge, in der sie aufgerufen werden, ist dabei wichtig.

Environment variables

Um die Einstellung jeder Konfigurationsoption über eine eigene Umgebungsvariable zu ermöglichen, verwenden Sie loadConfigFromEnv. Das Standardpräfix ist APP_, aber Sie können es ändern. Es lädt auch automatisch .env-Dateien. Standardmäßig wird eine Benennungsstrategie mit Großbuchstaben verwendet, aber auch das können Sie ändern.

Für Konfigurationsoptionen wie oben pageTitle, können Sie APP_PAGE_TITLE="Anderer Titel" verwenden, um den Wert zu verändern.

new App({
    config: config,
    controllers: [MyWebsite],
})
    .loadConfigFromEnv({prefix: 'APP_'})
    .run();
APP_PAGE_TITLE="Other title" ts-node app.ts server:start

JSON environment variable

Um mehrere Konfigurationsoptionen über eine einzige Umgebungsvariable zu ändern, verwenden Sie loadConfigFromEnvVariable. Das erste Argument ist der Name der Umgebungsvariablen.

new App({
    config: config,
    controllers: [MyWebsite],
})
    .loadConfigFromEnvVariable('APP_CONFIG')
    .run();
APP_CONFIG='{"pageTitle": "Other title"}' ts-node app.ts server:start

DotEnv Dateien

Um mehrere Konfigurationsoptionen über eine dotenv-Datei zu ändern, verwenden Sie loadConfigFromEnv. Das erste Argument ist entweder ein Pfad zu einer dotenv (relativ zu cwd) oder mehrere Pfade. Wenn es ein Array ist, wird jeder Pfad ausprobiert, bis eine vorhandene Datei gefunden wird.

new App({
    config: config,
    controllers: [MyWebsite],
})
    .loadConfigFromEnv({envFilePath: ['production.dotenv', 'dotenv']})
    .run();
$ cat dotenv
APP_PAGE_TITLE=Other title
$ ts-node app.ts server:start

Module Configuration

Jedes importierte Modul kann einen Modulnamen haben. Dieser Name wird für die oben verwendeten Konfigurationspfade verwendet.

Für die Konfiguration von Umgebungsvariablen lautet der Pfad für die FrameworkModule-Option port beispielsweise FRAMEWORK_PORT. Alle Namen werden standardmäßig in Großbuchstaben geschrieben. Wenn ein Präfix von APP_ verwendet wird, kann der Port über folgendes geändert werden:

$ APP_FRAMEWORK_PORT=9999 ts-node app.ts server:start
2021-06-12T18:59:26.363Z [LOG] Start HTTP server, using 1 workers.
2021-06-12T18:59:26.365Z [LOG] HTTP MyWebsite
2021-06-12T18:59:26.366Z [LOG]     GET / helloWorld
2021-06-12T18:59:26.366Z [LOG] HTTP listening at http://localhost:9999/

In Dotenv-Dateien wäre es auch APP_FRAMEWORK_PORT=9999.

In JSON-Umgebungsvariablen über loadConfigFromEnvVariable('APP_CONFIG') hingegen ist es die Struktur der eigentlichen Konfigurationsklasse. framework wird zu einem Objekt.

$ APP_CONFIG='{"framework": {"port": 9999}}' ts-node app.ts server:start

Dies funktioniert für alle Module gleich. Für die Konfigurationsoption Ihrer Anwendung (new App) ist kein Modulpräfix erforderlich.

Application Server

Public Directory

Das FrameworkModule bietet eine Möglichkeit, statische Dateien wie Bilder, PDFs, Binärdateien usw. über HTTP bereitzustellen. Mit der Konfigurationsoption publicDir können Sie angeben, welcher Ordner als Standard-Einstiegspunkt für Anfragen verwendet werden soll, die nicht zu einer HTTP-Controller-Route führen. Standardmäßig ist dieses Verhalten deaktiviert (leerer Wert).

Um die Bereitstellung öffentlicher Dateien zu aktivieren, setzen Sie publicDir auf einen Ordner Ihrer Wahl. Normalerweise würden Sie einen Namen wie publicDir wählen, um die Dinge offensichtlich zu machen.

.
├── app.ts
└── publicDir
    └── logo.jpg

Um die Option publicDir zu ändern, können Sie das erste Argument von FrameworkModule ändern.

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

// your config and http controller here

new App({
    config: config,
    controllers: [MyWebsite],
    imports: [
        new FrameworkModule({
            publicDir: 'publicDir'
        })
    ]
})
    .run();

Alle Dateien innerhalb dieses konfigurierten Ordners sind nun über HTTP zugänglich. Wenn Sie beispielsweise http://localhost:8080/logo.jpg öffnen, sehen Sie das Bild logo.jpg im Verzeichnis publicDir.

File Structure

Database

Deepkit verfügt über eine eigene leistungsstarke Datenbankabstraktionsbibliothek namens Deepkit ORM. Es handelt sich um eine ORM-Bibliothek (Object-Relational Mapping), die die Arbeit mit SQL-Datenbanken und MongoDB erleichtert.

Obwohl Sie jede beliebige Datenbankbibliothek verwenden können, empfehlen wir Deepkit ORM, da es die schnellste TypeScript-Datenbankabstraktionsbibliothek ist, die perfekt in das Deepkit-Framework integriert ist und viele Funktionen hat, die Ihren Workflow und Ihre Effizienz verbessern.

Um alle Informationen über Deepkit ORM zu erhalten, lesen Sie das Kapitel Database.

Database Klassen

Die einfachste Art, das Database-Objekt von Deepkit ORM innerhalb der Applikation zu verwenden, ist das Registrieren einer Klasse, die davon ableitet.

import { Database } from '@deepkit/orm';
import { SQLiteDatabaseAdapter } from '@deepkit/sqlite';
import { User } from './models';

export class SQLiteDatabase extends Database {
    name = 'default';
    constructor() {
        super(new SQLiteDatabaseAdapter('/tmp/myapp.sqlite'), [User]);
    }
}

Erstellen Sie eine neue Klasse und geben Sie in ihrem Konstruktor den Adapter mit seinen Parametern an und fügen Sie dem zweiten Parameter alle Entitäten/Modelle hinzu, die mit dieser Datenbank verbunden sein sollen.

Sie können nun diese Datenbankklasse als Provider registrieren. Wir aktivieren auch migrateOnStartup, das alle Tabellen in Ihrer Datenbank automatisch beim Bootstrap erstellt. Dies ist ideal für Rapid Prototyping, wird aber für ein ernsthaftes Projekt oder eine Produktionseinrichtung nicht empfohlen. Hier sollten dann normale Datenbank Migrationen verwendet werden.

Außerdem aktivieren wir debug, was uns erlaubt, den Debugger zu öffnen, wenn der Server der Anwendung gestartet wird, und Ihre Datenbankmodelle direkt in seinem integrierten ORM-Browser zu verwalten.

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { SQLiteDatabase } from './database.ts';

new App({
    providers: [SQLiteDatabase],
    imports: [
        new FrameworkModule({
            migrateOnStartup: true,
            debug: true,
        })
    ]
}).run();

Sie können nun überall auf SQLiteDatabase zugreifen, indem Sie Dependency Injection verwenden:

import { SQLiteDatabase } from './database.ts';

export class Controller {
    constructor(protected database: SQLiteDatabase) {}

    @http.GET()
    async startPage(): Promise<User[]> {
        //return all users
        return await this.database.query(User).find();
    }
}

Mehr Datenbanken

Sie können so viele Datenbankklassen hinzufügen, wie Sie möchten, und sie so benennen, wie Sie möchten. Achten Sie darauf, den Namen jeder Datenbank zu ändern, damit sie bei der Verwendung des ORM-Browsers nicht mit anderen in Konflikt gerät.

Daten Verwalten

Sie haben jetzt alles eingerichtet, um Ihre Datenbankdaten mit dem Deepkit ORM Browser zu verwalten. Um den ORM-Browser zu öffnen und den Inhalt zu verwalten, schreiben Sie alle Schritte von oben in die Datei app.ts und starten den Server.

$ ts-node app.ts server:start
2021-06-11T15:08:54.330Z [LOG] Start HTTP server, using 1 workers.
2021-06-11T15:08:54.333Z [LOG] Migrate database default
2021-06-11T15:08:54.336Z [LOG] RPC DebugController deepkit/debug/controller
2021-06-11T15:08:54.337Z [LOG] RPC OrmBrowserController orm-browser/controller
2021-06-11T15:08:54.337Z [LOG] HTTP OrmBrowserController
2021-06-11T15:08:54.337Z [LOG]     GET /_orm-browser/query httpQuery
2021-06-11T15:08:54.337Z [LOG] HTTP StaticController
2021-06-11T15:08:54.337Z [LOG]     GET /_debug/:any serviceApp
2021-06-11T15:08:54.337Z [LOG] HTTP listening at http://localhost:8080/
debugger database

Sie können das ER-Diagramm sehen. Im Moment ist nur eine Entität verfügbar. Wenn Sie weitere mit Beziehungen hinzufügen, sehen Sie alle Informationen auf einen Blick.

Wenn Sie in der linken Seitenleiste auf User klicken, können Sie dessen Inhalt verwalten. Klicken Sie auf das +-Symbol, und ändern Sie den Titel des neuen Datensatzes. Nachdem Sie die erforderlichen Werte (wie den Benutzernamen) geändert haben, klicken Sie auf "Bestätigen". Dadurch werden alle Änderungen an die Datenbank übertragen und bleiben dauerhaft bestehen. Die Autoinkrement-ID wird automatisch zugewiesen.

debugger database user

Mehr Lernen

Um mehr über die Funktionsweise von SQLiteDatabase zu erfahren, lesen Sie bitte das Kapitel Database und seine Unterkapitel, wie z.B. die Abfrage von Daten, die Manipulation von Daten über Sessions, die Definition von Relationen und vieles mehr. Bitte beachten Sie, dass sich die Kapitel dort auf die eigenständige Bibliothek @deepkit/orm beziehen und keine Dokumentation über den Teil des Deepkit Frameworks enthalten, den Sie oben in diesem Kapitel gelesen haben. In der Standalone-Bibliothek instanziieren Sie Ihre Datenbankklasse manuell, z. B. über new SQLiteDatabase(). In Ihrer Deepkit-Framework-Anwendung wird dies jedoch automatisch mithilfe des Dependency Injection Containers durchgeführt.

Migration

Logger

Deepkit Logger ist eine eigenständige Bibliothek mit einer primären Klasse Logger, die Sie zur Protokollierung von Informationen verwenden können. Diese Klasse wird automatisch im Dependency Injection Container Ihrer Deepkit Framework-Anwendung bereitgestellt.

Die Klasse Logger verfügt über mehrere Methoden, die sich jeweils wie console.log verhalten.

Name

Log Level

Level id

logger.error()

Error

1

logger.warning()

Warning

2

logger.log()

Default log

3

logger.info()

Special information

4

logger.debug()

Debug information

5

Standardmäßig hat ein Logger den Level "info", d.h. er verarbeitet nur Info-Meldungen und mehr (d.h. log, warning, error, aber nicht debug). Um den Log-Level zu ändern, rufen Sie zum Beispiel logger.level = 5 auf.

Benutzen in der Anwendung

Um den Logger in Ihrer Deepkit-Framework-Anwendung zu verwenden, können Sie einfach Logger in Ihre Services oder Controller injizieren.

import { Logger } from '@deepkit/logger';

class MyService {
    constructor(protected logger: Logger) {}

    doSomething() {
        const value = 'yes';
        this.logger.log('This is wild', value);
    }
}

Farben

Der Logger unterstützt farbige Protokollmeldungen. Sie können Farben bereitstellen, indem Sie XML-Tags verwenden, die den Text umgeben, der in Farbe erscheinen soll.

const username = 'Peter';
logger.log(`Hi <green>${username}</green>`);

Bei Transportern, die keine Farben unterstützen, werden die Farbinformationen automatisch entfernt. Im Standardtransporter (ConsoleTransport) wird die Farbe angezeigt. Die folgenden Farben sind verfügbar: black, red, green, blue, cyan, magenta, white und grey/gray.

Transporter

Sie können einen einzelnen oder mehrere Transporter konfigurieren. In einer Deepkit Framework-Anwendung wird der Transporter ConsoleTransport automatisch konfiguriert. Um zusätzliche Transporter zu konfigurieren, können Sie Setup Calls verwenden:

import { Logger, LoggerTransport } from '@deepkit/logger';


export class MyTransport implements LoggerTransport {
    write(message: string, level: LoggerLevel, rawMessage: string) {
        process.stdout.write(JSON.stringify({message: rawMessage, level, time: new Date}) + '\n');
    }

    supportsColor() {
        return false;
    }
}

new App()
    .setup((module, config) => {
        module.setupProvider(Logger).addTransport(new MyTransport);
    })
    .run();

Um alle Transporter durch eine neue Gruppe von Transportern zu ersetzen, verwenden Sie setTransport:

import { Logger } from '@deepkit/logger';

new App()
.setup((module, config) => {
    module.setupProvider(Logger).setTransport([new MyTransport]);
})
.run();
import { Logger, JSONTransport } from '@deepkit/logger';

new App()
    .setup((module, config) => {
        module.setupProvider(Logger).setTransport([new JSONTransport]);
    })
    .run();

Formatter

Mit Formatierern können Sie das Nachrichtenformat ändern, z. B. den Zeitstempel hinzufügen. Wenn eine Anwendung über server:start gestartet wird, wird automatisch ein DefaultFormatter hinzugefügt (der Zeitstempel, Bereich und Protokollstufe hinzufügt), wenn kein anderer Formatter vorhanden ist.

Scoped Logger

Scoped Logger fügen jedem Protokolleintrag einen beliebigen Bereichsnamen hinzu, der hilfreich sein kann, um festzustellen, aus welchem Teilbereich Ihrer Anwendung der Protokolleintrag stammt.

const scopedLogger = logger.scoped('database');
scopedLogger.log('Query', query);

JSON Transporter

Um die Ausgabe in JSON-Protokolle zu ändern, können Sie den mitgelieferten JSONTransport verwenden.

Context Data

Um einem Protokolleintrag kontextbezogene Daten hinzuzufügen, fügen Sie ein einfaches Objektliteral als letztes Argument hinzu. Nur Protokollaufrufe mit mindestens zwei Argumenten können kontextbezogene Daten enthalten.

const query = 'SELECT *';
const user = new User;
logger.log('Query', {query, user}); //last argument is context data
logger.log('Another', 'wild log entry', query, {user}); //last argument is context data

logger.log({query, user}); //this is not handled as context data.

Auto-CRUD

Events

Deepkit Framework kommt mit diversen Event-Tokens, auf die Event-Listener registriert werden können.

Siehe das Kapitel Events, um mehr darüber zu erfahren, wie Events funktionieren.

Dispatch Events

Events werden über die Klasse EventDispatcher gesendet. In einer Deepkit Framework Applikation kann dieser über Dependency Injection bereitgestellt werden.

import { cli, Command } from '@deepkit/app';
import { EventDispatcher } from '@deepkit/event';

@cli.controller('test')
export class TestCommand implements Command {
    constructor(protected eventDispatcher: EventDispatcher) {
    }

    async execute() {
        this.eventDispatcher.dispatch(UserAdded, new UserEvent({ username: 'Peter' }));
    }
}

Event Listener

Es gibt zwei Arten auf Events zu reagieren. Entweder über Controller Klassen oder reguläre Funktionen. Beide werden in der App oder in Modulen unter listeners registriert.

Controller Listener

import { eventDispatcher } from '@deepkit/event';

class MyListener {
    @eventDispatcher.listen(UserAdded)
    onUserAdded(event: typeof UserAdded.event) {
        console.log('User added!', event.user.username);
    }
}

new App({
    listeners: [MyListener],
}).run();

Functional Listener

new App({
    listeners: [
        UserAdded.listen((event) => {
            console.log('User added!', event.user.username);
        });
    ],
}).run();

Framework Events

Deepkit Framework selbst hat mehrere Ereignisse aus dem Anwendungsserver, auf die Sie hören können.

Functional Listener

import { onServerMainBootstrap } from '@deepkit/framework';
new App({
    listeners: [
        onServerMainBootstrap.listen((event) => {
            console.log('User added!', event.user.username);
        });
    ],
}).run();
Name Description

onServerBootstrap

Called only once for application server bootstrap (for main process and workers).

onServerBootstrapDone

Called only once for application server bootstrap (for main process and workers) as soon as the application server has started.

onServerMainBootstrap

Called only once for application server bootstrap (in the main process).

onServerMainBootstrapDone

Called only once for application server bootstrap (in the main process) as soon as the application server has started

onServerWorkerBootstrap

Called only once for application server bootstrap (in the worker process).

onServerWorkerBootstrapDone

Called only once for application server bootstrap (in the worker process) as soon as the application server has started.

ServerShutdownEvent

Called when application server shuts down (in master process and each worker).

onServerMainShutdown

Called when application server shuts down in the main process.

onServerWorkerShutdown

Called when application server shuts down in the worker process.

Deployment

In diesem Kapitel erfahren Sie, wie Sie Ihre Anwendung in JavaScript kompilieren, für Ihre Produktionsumgebung konfigurieren und über Docker bereitstellen können.

TypeScript kompilieren

Nehmen wir an, Sie haben eine Anwendung wie diese in einer Datei app.ts:

#!/usr/bin/env ts-node-script
import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';
import { http } from '@deepkit/http';

class Config {
    title: string = 'DEV my Page';
}

class MyWebsite {
    constructor(protected title: Config['title']) {
    }

    @http.GET()
    helloWorld() {
        return 'Hello from ' + this.title;
    }
}

new App({
    config: Config,
    controllers: [MyWebsite],
    imports: [new FrameworkModule]
})
    .loadConfigFromEnv()
    .run();

Wenn Sie ts-node app.ts server:start verwenden, sehen Sie, dass alles korrekt funktioniert. In einer Produktionsumgebung würden Sie den Server in der Regel nicht mit ts-node starten. Sie würden ihn in JavaScript kompilieren und dann den Node verwenden. Dazu müssen Sie eine korrekte tsconfig.json mit den richtigen Konfigurationsoptionen haben. In der Sektion "Erste Applikation" ist Ihre tsconfig.json so konfiguriert, dass sie JavaScript im Ordner ./dist ausgibt. Wir gehen davon aus, dass Sie das auch so konfiguriert haben.

Wenn alle Compiler-Einstellungen korrekt sind und Ihr outDir auf einen Ordner wie z.B. dist zeigt, dann werden, sobald Sie den Befehl tsc in Ihrem Projekt ausführen, alle Ihre verlinkten Dateien in den Dateien in der tsconfig.json zu JavaScript kompiliert. Es reicht, wenn Sie Ihre Einstiegsdateien in dieser Liste angeben. Alle importierten Dateien werden ebenfalls automatisch kompiliert und müssen nicht explizit in die tsconfig.json eingefügt werden. tsc ist Teil von Typescript, wenn Sie npm install typescript installieren.

$ ./node_modules/.bin/tsc

Der TypeScript-Compiler gibt nichts aus, wenn er erfolgreich war. Sie können die Ausgabe von dist jetzt überprüfen.

$ tree dist
dist
└── app.js

Sie sehen, dass es nur eine Datei gibt. Sie können sie über node dist/app.js ausführen und erhalten die gleiche Funktionalität wie mit ts-node app.ts.

Für ein Deployment ist es wichtig, dass die TypeScript-Dateien korrekt kompiliert werden und alles direkt über Node funktioniert. Sie könnten nun einfach Ihren dist-Ordner einschließlich Ihrer node_modules verschieben und node dist/app.js server:start ausführen und Ihre App ist erfolgreich deployed. Sie würden jedoch andere Lösungen wie Docker verwenden, um Ihre Anwendung korrekt zu verpacken.

Konfiguration

In einer Produktionsumgebung würden Sie den Server nicht an localhost binden, sondern höchstwahrscheinlich an alle Geräte über 0.0.0.0. Wenn Sie nicht hinter einem Reverse-Proxy stehen, würden Sie auch den Port auf 80 einstellen. Um diese beiden Einstellungen zu konfigurieren, müssen Sie das FrameworkModule anpassen. Die beiden Optionen, die uns interessieren, sind host und port. Damit sie von außen über Umgebungsvariablen oder über .dotenv-Dateien konfiguriert werden können, müssen wir dies zunächst zulassen. Glücklicherweise hat unser obiger Code dies bereits mit der Methode loadConfigFromEnv() getan.

Bitte lesen Sie das Kapitel Konfiguration, um mehr darüber zu erfahren, wie Sie die Konfigurationsoptionen der Anwendung einstellen können.

Um zu sehen, welche Konfigurationsoptionen verfügbar sind und welchen Wert sie haben, können Sie den Befehl ts-node app.ts app:config verwenden. Sie können sie auch im Framework-Debugger sehen.

SSL

Es wird empfohlen (und manchmal auch vorgeschrieben), Ihre Anwendung über HTTPS mit SSL laufen zu lassen. Es gibt mehrere Optionen zur Konfiguration von SSL. Um SSL zu aktivieren, verwenden Sie framework.ssl und konfigurieren Sie dessen Parameter mit den folgenden Optionen.

Name Type Description

framework.ssl

boolean

Enables HTTPS server when true

framework.httpsPort

number?

If httpsPort and ssl is defined, then the https server is started additional to the http server.

framework.sslKey

string?

A file path to a ssl key file for https

framework.sslCertificate

string?

A file path to a certificate file for https

framework.sslCa

string?

A file path to a ca file for https

framework.sslCrl

string?

A file path to a crl file for https

framework.sslOptions

object?

Same interface as tls.SecureContextOptions & tls.TlsOptions.

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

// your config and http controller here

new App({
    config: Config,
    controllers: [MyWebsite],
    imports: [
        new FrameworkModule({
            ssl: true,
            selfSigned: true,
            sslKey: __dirname + 'path/ssl.key',
            sslCertificate: __dirname + 'path/ssl.cert',
            sslCA: __dirname + 'path/ssl.ca',
        })
    ]
})
    .run();

Local SSL

In der lokalen Entwicklungsumgebung können Sie selbstsignierte HTTPs mit der Option framework.selfSigned aktivieren.

import { App } from '@deepkit/app';
import { FrameworkModule } from '@deepkit/framework';

// your config and http controller here

new App({
    config: config,
    controllers: [MyWebsite],
    imports: [
        new FrameworkModule({
            ssl: true,
            selfSigned: true,
        })
    ]
})
    .run();
$ ts-node app.ts server:start
2021-06-13T18:04:01.563Z [LOG] Start HTTP server, using 1 workers.
2021-06-13T18:04:01.598Z [LOG] Self signed certificate for localhost created at var/self-signed-localhost.cert
2021-06-13T18:04:01.598Z [LOG] Tip: If you want to open this server via chrome for localhost, use chrome://flags/#allow-insecure-localhost
2021-06-13T18:04:01.606Z [LOG] HTTP MyWebsite
2021-06-13T18:04:01.606Z [LOG]     GET / helloWorld
2021-06-13T18:04:01.606Z [LOG] HTTPS listening at https://localhost:8080/

Wenn Sie diesen Server jetzt starten, ist Ihr HTTP-Server als HTTPS unter https://localhost:8080/ verfügbar. In Chrome erhalten Sie beim Öffnen dieser URL jetzt die Fehlermeldung "NET::ERR_CERT_INVALID", da selbstsignierte Zertifikate als Sicherheitsrisiko gelten: chrome://flags/#allow-insecure-localhost.

Testing

Die Services und Controller im Deepkit Framework sind so konzipiert, dass sie SOLID und sauberen Code unterstützen, der gut konzipiert, gekapselt und getrennt ist. Diese Eigenschaften machen den Code einfach zu testen.

Diese Dokumentation zeigt Ihnen, wie Sie ein Test-Framework namens Jest mit ts-jest einrichten können. Führen Sie dazu den folgenden Befehl aus, um jest und ts-jest zu installieren.

npm install jest ts-jest @types/jest

Jest benötigt ein paar Konfigurationsoptionen, um zu wissen, wo die Testanzüge zu finden sind und wie der TS-Code zu kompilieren ist. Fügen Sie die folgende Konfiguration zu Ihrer package.json hinzu:

{
  ...,

  "jest": {
    "transform": {
      "^.+\\.(ts|tsx)$": "ts-jest"
    },
    "testEnvironment": "node",
    "resolver": "@deepkit/framework/resolve",
    "testMatch": [
      "**/*.spec.ts"
    ]
  }
}

Ihre Testdateien sollten den Namen *.spec.ts tragen. Erstellen Sie eine Datei test.spec.ts mit folgendem Inhalt.

test('first test', () => {
    expect(1 + 1).toBe(2);
});

Mit dem Befehl jest können Sie nun alle Ihre Testanzüge auf einmal ausführen.

$ node_modules/.bin/jest
 PASS  ./test.spec.ts
  ✓ first test (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.23 s, estimated 1 s
Ran all test suites.

Bitte lesen Sie die Jest-Dokumentation, um mehr darüber zu erfahren, wie das Jest CLI-Tool funktioniert und wie Sie anspruchsvollere Tests und ganze Test-Suites schreiben können.

Unit Test

Wann immer möglich sollten Sie Ihre Services mit einem Unit-Test teste. Je einfacher, besser getrennt und besser definiert Ihre Service-Abhängigkeiten sind, desto einfacher ist es, sie zu testen. In diesem Fall können Sie einfache Tests wie den folgenden schreiben:

export class MyService {
    helloWorld() {
        return 'hello world';
    }
}
//
import { MyService } from './my-service.ts';

test('hello world', () => {
    const myService = new MyService();
    expect(myService.helloWorld()).toBe('hello world');
});

Integration tests

Es ist nicht immer möglich, Unit-Tests zu schreiben, und es ist auch nicht immer der effizienteste Weg, um geschäftskritischen Code und Verhalten abzudecken. Besonders wenn Ihre Architektur sehr komplex ist, ist es von Vorteil, wenn Sie einfach End-to-End-Integrationstests durchführen können.

Wie Sie bereits im Kapitel Dependency Injection gelernt haben, ist der Dependency Injection Container das Herzstück von Deepkit. Hier werden alle Dienste aufgebaut und betrieben. Ihre Anwendung definiert Dienste (Provider), Controller, Listener und Importe. Bei Integrationstests wollen Sie nicht unbedingt alle Dienste in einem Testfall zur Verfügung haben, aber Sie wollen in der Regel eine abgespeckte Version der Anwendung zur Verfügung haben, um die kritischen Bereiche zu testen.

import { createTestingApp } from '@deepkit/framework';
import { http, HttpRequest } from '@deepkit/http';

test('http controller', async () => {
    class MyController {

        @http.GET()
        hello(@http.query() text: string) {
            return 'hello ' + text;
        }
    }

    const testing = createTestingApp({ controllers: [MyController] });
    await testing.startServer();

    const response = await testing.request(HttpRequest.GET('/').query({text: 'world'}));

    expect(response.getHeader('content-type')).toBe('text/plain; charset=utf-8');
    expect(response.body.toString()).toBe('hello world');
});
import { createTestingApp } from '@deepkit/framework';

test('service', async () => {
    class MyService {
        helloWorld() {
            return 'hello world';
        }
    }

    const testing = createTestingApp({ providers: [MyService] });

    //access the dependency injection container and instantiate MyService
    const myService = testing.app.get(MyService);

    expect(myService.helloWorld()).toBe('hello world');
});

Wenn Sie Ihre Anwendung in mehrere Module aufgeteilt haben, können Sie diese leichter testen. Nehmen wir zum Beispiel an, Sie haben ein AppCoreModul erstellt und möchten einige Services testen.

class Config {
    items: number = 10;
}

export class MyService {
    constructor(protected items: Config['items']) {

    }

    doIt(): boolean {
        //do something
        return true;
    }
}

export AppCoreModule = new AppModule({
    config: config,
    provides: [MyService]
}, 'core');

Sie verwenden Ihr Modul wie folgt:

import { AppCoreModule } from './app-core.ts';

new App({
    imports: [new AppCoreModule]
}).run();

Und testen Sie es, ohne den gesamten Anwendungsserver zu booten.

import { createTestingApp } from '@deepkit/framework';
import { AppCoreModule, MyService } from './app-core.ts';

test('service simple', async () => {
    const testing = createTestingApp({ imports: [new AppCoreModule] });

    const myService = testing.app.get(MyService);
    expect(myService.doIt()).toBe(true);
});

test('service simple big', async () => {
    // you change configurations of your module for specific test scenarios
    const testing = createTestingApp({
        imports: [new AppCoreModule({items: 100})]
    });

    const myService = testing.app.get(MyService);
    expect(myService.doIt()).toBe(true);
});