Building A Modern MUSH With Typescript Part 5: Slaying the Databeast!
Tags: typescript, tutorial, node, mush
Published: 2020-03-17 22:15:20

Hello! Welcome back to my tutorial series where we're building a MUSH engine piece by piece. In [Part 4](https://dev.to/lcanady/building-a-modern-mush-with-typescript-part-4-starting-on-ursamajor-boc) we covered creating the `flag` and `file` systems! In this installment, we're going to build an adapter for our database, and then implement it using NeDB.

## Defining The Adapter
First things first, we need to define the database adapter.  In the spirit of keeping UrsaMU extendable, I decided to go with an adapter pattern instead of just wiring the game up to a single database. We're going to define our adapter stub in `src/api/mu.ts`
```JavaScript
export abstract class DbAdapter {
  abstract model(...args: any[]): any | Promise<any>;
  abstract get(...args: any[]): any | Promise<any>;
  abstract find(...args: any[]): any | Promise<any>;
  abstract create(...args: any[]): any | Promise<any>;
  abstract update(...args: any[]): any | Promise<any>;
  abstract delete(...args: any[]): any | Promise<any>;
}
``` 
Because it's an abstract class it's never meant to be called directly, instead any inheriting class will have to implement its methods.

## Writing our Database Code - src/api/database.ts
Now we'll extend the adapter class and make a module for NeDB.  In the future, we could swap the database out with anything we want, without affecting the game engine.
```JavaScript
import DataStore from "nedb";
import { DbAdapter } from "./mu.ts";
import { resolve } from "path";
```
Here we're defining the shape of our NeDB data. We'll pass it as a type when we instantiate the game object database.
```JavaScript
export interface DBObj {
  _id?: string;
  id: string;
  desc: string;
  name: string;
  type: "thing" | "player" | "room" | "exit";
  alias?: string;
  password?: string;
  attribites: Attribute[];
  flags: string[];
  location: string;
  contents: string[];
  exits?: string[];
  owner?: string;
}
```
Here, we use our first Generic! The `T` in `NeDB<T>` is a stand-in for whatever kind of type we want to check our typescript code against when manually entering new items into the database.  You'll notice that `T` is used in places for our returns instead of a predefined type. 
```JavaScript
export class NeDB<T> implements DbAdapter {
  path?: string;
  db: DataStore | undefined;

  constructor(path?: string) {
    this.path = path || "";
  }

  /** create the database model  */
  model() {
    if (this.path) {
      this.db = new DataStore<T>({
        filename: this.path,
        autoload: true
      });
    } else {
      this.db = new DataStore<T>();
    }
  }

  /** Initialize the database */
  init() {
    this.model();
    console.log(`Database loaded: ${this.path}`);
  }
```
NeDB is callback-based, which is kind of a bummer - However! Making promises out of callbacks is easy! In fact, [NodeJS has a way](https://nodejs.org/dist/latest-v13.x/docs/api/util.html#util_util_promisify_original)! But I thought for the sake of demonstration, I'd show how you go about converting your own functions. :)
```JavaScript
/** Create a new DBObj */
  create(data: T): Promise<T> {
    return new Promise((resolve: any, reject: any) =>
      this.db?.insert(data, (err: Error, doc: T) => {
        if (err) reject(err);
        return resolve(doc);
      })
    );
  }

  /**
   * Get a single database document.
   * @param query The query object to search for.
   */
  get(query: any): Promise<T> {
    return new Promise((resolve: any, reject: any) =>
      this.db?.findOne<T>(query, (err: Error, doc: any) => {
        if (err) reject(err);
        return resolve(doc);
      })
    );
  }

  /**
   * Find an array of documents that match the query
   * @param query The query object.
   */
  find(query: any): Promise<T[]> {
    return new Promise((resolve: any, reject: any) =>
      this.db?.find<T>(query, (err: Error, docs: T[]) => {
        if (err) reject(err);
        return resolve(docs);
      })
    );
  }

  /**
   * Update fields of the NeDB database
   * @param query The NeDB query for the fields to be updated.
   * @param data The data to update with
   */
  update(query: any, data: T): Promise<T | T[]> {
    return new Promise((resolve: any, reject: any) =>
      this.db?.update(
        query,
        data,
        { returnUpdatedDocs: true },
        (err: Error, _, docs: T) => {
          if (err) return reject(err);
          return resolve(docs);
        }
      )
    );
  }

  /**
   * Delete a a field from the NeDB instance.
   * @param query The object to query against.
   */
  delete(query: any): Promise<number> {
    return new Promise((resolve: any, reject: any) =>
      this.db?.remove(query, {}, (err: Error, n: number) => {
        if (err) reject(resolve);
        return resolve(n);
      })
    );
  }
}

const db = new NeDB<DBObj>(resolve(__dirname, "../../data/ursa.db"));
db.init();
export default db;
```
## Making the client!
I originally wrote this client as a stub in one file, before I had created (or needed) the static directories provided by `ExpressJS`.  In the future, I want to make a MUCH more robust client using `ReactJS`.  Perhaps that's another tutorial series in the making!  But! For now!  Here's what the client code looks like.  First, our markup
```HTML
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>UrsaMU Client</title>
    <link
      href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,700;1,400;1,700&display=swap"
      rel="stylesheet"
    />
  </head>
  <body>
    <div id="container">
      <div id="input" contenteditable></div>
      <ul id="feed"></ul>
    </div>
  </body>
```
Nothing too out of the ordinary there!  Now for some basic styling:
```HTML
<style>
    * {
      color: white;
      background-color: black;
      font-family: "Source Code Pro", monospace;
      font-size: 1rem;
      margin: 0;
      padding: 0;
      top: 0;
      left: 0;
    }

    #container {
      display: flex;
      height: 95vh;
      max-width: 800px;
      margin-left: auto;
      margin-right: auto;
      box-sizing: border-box;
      flex-direction: column-reverse;
    }

    #input {
      border: 1px;
      border-style: solid;
      border-color: rgba(255, 255, 255, 0.4);
      border-radius: 5px;
      margin-top: 16px;
      min-height: 48px;
      max-height: 112px;
      overflow-y: auto;
      overflow-x: hidden;
    }

    ul {
      padding: 0;
      margin: 0;
    }

    ul li {
      list-style-type: none;
      padding: 0;
      margin: 0;
    }

    img {
      width: 100%;
      object-fit: cover;
    }

    p {
      padding-top: 4px;
      padding-bottom: 4px;
      font-weight: lighter;
    }

    strong {
      font-weight: bold;
    }

    .item {
      width: 100%;
      word-wrap: break-word;
    }
  </style>
```
And finally, the JavaScript! A note, the first script tag that imports `socket.io.js` is provided by our socket.io instance.
```HTML
  <script src="/socket.io/socket.io.js"></script>

  <script>
    // Declare our variables.
    const feed = document.getElementById("feed");
    const socket = io("http://localhost:8090/");
    const input = document.getElementById("input");

    // Monitor keypresses.  If the user hits enter, send
    // the message off to the server!
    input.addEventListener("keypress", ev => {
      if (ev.keyCode === 13 && !ev.shiftKey) {
        ev.preventDefault();
        socket.send(input.innerText);
        input.innerText = "";
      }
    });

    // When a new message comes in, add it to the feed.

    socket.on("message", res => {
      const li = document.createElement("li");
      li.className = "item";
      console.log(res);
      li.innerHTML = res.message;
      feed.appendChild(li);
    });
  </script>
</html>
```
And now the moment we've all been waiting for: A screen cap gif! :D
![connect screen gif!](https://dev-to-uploads.s3.amazonaws.com/i/0mtl6c3155ycurqhzhxu.gif)
There we have it! I think that's a good wrapping up point for this article. In our next installment we're going to add a few more commands to the system: Specifically character creation and connection commands to test out the shiny new database! 

Thanks for stopping in! Make sure to **Follow** to get notifications on my future articles.  Feel free to leave a comment!