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 onlyCallExpression