Coder Social home page Coder Social logo

gkyoe / workflow.ts Goto Github PK

View Code? Open in Web Editor NEW

This project forked from madeindjs/workflow.ts

0.0 0.0 0.0 157 KB

TypeScript + Sequelize ORM + Express.js

Home Page: https://rousseau-alexandre.fr/en/programming/2019/06/19/express-typescript.html

TypeScript 100.00%

workflow.ts's Introduction

graph_api.ts

The purpose of this article is to discover an implementation of anAPIRESTfull usingTypeScript.

TypeScript is a free and open source programming language developed by Microsoft to improve and secure the production of JavaScript code. (...) . The TypeScript code is transcompiled into JavaScript (...) TypeScript allows optional static typing of variables and functions (...) while maintaining the non-binding approach of JavaScript. Wikipedia - TypeScript

We will therefore set up a very basic graph_api system. We will create two models:

  • a node (node) which represents a simple step. It just contains a name' and anid'.
  • a link (link) that connects only two nodes with attributes from_id' andto_id'.

It's as simple as that.

To build the API, I will use[Express.js], a minimalist framework that allows us to make APIs in JavaScript. At the end of the article, the API will be able to generate a definition of a Mermaid graph which allows to convert the graph_api into a beautiful graph like the one below:

Mermaid example

Let's go!

NOTE: I'm going to go a little fast because it's a bit of a reminder for myself. All the code is available on the repository Github graph_api.ts

TL;DR: Express great freedom allows us to decide for ourselves the architecture of our application and TypeScript gives us the possibility to create real design paterns.

Setup project

Let's create a brand new project using NPM and Git versionning.

$ mkdir graph_api.ts
$ cd graph_api.ts/
$ npm init
$ git init

Then we start to install somes dependencies to build the HTTP server:

$ npm install --save express body-parser
$ npm install --save-dev typescript ts-node @types/express @types/node

As we use TypeScript we need to create a tsconfig.json to indicate how transcript TypeScript files from lib to dist folder. Also we transpile to ES6 version:

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "pretty": true,
    "sourceMap": true,
    "target": "es6",
    "outDir": "./dist",
    "baseUrl": "./lib"
  },
  "include": ["lib/**/*.ts"],
  "exclude": ["node_modules"]
}

Now we we'll create the the lib/app.ts. This will be in charge to configure, load routes and start Express server:

// lib/app.ts
import * as express from "express";
import * as bodyParser from "body-parser";
import { Routes } from "./config/routes";

class App {
  public app: express.Application;
  public routePrv: Routes = new Routes();

  constructor() {
    this.app = express();
    this.config();
    this.routePrv.routes(this.app);
  }

  private config(): void {
    this.app.use(bodyParser.json());
    this.app.use(bodyParser.urlencoded({ extended: false }));
  }
}

export default new App().app;

As you can see, we load the routes to define the controllers and the routes to comply with the MVC model. Here is the first NodesController which will handle actions related to nodes with an action index:

// lib/controllers/nodes.controller.ts
import { Request, Response } from "express";

export class NodesController {
  public index(req: Request, res: Response) {
    res.json({
      message: "Hello boi"
    });
  }
}

We will now separate the routes into a separate file:

// lib/config/routes.ts
import { Request, Response } from "express";
import { NodesController } from "../controllers/nodes.controller";

export class Routes {
  public nodesController: NodesController = new NodesController();

  public routes(app): void {
    app.route("/").get(this.nodesController.index);

    app.route("/nodes").get(this.nodesController.index);
  }
}

And a lib/server.ts file to start App object:

// lib/server.ts
import app from "./app";
const PORT = process.env.PORT || 3000;

app.listen(PORT, () => console.log(`Example app listening on port ${PORT}!`));

And that's it. You can start server using npm run dev and try API using cURL:

$ curl http://localhost:3000/nodes
{"message":"Hello boi"}

Setup sequelize

Sequelize is an ORM (Object Relational Mapping) which is in charge to translate TypeScript object in SQL queries to save models. The sequelize documentation about TypeScrypt is really complete but don't panics I'll show you how to implement with Express.

We start to add librairies:

$ npm install --save sequelize sqlite
$ npm install --save-dev @types/bluebird @types/validator @types/sequelize

You may notice I choose SQLite database because of simplicity but you can use MySQL or Postgres

Then we will create a lib/config/database.ts file to setup Sequelize database system. For simplicity, I create a Sqlite database in memory:

// lib/config/database.ts
import { Sequelize } from "sequelize";

export const database = new Sequelize({
  database: "some_db",
  dialect: "sqlite",
  storage: ":memory:"
});

Then we'll be able to create a model. We'll begin with Node model who extends Sequelize Model class:

// lib/models/node.model.ts
import { Sequelize, Model, DataTypes, BuildOptions } from "sequelize";
import { database } from "../config/database";

export class Node extends Model {
  public id!: number;
  public name!: string;
  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;
}
// ...

You can notice that I added two fields createdAt and updatedAt thatSequelize will fill automatically.

Then, we configure the SQL schema of the table and call Node.sync. This will create a table in the Sqlite database.

// lib/models/node.model.ts
// ...
Node.init(
  {
    id: {
      type: DataTypes.INTEGER.UNSIGNED,
      autoIncrement: true,
      primaryKey: true
    },
    name: {
      type: new DataTypes.STRING(128),
      allowNull: false
    }
  },
  {
    tableName: "nodes",
    sequelize: database // this bit is important
  }
);

Node.sync({ force: true }).then(() => console.log("Node table created"));

That's it!

Setup Node controller

Now we setup database, let's create simple CRUD methods in controller. This means:

  • index to show list of nodes
  • show to show a node
  • create to add a new node
  • update to edit a node
  • delete to remove a node

Index

// lib/controllers/nodes.controller.ts
import { Request, Response } from "express";
import { Node } from "../models/node.model";

export class NodesController {
  public index(req: Request, res: Response) {
    Node.findAll<Node>({})
      .then((nodes: Array<Node>) => res.json(nodes))
      .catch((err: Error) => res.status(500).json(err));
  }
}

And setup route:

// lib/config/routes.ts
import { Request, Response } from "express";
import { NodesController } from "../controllers/nodes.controller";

export class Routes {
  public nodesController: NodesController = new NodesController();

  public routes(app): void {
    // ...
    app.route("/nodes").get(this.nodesController.index);
  }
}

You can try the route using cURL:

$ curl http://localhost:3000/nodes/
[]

It's seem to work but we have not data in SQlite database yet. Let's continue to add them.

Create

We'll first define an interface which define properties we should receive from POST query. We only want to receive name property as String. We'll use this interface to cast req.body object properties. This will prevent user to inject a parameters who we not want to save in database. This is a good practice.

// lib/models/node.model.ts
// ...
export interface NodeInterface {
  name: string;
}

Now back in controller. We simply call Node.create and pass params from req.body. Then we'll use Promise to handle some errors:

// lib/controllers/nodes.controller.ts
// ...
import { Node, NodeInterface } from "../models/node.model";

export class NodesController {
  // ...
  public create(req: Request, res: Response) {
    const params: NodeInterface = req.body;

    Node.create<Node>(params)
      .then((node: Node) => res.status(201).json(node))
      .catch((err: Error) => res.status(500).json(err));
  }
}

And setup route:

// lib/config/routes.ts
// ...
export class Routes {
  // ...
  public routes(app): void {
    // ...
    app
      .route("/nodes")
      .get(this.nodesController.index)
      .post(this.nodesController.create);
  }
}

You can try the route using cURL:

$ curl -X POST --data "name=first" http://localhost:3000/nodes/
{"id":2,"name":"first","updatedAt":"2019-06-14T11:12:17.606Z","createdAt":"2019-06-14T11:12:17.606Z"}

It's seem work. Let's try with a bad request:

$ curl -X POST http://localhost:3000/nodes/
{"name":"SequelizeValidationError","errors":[{"message":"Node.name cannot be null","type":"notNull Violation",...]}

Perfect. We can now continue.

Show

The show method have a little difference because we need an id as GET parameter. This means we should have an URL like this: /nodes/1. It's simple to make this when you build the route. There the implementation.

// lib/config/routes.ts
// ...
export class Routes {
  // ...
  public routes(app): void {
    // ...
    app.route("/nodes/:id").get(this.nodesController.show);
  }
}

Now we can get the id parameter using req.params.id. THen we simply use Node.findByPk method and handle when this Promise get a null value which mean the node was not found. In this case, we return a 404 response:

// lib/controllers/nodes.controller.ts
// ...
export class NodesController {
  // ...
  public show(req: Request, res: Response) {
    const nodeId: number = req.params.id;

    Node.findByPk<Node>(nodeId)
      .then((node: Node | null) => {
        if (node) {
          res.json(node);
        } else {
          res.status(404).json({ errors: ["Node not found"] });
        }
      })
      .catch((err: Error) => res.status(500).json(err));
  }
}

Now it should be alright. Let's try it:

$ curl -X POST http://localhost:3000/nodes/1
{"id":1,"name":"first","createdAt":"2019-06-14T11:32:47.731Z","updatedAt":"2019-06-14T11:32:47.731Z"}
$ curl -X POST http://localhost:3000/nodes/99
{"errors":["Node not found"]}

Update

Update method seams like the previous one and need an id parameter. Let's build route:

// lib/config/routes.ts
// ...
export class Routes {
  // ...
  public routes(app): void {
    // ...
    app
      .route("/nodes/:id")
      .get(this.nodesController.show)
      .put(this.nodesController.update);
  }
}

Now we'll use the Node.update method which take two parameters:

  • an NodeInterface interface which contains properties to update
  • an UpdateOptions interface which contains the SQL WHERE constraint

Then Node.update return a Promise like many Sequelize methods.

// lib/controllers/nodes.controller.ts
import { UpdateOptions } from "sequelize";
// ...
export class NodesController {
  // ...
  public update(req: Request, res: Response) {
    const nodeId: number = req.params.id;
    const params: NodeInterface = req.body;

    const update: UpdateOptions = {
      where: { id: nodeId },
      limit: 1
    };

    Node.update(params, update)
      .then(() => res.status(202).json({ data: "success" }))
      .catch((err: Error) => res.status(500).json(err));
  }
}

Get it? Let's try it:

$ curl -X PUT --data "name=updated" http://localhost:3000/nodes/1
{"data":"success"}}

Beautiful. Let's continue.

Destroy

Destroy method seams like the previous one and need an id parameter. Let's build route:

// lib/config/routes.ts
// ...
export class Routes {
  // ...
  public routes(app): void {
    // ...
    app
      .route("/nodes/:id")
      .get(this.nodesController.show)
      .put(this.nodesController.update)
      .delete(this.nodesController.delete);
  }
}

For destroy method we call Node.destroy we take a DestroyOptions interface like Node.update.

// lib/controllers/nodes.controller.ts
// ...
import { UpdateOptions, DestroyOptions } from "sequelize";

export class NodesController {
  // ...
  public delete(req: Request, res: Response) {
    const nodeId: number = req.params.id;
    const options: DestroyOptions = {
      where: { id: nodeId },
      limit: 1
    };

    Node.destroy(options)
      .then(() => res.status(204).json({ data: "success" }))
      .catch((err: Error) => res.status(500).json(err));
  }
}

Let's try it:

$ curl -X DELETE  http://localhost:3000/nodes/1

Perfect!

Create the Link relationship

Now we want to create the second model: the link. It has two attributes:

  • from_id which will be a link on the previous node
  • to_id which will be a link on the next node

Setup CRUD

I will be quick on basic CRUD link implementation because this is the same than nodes CRUD:

The model:

// lib/models/node.model.ts
import { Model, DataTypes } from "sequelize";
import { database } from "../config/database";
import { Node } from "./node.model";

export class Link extends Model {
  public id!: number;
  public fromId!: number;
  public toId!: number;
  // timestamps!
  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;
}

export interface LinkInterface {
  name: string;
  fromId: number;
  toId: number;
}

Link.init(
  {
    id: {
      type: DataTypes.INTEGER.UNSIGNED,
      autoIncrement: true,
      primaryKey: true
    },
    fromId: {
      type: DataTypes.INTEGER.UNSIGNED,
      allowNull: false
    },
    toId: {
      type: DataTypes.INTEGER.UNSIGNED,
      allowNull: false
    }
  },
  {
    tableName: "links",
    sequelize: database
  }
);

Link.sync({ force: true }).then(() => console.log("Link table created"));

Controller:

// lib/controllers/links.controller.ts
import { Request, Response } from "express";
import { Link, LinkInterface } from "../models/link.model";
import { UpdateOptions, DestroyOptions } from "sequelize";

export class LinksController {
  public index(_req: Request, res: Response) {
    Link.findAll<Link>({})
      .then((links: Array<Link>) => res.json(links))
      .catch((err: Error) => res.status(500).json(err));
  }

  public create(req: Request, res: Response) {
    const params: LinkInterface = req.body;

    Link.create<Link>(params)
      .then((link: Link) => res.status(201).json(link))
      .catch((err: Error) => res.status(500).json(err));
  }

  public show(req: Request, res: Response) {
    const linkId: number = req.params.id;

    Link.findByPk<Link>(linkId)
      .then((link: Link | null) => {
        if (link) {
          res.json(link);
        } else {
          res.status(404).json({ errors: ["Link not found"] });
        }
      })
      .catch((err: Error) => res.status(500).json(err));
  }

  public update(req: Request, res: Response) {
    const linkId: number = req.params.id;
    const params: LinkInterface = req.body;

    const options: UpdateOptions = {
      where: { id: linkId },
      limit: 1
    };

    Link.update(params, options)
      .then(() => res.status(202).json({ data: "success" }))
      .catch((err: Error) => res.status(500).json(err));
  }

  public delete(req: Request, res: Response) {
    const linkId: number = req.params.id;
    const options: DestroyOptions = {
      where: { id: linkId },
      limit: 1
    };

    Link.destroy(options)
      .then(() => res.status(204).json({ data: "success" }))
      .catch((err: Error) => res.status(500).json(err));
  }
}

And routes:

// lib/config/routes.ts
import { Request, Response } from "express";
import { NodesController } from "../controllers/nodes.controller";
import { LinksController } from "../controllers/links.controller";

export class Routes {
  public nodesController: NodesController = new NodesController();
  public linksController: LinksController = new LinksController();

  public routes(app): void {
    // ...
    app
      .route("/links")
      .get(this.linksController.index)
      .post(this.linksController.create);

    app
      .route("/links/:id")
      .get(this.linksController.show)
      .put(this.linksController.update)
      .delete(this.linksController.delete);
  }
}

Now everything should work like node endpoints:

$ curl -X POST --data "fromId=420" --data "toId=666"  http://localhost:3000/links
$ curl http://localhost:3000/links
[{"id":1,"fromId":420,"toId":666,"createdAt":"2019-06-18T11:09:49.739Z","updatedAt":"2019-06-18T11:09:49.739Z"}]

It's seem good but you see what going wrong? Actually we setup toId and fromId to nonexisting nodes. Let's correct that.

Relationships

With sequelize we can easily build relationships between model using belongTo & hasMany. Let's do that:

// lib/models/node.model.ts
import { Link } from "./link.model";
// ...

Node.hasMany(Link, {
  sourceKey: "id",
  foreignKey: "fromId",
  as: "previousLinks"
});

Node.hasMany(Link, {
  sourceKey: "id",
  foreignKey: "toId",
  as: "nextLinks"
});

Node.sync({ force: true }).then(() => console.log("Node table created"));

Then restart the server using npm run dev. You may see the SQL query who create base:

Executing (default): CREATE TABLE IF NOT EXISTS `links` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `fromId` INTEGER NOT NULL REFERENCES `nodes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, `toId` INTEGER NOT NULL REFERENCES `nodes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);

You see the difference? Sequelize create foreign keys between nodes and links. Now we can't create a link with broken relationship:

$ curl -X POST --data "fromId=420" --data "toId=666"  http://localhost:3000/links
{"name":"SequelizeForeignKeyConstraintError"

Let's retry to create link with valid nodes:

$ curl -X POST --data "name=first"  http://localhost:3000/nodes
{"id":1,"name":"first","updatedAt":"2019-06-18T11:21:38.264Z","createdAt":"2019-06-18T11:21:38.264Z"}
$ curl -X POST --data "name=second"  http://localhost:3000/nodes
{"id":2,"name":"second","updatedAt":"2019-06-18T11:21:47.327Z","createdAt":"2019-06-18T11:21:47.327Z"}
$ curl -X POST --data "fromId=1" --data "toId=2"  http://localhost:3000/links
{"id":1,"fromId":"1","toId":"2","updatedAt":"2019-06-18T11:22:10.439Z","createdAt":"2019-06-18T11:22:10.439Z"}

Perfect! Sequelize allow you to do many things so I suggest you to take a look at their documentation.

Draw the graph

Now we'll use our model to draw a graph. To do that, using Mermaid.js who generate beautiful graph from plan text definitions. A valid definition look something like this:

graph TD;

1[first node];
2[second node];
3[third node];

1 --> 2;
2 --> 3

It's really simple. Let's do create a new route linked to a new

// lib/config/routes.ts
// ...
import { GraphController } from "../controllers/graph.controller";

export class Routes {
  public nodesController: NodesController = new NodesController();
  public linksController: LinksController = new LinksController();
  public graphController: GraphController = new GraphController();

  public routes(app): void {
    app.route("/").get(this.graphController.mermaid);
    // ...
  }
}

Then use Sequelize to get all the nodes and links to draw the graph.

I move all the code into a Promise to improve readability and error handling. It's a simple implementation so you might want to improve it (and you'd be right) but it's enough in my case.

// lib/controllers/build.controller.ts
import { Request, Response } from "express";
import { Link } from "../models/link.model";
import { Node } from "../models/node.model";

export class GraphController {
  public mermaid(_req: Request, res: Response) {
    // we'll englobe all logic into a big promise
    const getMermaid = new Promise<string>((resolve, reject) => {
      let graphDefinition = "graph TD;\r\n";

      Node.findAll({})
        // get all nodes and build mermaid definitions like this `1[The first node]`
        .then((nodes: Array<Node>) => {
          nodes.forEach((node: Node) => {
            graphDefinition += `${node.id}[${node.name}];\r\n`;
          });
        })
        // get all links
        .then(() => Link.findAll())
        // build all link in mermaid
        .then((links: Array<Link>) => {
          links.forEach((link: Link) => {
            graphDefinition += `${link.fromId} --> ${link.toId};\r\n`;
          });

          resolve(graphDefinition);
        })
        .catch((err: Error) => reject(err));
    });

    // call promise and return plain text
    getMermaid
      .then((graph: string) => res.send(graph))
      .catch((err: Error) => res.status(500).json(err));
  }
}
$ curl -X POST --data "name=first"  http://localhost:3000/nodes &&
  curl -X POST --data "name=second"  http://localhost:3000/nodes &&
  curl -X POST --data "name=third"  http://localhost:3000/nodes &&
  curl -X POST --data "fromId=1" --data "toId=2"  http://localhost:3000/links &&
  curl -X POST --data "fromId=2" --data "toId=3"  http://localhost:3000/links
~~~

And try the result output:

~~~bash
$ curl http://localhost:3000
graph TD;
1[first];
2[second];
3[third];
1 --> 2;
2 --> 3;

Beautiful!

Go further

We just build foundations of a graph_api system. We can easily con further. Here some ideas:

  • build unit test
  • add a names to links
  • add a names to links
  • add a Graph object who own some nodes
  • add an authentification using JWT token

As you can see, ExpressJS is a toolbox that interfaces very well with TypeScript. Express' great freedom allows us to decide for ourselves the architecture of our application and TypeScript gives us the possibility to create real design paterns.

Liens

workflow.ts's People

Contributors

madeindjs avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.