Untitled

mail@pastecode.io avatar
unknown
typescript
9 days ago
6.5 kB
1
Indexable
Never
import http from 'node:http';
import path from 'node:path';
import https from 'node:https';
import { readFileSync } from 'node:fs';
import { json, urlencoded, Application, Request, Response, NextFunction, Router } from 'express';
import cookieParser from 'cookie-parser';
import form from 'express-form-data';
import express from 'express';
import morgan from 'morgan';
import actuator from 'express-actuator';
import cors from 'cors';
import rateLimit from 'express-rate-limit';

// Local Services
import { hooks } from './hooks';
import { services } from './services';
import socket, { listen as wsListen } from './controllers/socket';
import SearchCtrl from './controllers/search/search';
import NewMailer from './controllers/email';
import * as operations from './controllers/operations';

// Settings
import settings from './settings';

// Controllers
import { imageUp } from './controllers/imageUp';
import gracefullShutdown from './controllers/gracefullShutdown';

// TypeScript Types
interface Dependency {
  method: (...args: any[]) => Promise<any>;
  args: any[];
}

interface AppConfig {
  origin: string;
  port: number;
  useHTTP2: boolean;
}

interface WebSocketEvent {
  method: Function;
  props: {
    ws: any;
    lyra: SearchCtrl;
    db: typeof operations;
    mail: ReturnType<typeof NewMailer>;
    settings: AppConfig;
  };
}

export default class App {
  private express: Application;
  private router: Router;
  private config: AppConfig;
  private search: SearchCtrl;
  private mail: ReturnType<typeof NewMailer>;
  private imageUp: typeof imageUp;
  private db: typeof operations;
  private events: Record<string, WebSocketEvent>;
  private wsMiddlewares: any[];
  private depPromises: Promise<any>[];
  private server!: http.Server | https.Server;
  private ssl?: { key: Buffer; cert: Buffer };
  private options?: { key: Buffer; cert: Buffer; allowHTTP1: boolean };
  private socket?: any;
  private ready: boolean = false;

  constructor({ deps }: { deps?: Dependency[] } = {}) {
    this.express = express();
    this.router = Router();
    this.config = settings;
    this.search = new SearchCtrl();
    this.mail = NewMailer(this.config);
    this.imageUp = imageUp;
    this.db = operations;
    this.events = {};
    this.wsMiddlewares = [];
    this.depPromises = [];

    if (deps) {
      this.depPromises = deps.map(
        ({ method, args }) =>
          new Promise((resolve, reject) =>
            method(...args)
              .then((r) => resolve(r))
              .catch((err) => reject(err))
          )
      );
    }

    Promise.all(this.depPromises)
      .then((res) => res && res.length > 0 && console.log(`=> '${res}'!`))
      .then(() => this.init())
      .catch((err) => console.log(err));
  }

  /**
   * Boots the actual express application
   */
  start(): void {
    const readyStatus = setInterval(() => {
      if (this.ready) {
        this.listen();
        clearInterval(readyStatus);
      }
    }, 300);
  }

  /**
   * Initialize Express app with dependencies
   * and then the http(s) server
   */
  private init(): void {
    const { parse } = form;

    // Rate Limiter
    const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
      standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
      legacyHeaders: false, // Disable the `X-RateLimit-*` headers
    });

    // Load the middlewares
    this.express.use(
      cors({
        origin: this.config.origin,
        credentials: true,
      })
    );
    this.express.use(morgan('common')); // Logger
    this.express.use(actuator({ infoGitMode: 'full' })); // Health Checker
    this.express.use(json()); // Parse JSON response
    this.express.use(urlencoded({ extended: false })); // Legacy URL encoding
    this.express.use(cookieParser()); // Parse cookies
    this.express.use(parse()); // Parse Form data as JSON
    this.express.use('/api', limiter, this.router); // All the API routes
    this.express.use(express.static(path.resolve(process.cwd(), '..', 'client'))); // REACT build files (Statics)

    if (this.config.useHTTP2) {
      // SSL configuration
      this.ssl = {
        key: readFileSync(path.resolve('ssl', 'privatekey.pem')),
        cert: readFileSync(path.resolve('ssl', 'certificate.pem')),
      };

      this.options = {
        ...this.ssl,
        allowHTTP1: true,
      };

      // HTTP/2 Server
      this.server = https.createServer(this.options, this.express);

      // Load the Hooks
      hooks(this);
    } else {
      // HTTP Server
      this.server = http.createServer(this.express);
    }

    // Start Search service
    this.search.start();

    // Socket Server
    this.socket = socket(this.server, { origin: this.config.origin });

    // Load the Services
    services(this);

    // Listen for events
    wsListen(this.socket, this.events, ...this.wsMiddlewares);

    this.ready = true;
    gracefullShutdown.call({ ...this });
  }

  /**
   * Listener for http requests
   */
  private listen(): void {
    // Serve Front-end
    this.express.get('*', (req: Request, res: Response) => {
      res.sendFile(path.resolve(process.cwd(), '..', 'client', 'index.html'));
    });

    // Boot the server
    this.server.listen(this.config.port, () => {
      console.log(`=> Listening on ${this.config.port}`);
    });
  }

  /**
   * Register Hooks
   * @param callback function of hook
   */
  hook(callback: Function): void {
    callback.call({ ...this });
  }

  /**
   * Configure service with api
   * @param callback function for services
   */
  configure(callback: Function): void {
    callback.call({
      ...this.express,
      route: this.router,
      ws: this.socket,
      imageUp: this.imageUp,
      lyra: this.search,
      db: this.db,
      mail: this.mail,
      settings: this.config,
    });
  }

  /**
   * Register events for ws with service
   * @param event
   * @param middlewares
   * @param callback
   */
  register(
    event: string,
    middlewares: any[] | Function,
    callback?: Function
  ): void {
    let callee;
    if (typeof middlewares === 'function') {
      callee = middlewares;
    } else {
      callee = callback;
      this.wsMiddlewares = middlewares;
    }
    this.events[event] = {
      method: callee!,
      props: {
        ws: this.socket,
        lyra: this.search,
        db: this.db,
        mail: this.mail,
        settings: this.config,
      },
    };
  }
}
Leave a Comment