Unbounded Microservice Part 2: Searching and Updating with Javascript
In part 1 of this series, we demonstrated how easy it is to get started using Unbounded, by giving an example of a service written in Node.js with the Serverless framework. For reference, here’s a slightly condensed version of the code for that service:
While it’s nice that this code is relatively simple and we didn’t need to configure anything on the database side to deploy it, it’s a little, shall we say, plain when it comes to functionality. One thing we’d like to do would be to update an existing movie rather than just inserting a new one.
One way to do this would be to call insert on a document with an existing primary key, which by default would replace our existing document with a brand new one. But what if we want to do some more complex manipulation of the existing data? In that case, we’ll need to introduce a new Unbounded concept: custom Javascript functions.
To help us understand how to use custom functions, consider what would happen if we wanted to modify the “GET /movies” endpoint to take a parameter that enabled us to search by name, rather than returning every movie in the database. If we were writing that query in SQL, it might look something like this:
select * from movies where name like '%Future%'
But Unbounded doesn’t use SQL for queries. Instead, to specify a where clause, we can write a function in Javascript that takes a document as a parameter, and returns a truthy value if that document should be included in the result set:
db.query().where(movie => movie.name.includes('Future')).send();
To clarify what is happening here: we’ve modified the db.query call above and chained in a .where
call with a function parameter. This function gets sent to the Unbounded server and executed there on every document in the database, and only those documents where the function returns true are included in the query results.
This means a couple things: you have a lot of flexibility in how you want to write your where function. You can use a different function syntax, or make the actual return value more verbose for clarity:
db.query().where(function (movie) {
if (movie.name.includes('Future'))
return true;
else
return false;
}).send();
Moreover, since you already know Javascript (or at least if you’ve been following these articles so far I would hope you do), you already know everything there is to know about the semantics of these queries. Unlike some random dialect of SQL which may have its own take on data types, operators, functions, etc., everything above is just standard Javascript code that does exactly what you expect it to.
There is one caveat, though, which is similar to one you’ll find when writing code that calls SQL: passing a parameter into our function requires a little more work than we might expect.
For example, if we declared our search string as a query parameter, you might think we could then run the query like this:
app.getAsync('/movies', async (req, res, next) => {
let search = req.query.search; // this is wrong and will not work!
res.json(await db.query()
.where(movie =>
movie.name.includes(search)
).send());
});
However, if you try this you’ll get an error that the identifier “search” cannot be found. This is because when your function code is sent to Unbounded to execute, it’s traveling on its own, without the surrounding context where your search variable is declared.
To get around this, Unbounded has the ability to bind one or more values to a function, similar to the .bind() call in Javascript. It works like this:
db.query()
.where((search, movie) => movie.name.includes(search))
.bind(search)
.send();
By appending a .bind
call to our query chain and passing one ore more values, those values are pre-pended to the argument list of the function when it’s called on the database server, so we can use them just like any other variable.
With that established, let’s add a new endpoint to our service, which lets us add a review to an existing movie:
The interesting part here is the db.update call, which returns a builder object that we chain two more calls to: a match
call which does the same thing as the first parameter of the db.match method, telling us which object(s) we’d like to update, and a set
function, which takes the place of the set clause in a SQL update statement. This function operates similarly to the where function in the examples above, except that it takes as an argument a document to modify, and returns a new version of that document to write back to the database.
(Incidentally, update and the as-yet-undiscussed delete methods can use a where function in addition to or instead of the match object to determine which documents to update or delete…for details see the module documentation).
Just to prove it works, let’s try it out:
curl https://xxxxxxxxx.execute-api.us-east-2.amazonaws.com/dev/movies/P0ePhfWRdZu4UNZV20Mw
{"name":"Back to the Future","year":1985,"reviews":[],"id":"P0ePhfWRdZu4UNZV20Mw"}$ curl -X POST https://xxxxxxxxx.execute-api.us-east-2.amazonaws.com/dev/movies/P0ePhfWRdZu4UNZV20Mw -d 'comment=classic flick&stars=5'$ curl -X POST https://xxxxxxxxx.execute-api.us-east-2.amazonaws.com/dev/movies/P0ePhfWRdZu4UNZV20Mw -d 'comment=why is a scientist friends with a doofus, I dont get it&stars=1'curl https://xxxxxxxxx.execute-api.us-east-2.amazonaws.com/dev/movies/P0ePhfWRdZu4UNZV20Mw
{"name":"Back to the Future","year":1985,"reviews":[{"comment":"classic flick", "stars": 5}, {"comment":"why is a scientist friends with a doofus, I dont get it", "stars": 1}],"id":"P0ePhfWRdZu4UNZV20Mw"}
Unbounded’s query engine is pretty unique, which might make it seem intimidating at first — but we think once you wrap your head around it, you’ll see that programming your database with Javascript lets you do some pretty neat things in a natural-feeling way.
Stay tuned for part 3 of this intro series, where we’ll discuss how to make Unbounded process huge amounts of data in parallel using webhooks and map functions, while hopefully using less than 1.21GW in the process…😉