0.0.1 • Published 4 years ago

studybear-api v0.0.1

Weekly downloads
-
License
ISC
Repository
-
Last release
4 years ago

Studybear API

Getting started

This project uses npm as a package manager, clone the repository and run npm install.

Running databases for tests

The project includes a docker-compose.yml for starting test databases. Simply run docker-compose up to start, and docker-compose down -v to stop the database cluster. The database console should be accessible under localhost:7474.

Running tests

The package.json includes scripts to run all tests. Make sure the database cluster is running and run npm test as root.

Adding new endpoints

Authentication/Authorization

For authentication this api uses temporary, user-specific keys with associated permissions. Everytime a user logs in with their email-address and password, the api generates a new key and returns it. A user can have up to 5 keys at once, when adding another key, the oldest gets deleted.

When adding a new endpoint we need to define which permissions a client needs, and then verify if a supplied key has those permissions. Fortunately, we have an easy way to integrate that functionality: In our route definition, we add the following preHandler:

preHandler: fastify.auth([
  api.needsPermissions(
    ...
  )
])

That's it! All the verification stuff is done behind the scenes. If the request api key is invalid for some reason, it automatically returns a 401 Not Authorized and the handler is not executed. The parameters are Permission enum values in CNF. CNF Clauses are Permission arrays, and clauses are separated by commas. Clauses containing only a single literal can also be written without array brackets. Negations -- although hardly ever necessary -- are also possible as an object: { not: Permission.SomePermission }.

Some examples

Let eq be arbitrary permissions. Then we can convert the following formulas to our authentication format:

e0

preHandler: fastify.auth([
  api.needsPermissions(
    Permission.P1
  )
])

e1

preHandler: fastify.auth([
  api.needsPermissions(
    Permission.P1,
    [
      Permission.P2,
      Permission.P3
    ]
  )
])

e2

preHandler: fastify.auth([
  api.needsPermissions(
    [
      { not: Permission.P1 },
      Permission.P3
    ]
  )
])

e3

preHandler: fastify.auth([
  api.needsPermissions(
    [
      Permission.P1,
      Permission.P2
    ],
    [
      { not: Permission.P1 },
      { not: Permission.P2 }
    ]
  )
])

Ok, I got carried away, but we get the concept. Should a new endpoint need permissions, that are not part of the Permission enum already, we can just add them, as long as we stick to the schema :<Region>...:...[Self] for values.

Bookmarks

Because this api is powered by a Neo4j database with causal clustering, we need to consider cases where a user makes two successive request to the api and expects them to be consistent, i.e. possible changes made by the first request should be accessible by the second request. This can be an issue, because the database consists of multiple instances that propagate changes asynchronously. Because of that, it may happen that the second requests gets information from an instance that does not know about the changes of the first request yet.

Luckily, Neo4j already solves this problem by introducing the concept of bookmarks. Each transaction returns a bookmark which logs the changes made by the transaction. These are simple strings that can be added to a database query to cause it to be routed to an instance that already knows about the corresponding transaction.

bookmarks

Because this is a RESTful api, we cannot manage those bookmarks server side or associate them with clients. Instead we leave that task to the client. Each write-endpoint (POST, PATCH, DELETE, PUT) returns a bookmark header if successful and every endpoint takes a bookmark request header to query the correct database. The request headers are optional though, so the client can decide for himself and for every api call whether or not this added consistency is necessary.

Once again, this is probably a lot of work to separately implement for every endpoint. So we added a convenient shortcut syntax. We can just access the property FastifyRequest.bookmark to get the bookmark in neo4j-readable format or a default bookmark, if no such header was supplied. To further simplify working with Neo4j sessions, the FastifyRequest object also exposes a session property that already includes the relevant bookmark (see example below).

To return a bookmark we can set the property FastifyReply.bookmark. This adds the header bookmark with the supplied value if the request was successful (i.e. we return a status code between 200-308). Also, to make api responses a little easier to work with, especially when testing, we want to obfuscate the bookmarks before sending them, this has the added benefit, that it is not immediately obvious to clients what database system we are using. For obfuscation, the fastify instance api exposes the self-explanatory function obfuscateBookmark(string). We can also import the function lastBookmarkOrDefault(Session) from neo4j-extensions, that always returns a valid bookmark for the last transaction of the given session.

An Example

An example endpoint could look roughly like this

...,
handler: async (request, response) => {
  // do something...

	// we are using the neo4j session the request already created for us
  const r = await request.session.writeTransaction(async (tx) => { // or readTransaction of course
    // some database interaction

    if (error) {
      response.setStatusCode(400)
      return {
        // some error response
      }
    }

    return {
      // some response message
    }
  })

  // this sets the bookmark header on a successful response
  response.bookmark = api.obfuscateBookmark(lastBookmarkOrDefault(session))

  session.close()
  return r
}

Note: Instead of response.status(400) we use response.setStatusCode(400). Otherwise FastifyReply.bookmark cannot distinguish between successful and unsuccessful responses.