Summary

Connecting all the dots

Let's sum up what we've learned so far and create example service - "MetaQL", a GraphQL-inspired library.

We want to send a string query to the server through HTTP request and get JSON response.

First, a node.js part with express.js and body-parser:

import { metaesEval } from "metaes/lib/metaes";

const express = require("express");
const bodyParser = require("body-parser");
const app = express();
const port = 3000;

app.use(bodyParser.json());

app.post("/", (req, res) => {
  const { source, env } = req.body;
  console.log(req.body);
  try {
    metaesEval(
      source,
      (result) => res.json(result),
      (error) =>
        res.status(500).json(
          Object.assign({}, error, {
            value: error.value.toString()
          })
        ),
      env
    );
  } catch (e) {
    res.status(500).json({ value: e.message });
  }
});

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

Running $ curl --header "Content-Type: application/json" -X POST -d '{"source":"2+2"}' localhost:3000 will give back 4, but

$ curl --header "Content-Type: application/json" -X POST  -d '{"source":"a"}' localhost:3000

will output:

{
  "type": "ReferenceError",
  "value": "ReferenceError: \"a\" is not defined.",
  "location": {
    "type": "Identifier",
    "name": "a",
    "range": [0, 1],
    "loc": {
      "start": { "line": 1, "column": 0 },
      "end": { "line": 1, "column": 1 },
      "source": "true"
    }
  }
}

Adding env field fixes the problem:

$ curl --header "Content-Type: application/json" -X POST  -d '{"source":"a", "env":{"a":4}}' localhost:3000

gives 4.

Now onto implementing something useful: current user and his friends:

import { metaesEval } from "metaes/lib/metaes";

const express = require("express");
const bodyParser = require("body-parser");
const app = express();
const port = 3000;

app.use(bodyParser.json());

const users = Array.from({ length: 10 }, (_, i) => ({ name: "User" + i }));

function getCurrentUser() {
  return users[Math.floor(Math.random() * users.length)];
}

function getFriendsOf(user) {
  return users.filter((candidate) => candidate !== user);
}

const global = createEnvironment({ getCurrentUser, getFriendsOf });

app.post("/", (req, res) => {
  const { source, env } = req.body;
  try {
    metaesEval(
      source,
      (result) => res.json(result),
      (error) =>
        res.status(500).json(
          Object.assign({}, error, {
            value: error.value.toString()
          })
        ),
      createEnvironment(env || {}, global)
    );
  } catch (e) {
    res.status(500).json({ value: e.message });
  }
});

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

Usage:

curl --header "Content-Type: application/json" -X POST  -d '{"source":"getFriendsOf(getCurrentUser())"}' localhost:3000

outputs: [{"name":"User0"},{"name":"User1"},{"name":"User2"},{"name":"User3"},{"name":"User4"},{"name":"User5"},{"name":"User6"},{"name":"User8"},{"name":"User9"}].

Done. You can obviously compose results in any way you want, using complex queries like:

curl --header "Content-Type: application/json" -X POST  -d '{"source":"let user = getCurrentUser(); let friends = getFriendsOf(user); ({ user, friends }); "}' localhost:3000

to get:

{
  "user": { "name": "User8" },
  "friends": [
    { "name": "User0" },
    { "name": "User1" },
    { "name": "User2" },
    { "name": "User3" },
    { "name": "User4" },
    { "name": "User5" },
    { "name": "User6" },
    { "name": "User7" },
    { "name": "User9" }
  ]
}

Note that script in metaES evaluates to last expression; we had to wrap it in () to make it parsable by parser.

Further ideas to support as an exercise:

  • allow asynchronous access - maybe getCurrentUser() will talk to database?
  • restrict interpreters - maybe allow using only CallExpression