Browse Source

Adds typescript-rest framework (#4114)

Julian Coleman 6 years ago
parent
commit
bf42768cf3

+ 1 - 1
.travis.yml

@@ -54,7 +54,7 @@ env:
      - "TESTDIR=Java/jlhttp"
      - "TESTDIR=Java/jlhttp"
      - "TESTDIR=Java/jooby"
      - "TESTDIR=Java/jooby"
      - "TESTDIR=Java/light-java"
      - "TESTDIR=Java/light-java"
-     - "TESTDIR=Java/micronaut"     
+     - "TESTDIR=Java/micronaut"
      - "TESTDIR=Java/minijax"
      - "TESTDIR=Java/minijax"
      - "TESTDIR=Java/nanohttpd"
      - "TESTDIR=Java/nanohttpd"
      - "TESTDIR=Java/netty"
      - "TESTDIR=Java/netty"

+ 27 - 0
frameworks/TypeScript/typescript-rest/benchmark_config.json

@@ -0,0 +1,27 @@
+{
+  "framework": "typescript-rest",
+  "tests": [
+    {
+      "default": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "update_url": "/updates?queries=",
+        "query_url": "/queries?queries=",
+        "db_url": "/db",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "framework": "express",
+        "platform": "NodeJS",
+        "language": "TypeScript",
+        "flavor": "NodeJS",
+        "os": "Linux",
+        "orm": "Full",
+        "database": "Postgres",
+        "database_os": "Linux",
+        "display_name": "typescript-rest",
+        "versus": "nodejs"
+      }
+    }
+  ]
+}

+ 14 - 0
frameworks/TypeScript/typescript-rest/nodemon.json

@@ -0,0 +1,14 @@
+{
+  "ignore": [
+    ".git",
+    "node_modules"
+  ],
+  "watch": [
+    "."
+  ],
+  "events": {
+    "restart": "fuser -k 3000/tcp"
+  },
+  "exec": "ts-node ./src/index.ts",
+  "ext": "ts"
+}

+ 28 - 0
frameworks/TypeScript/typescript-rest/package.json

@@ -0,0 +1,28 @@
+{
+  "name": "typescript-rest",
+  "version": "1.0.0",
+  "scripts": {
+    "dev": "nodemon --delay 1500ms",
+    "lint": "tslint -p tsconfig.json",
+    "start": "ts-node -r tsconfig-paths/register src/index.ts"
+  },
+  "dependencies": {
+    "express": "^4.16.3",
+    "knex": "^0.15.2",
+    "objection": "^1.3.0",
+    "pg": "^7.5.0",
+    "typescript-rest": "^1.7.0"
+  },
+  "devDependencies": {
+    "@types/express": "^4.16.0",
+    "@types/knex": "^0.14.26",
+    "@types/node": "^10.11.6",
+    "@types/pg": "^7.4.11",
+    "nodemon": "^1.18.4",
+    "ts-node": "^7.0.1",
+    "tsconfig-paths": "^3.6.0",
+    "tslint": "^5.11.0",
+    "tslint-config-airbnb": "^5.11.0",
+    "typescript": "^3.1.2"
+  }
+}

+ 12 - 0
frameworks/TypeScript/typescript-rest/src/config/Knexfile.ts

@@ -0,0 +1,12 @@
+// tslint:disable:max-line-length
+import { Config, PoolConfig } from "knex";
+
+const client: string = "pg"; // can also be "postgresql"
+const pool: Readonly<PoolConfig> = { min: 2, max: 10 };
+const connection: string = "postgres://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world";
+
+export default <Config> {
+  client,
+  connection,
+  pool,
+};

+ 22 - 0
frameworks/TypeScript/typescript-rest/src/controllers/db.ts

@@ -0,0 +1,22 @@
+import { GET, Path } from "typescript-rest";
+
+import randomNumber from "../helpers/randomNumber";
+import World from "../models/world";
+
+@Path("/db")
+export default class SingleQuery {
+  /**
+   * Implements the `Single query` test type.
+   */
+
+  @GET
+  async singleQuery(): Promise<World> {
+    const id: number = randomNumber();
+    const world: World = await World
+      .query()
+      .findById(id)
+      .throwIfNotFound();
+
+    return world;
+  }
+}

+ 19 - 0
frameworks/TypeScript/typescript-rest/src/controllers/json.ts

@@ -0,0 +1,19 @@
+import { GET, Path } from "typescript-rest";
+
+interface IResult {
+  message: string;
+}
+
+@Path("/json")
+export default class Json {
+  /**
+   * Implements the `JSON Serialization` test type. Under
+   * the hood, Express should have serialized the result
+   * with `res.json`.
+   */
+
+  @GET
+  json(): IResult {
+    return { message: "Hello, World!" };
+  }
+}

+ 15 - 0
frameworks/TypeScript/typescript-rest/src/controllers/plaintext.ts

@@ -0,0 +1,15 @@
+import * as express from "express";
+import { ContextResponse, GET, Path } from "typescript-rest";
+
+@Path("/plaintext")
+export default class Plaintext {
+  /**
+   * Implements the `Plaintext` test type.
+   */
+
+  @GET
+  plaintext(@ContextResponse response: express.Response): string {
+    response.contentType("text/plain"); // defaults to `text/html` for this function
+    return "Hello, World!";
+  }
+}

+ 30 - 0
frameworks/TypeScript/typescript-rest/src/controllers/queries.ts

@@ -0,0 +1,30 @@
+import { GET, Path, QueryParam } from "typescript-rest";
+
+import randomNumber from "../helpers/randomNumber";
+import sanitizeQueries from "../helpers/sanitizeQueries";
+
+import World from "../models/world";
+
+@Path("/queries")
+export default class MultipleQueries {
+  /**
+   * Implements the `Multiple queries` test type.
+   */
+
+  @GET
+  async multipleQueries(@QueryParam("queries") queries: string): Promise<World[]> {
+    const length: number = sanitizeQueries(queries);
+    const worlds: World[] = [];
+
+    // Use a for-loop here because Array.from is just not
+    // performant at all, but is really nice.
+    // https://jsbench.me/ntjn3s2t0y/1
+    for (let i = 0; i < length; i += 1) {
+      const id: number = randomNumber();
+      const world: World = await World.query().findById(id).throwIfNotFound();
+      worlds.push(world);
+    }
+
+    return worlds;
+  }
+}

+ 34 - 0
frameworks/TypeScript/typescript-rest/src/controllers/updates.ts

@@ -0,0 +1,34 @@
+import { QueryBuilder } from "objection";
+import { GET, Path, QueryParam } from "typescript-rest";
+
+import randomNumber from "../helpers/randomNumber";
+import sanitizeQueries from "../helpers/sanitizeQueries";
+
+import World from "../models/world";
+
+@Path("/updates")
+export default class DataUpdates {
+  /**
+   * Implements the `Data updates` test type.
+   */
+
+  @GET
+  async dataUpdates(@QueryParam("queries") queries: string): Promise<World[]> {
+    const length: number = sanitizeQueries(queries);
+    const worlds: QueryBuilder<World, World, World>[] = [];
+
+    for (let i = 0; i < length; i += 1) {
+      const id: number = randomNumber();
+      worlds.push(
+        World
+          .query()
+          .patch({ randomnumber: randomNumber() })
+          .findById(id)
+          .returning("*")
+          .throwIfNotFound()
+      );
+    }
+
+    return Promise.all(worlds);
+  }
+}

+ 24 - 0
frameworks/TypeScript/typescript-rest/src/helpers/defaultTo.ts

@@ -0,0 +1,24 @@
+// tslint:disable:max-line-length
+
+/**
+ * Returns the second argument if it is not `null`, `NaN`,
+ * or `undefined`; otherwise the first argument is returned.
+ * Arguments must be the same type. This function is not
+ * curried, so all arguments must be supplied at once.
+ *
+ * @function
+ * @sig a -> b -> a | b
+ * @param {a} def The default value.
+ * @param {b} val The value returned instead of `def` unles `val` is `null`, `NaN`, or `undefined`
+ * @return {a|b} The second value if it is not `null`, `NaN`, or `undefined`, otherwise the default value
+ * @example
+ *
+ *     const port = defaultTo(3000, +process.env.PORT);
+ */
+
+export default function<T>(def: T, val: T): T {
+  return val == null // handles `null` and `undefined`
+    || val !== val   // handles NaN
+    ? def
+    : val;
+}

+ 16 - 0
frameworks/TypeScript/typescript-rest/src/helpers/randomNumber.ts

@@ -0,0 +1,16 @@
+/**
+ * Generates a random number between 1 and 10000
+ *
+ * @function
+ * @returns {*} A number between 1 and 10000
+ * Math.floor(Math.random() * 10000) + 1}
+ */
+
+export default function (): number {
+  const max = 10000;
+  return truncate((Math.random() * max) + 1);
+}
+
+function truncate(n: number): number {
+  return n | 0;
+}

+ 21 - 0
frameworks/TypeScript/typescript-rest/src/helpers/sanitizeQueries.ts

@@ -0,0 +1,21 @@
+import defaultTo from "./defaultTo";
+
+/**
+ * Provides an upper limit on queries.
+ *
+ * @function
+ * @sig a -> b
+ * @param {a} queries A string representation of a number.
+ * @returns {1..500|n} A number-casted version of the provided string between 1 and 500.
+ */
+
+export default function (queries: string): number {
+  const int = defaultTo(1, parseInt(queries, undefined));
+  const max = 500;
+  const min = 1;
+
+  if (int > max) { return max; }
+  if (int < min) { return min; }
+
+  return int;
+}

+ 17 - 0
frameworks/TypeScript/typescript-rest/src/index.ts

@@ -0,0 +1,17 @@
+import * as cluster from "cluster";
+import * as os from "os";
+import Server from "./server";
+
+if (cluster.isMaster) {
+  const cpuCount: os.CpuInfo[] = os.cpus();
+
+  for (const cpu of cpuCount) {
+    cluster.fork();
+  }
+
+  cluster.on("exit", () => {
+    process.exit(1);
+  });
+} else {
+  new Server().start();
+}

+ 8 - 0
frameworks/TypeScript/typescript-rest/src/models/world.ts

@@ -0,0 +1,8 @@
+import { Model } from "objection";
+
+export default class World extends Model {
+  static tableName: string = "world";
+
+  readonly id: number;
+  randomnumber: number;
+}

+ 65 - 0
frameworks/TypeScript/typescript-rest/src/server.ts

@@ -0,0 +1,65 @@
+import * as express from "express";
+import * as Knex from "knex";
+import { Model } from "objection";
+import { Server } from "typescript-rest";
+
+import Knexfile from "./config/Knexfile";
+import defaultTo from "./helpers/defaultTo";
+
+import Plaintext from "./controllers/plaintext";
+import Json from "./controllers/json";
+import SingleQuery from "./controllers/db";
+import MultipleQueries from "./controllers/queries";
+import DataUpdates from "./controllers/updates";
+
+const DEFAULT_PORT = 3000;
+// @ts-ignore - process.env.PORT may be undefined, and
+// that's the point.
+const PORT = defaultTo(DEFAULT_PORT, +process.env.PORT);
+
+export default class ApiServer {
+  private readonly app: express.Application;
+
+  constructor() {
+    this.app = express();
+    this.app.set("etag", false);         // unsets the defaulted Etag header
+    this.app.set("x-powered-by", false); // unsets the defaulted X-Powered-By header
+
+    this.config();
+
+    Server.buildServices(
+      this.app,
+      Plaintext,
+      Json,
+      SingleQuery,
+      MultipleQueries,
+      DataUpdates
+    );
+  }
+
+  private config(): void {
+    // Sets the global header `Server` as a middleware. We
+    // are intentionally receiving and ignoring the `req`
+    // parameter, indicated by the underscore.
+    this.app.use((_, res, next): void => {
+      res.set("Server", "TypeScript-rest");
+
+      next();
+    });
+
+    // Initiatlize connection to the database and connect
+    // the knex query builder to our objection models.
+    const knex = Knex(Knexfile);
+    Model.knex(knex);
+  }
+
+  start(): void {
+    this.app.listen(PORT, (err: any) => {
+      if (err) {
+        throw err;
+      }
+
+      console.info(`Server listening on port ${PORT}`);
+    });
+  }
+}

+ 22 - 0
frameworks/TypeScript/typescript-rest/tsconfig.json

@@ -0,0 +1,22 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "moduleResolution": "node",
+    "strictNullChecks": true,
+    "noImplicitAny": true,
+    "noImplicitReturns": true,
+    "noImplicitThis": true,
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true,
+    "baseUrl": "./src",
+    "typeRoots": [
+      "./node_modules/@types"
+    ]
+  },
+  "include": [
+    "**/**.ts"
+  ],
+  "exclude": [
+    "node_modules"
+  ]
+}

+ 14 - 0
frameworks/TypeScript/typescript-rest/tslint.json

@@ -0,0 +1,14 @@
+{
+  "extends": "tslint-config-airbnb",
+  "rules": {
+    "import-name": false,
+    "no-magic-numbers": true,
+    "quotemark": [true, "double"],
+    "semicolon": true,
+    "trailing-comma": [true, {
+        "singleline": "never",
+        "multiline": { "functions": "never" }
+      }
+    ]
+  }
+}

+ 11 - 0
frameworks/TypeScript/typescript-rest/typescript-rest.dockerfile

@@ -0,0 +1,11 @@
+FROM node:10
+
+WORKDIR /home
+COPY . .
+
+ENV PORT 8080
+
+RUN rm -rf node_modules/
+RUN yarn install --pure-lockfile
+
+CMD ["yarn", "start"]