One of the cool things about Booster is that most of itās functionality sits on top of an abstract interface that expects some stuff from the cloud. The framework itself doesnāt have a single call to any service from AWS, Azure, or Kubernetes. Thatās the job of the provider packages.
When you are developing your app, you probably donāt wanna think about the very little details of each database, cloud service or whatever. Perhaps you, like me, hate even having to learn each and every library or SDK for the technology/service at hand.
Thanks to this abstraction, you just code by using Booster concepts (command, events, etc.) and forget about the rest. But what happens underneath? Letās take a look š
Cloud vs local development
The cloud is cool and all that jazz, but whatās better than developing locally and seeing your changes instantly?
Yeah, there are things that emulate the workings of specific services, like DynamoDB, or there are folks who run their entire Kubernetes apps, with all the required processes, like MongoDB, MySQL, Redis, etc. Or even things like Serverless framework that deploy your app relatively quickly, but at the cost of maintaining a huge, messy, YAML file.
Stuff should be simpler, you shouldnāt need a beefy computer to develop your app.
Due to many reasons, but along them, the ones I just described, people resolve to coding their app in the simplest way possible, probably an express
server, or alike.
What if we had an express
server that behaved as our app in the cloud? Thatās the idea with a local provider.
Implementing a Booster provider to work locally
To implement a Booster provider, youāll need to create two npm
packages:
framework-provider-<name of your environment>
- This package is in charge of:- Provide the functions to store/retrieve data from your cloud.
- Transform the specific objects of your cloud into Booster ones, e.g. converting an AWS event into a Booster one.
framework-provider-<name of your environment>-infrastructure
- This package is in charge of:- Provide a
deploy
function that will set all the required resources in your cloud provider and upload the code correctly, as well as anuke
function that deletes everything deployed, OR - Provide a
start
function that will start a server and all the appropriate processes in order to run the project in a specific environment. This one is the one that Iāll be using for the local provider.
- Provide a
Given that Iām implementing the local provider, I just named them like:
framework-provider-local
framework-provider-local-infrastructure
To implement the local provider, Iāll be using express
that will act as the endpoints provided by Booster, and nedb, which is a local, filesystem implementation of a NoSQL database, with an API very similar to MongoDB. It would be the equivalent of SQLite but for NoSQL databases.
Letās start implementing the first package.
The provider interface
Boosterās provider interface is a regular TypeScript interface that must have itās methods implemented, an implementation could look like this:
export const Provider = {
events: {
rawToEnvelopes: ...,
forEntitySince: ...,
latestEntitySnapshot: ...,
store: ...,
},
readModels: {
rawToEnvelopes: ...,
fetch: ...,
search: ...,
store: ...,
// ...
},
graphQL: {
rawToEnvelope: ...,
handleResult: ...,
},
api: {
requestSucceeded,
requestFailed,
},
// ...
}
To begin implementing the basics, letās start with rawToEnvelopes
which are functions that convert from the cloud data type to the Booster one.
In the case of the local provider, the data will arrive as it is, as we are in charge of handling it with express
, so the implementation is pretty simple:
export function rawEventsToEnvelopes(rawEvents: Array<unknown>): Array<EventEnvelope> {
return rawEvents as Array<EventEnvelope>
}
export function rawReadModelEventsToEnvelopes(rawEvents: Array<unknown>): Array<ReadModelEnvelope> {
return rawEvents as Array<ReadModelEnvelope>
}
In the case of the rawToEnvelope
function for the graphQL
field, we will have to get some more information from the request, like a request ID, a connection ID, or the event type, which will come in the request, to simplify things, letās ignore them:
export async function rawGraphQLRequestToEnvelope(
request: express.Request
): Promise<GraphQLRequestEnvelope | GraphQLRequestEnvelopeError> {
return {
requestID: UUID.generate(), // UUID.generate() provided by Booster
eventType: 'MESSAGE',
connectionID: undefined,
value: request.body,
}
}
With these functions implemented, we already have our endpoints connected to Booster, now we just have to teach it how to store/retrieve data!
Creating a local database
Given that weāll be using NeDB to store our Booster app data, we will need to initialize it first. We can do it in the same file as the Provider
implementation:
import * as DataStore from 'nedb'
import { ReadModelEnvelope, EventEnvelope } from '@boostercloud/framework-types'
const events: DataStore<EventEnvelope> = new DataStore('events.json')
const readModels: DataStore<ReadModelEnvelope> = new DataStore('read_models.json')
NeDB uses a file for each ātableā, so we create two DataStore
s to interact with.
Now we have to implement the methods that the providers require, for example store
:
async function storeEvent(event: EventEnvelope): Promise<void> {
return new Promise((resolve, reject) => {
events.insert(event, (err) => {
err ? reject(err) : resolve()
})
})
}
async function storeReadModel(readModel: ReadModelEnvelope): Promise<void> {
return new Promise((resolve, reject) => {
readModels.insert(readModel, (err) => {
err ? reject(err) : resolve()
})
})
}
Sadly, NeDB doesnāt provide a Promise
based API, and doesnāt play well with promisify
, so we have to wrap it manually. The implementation is pretty straightforward.
The rest of the methods are a matter of implementing the proper queries, for example:
async function readEntityLatestSnapshot(
entityID: UUID,
entityTypeName: string
): Promise<EventEnvelope> {
const queryPromise = new Promise((resolve, reject) =>
this.events
.find({ entityID, entityTypeName, kind: 'snapshot' })
.sort({ createdAt: -1 }) // Sort in descending order
.exec((err, docs) => {
if (err) reject(err)
else resolve(docs)
})
)
}
There are some other methods that can be a bit confusing, but they also act as interaction at some point, like managing HTTP responses:
async function requestSucceeded(body?: any): Promise<APIResult> {
return {
status: 'success',
result: body,
}
}
async function requestFailed(error: Error): Promise<APIResult> {
const statusCode = httpStatusCodeFor(error)
return {
status: 'failure',
code: statusCode,
title: toClassTitle(error),
reason: error.message,
}
}
After implementing all the methods of the Provider
, we are pretty much done with the first package, and we can hop onto the infrastructure train š
Wiring everything up with an Express server
In the same case as the Provider
, your Infrastructure
object must conform to an interface, which in our case is a start
method that initializes everything. Here we will create an express
server and wire it into Booster, by calling the functions that the framework core provides.
Letās begin by initializing the express
server:
export const Infrastructure = {
start: (config: BoosterConfig, port: number): void => {
const expressServer = express()
const router = express.Router()
const userProject: UserApp = require(path.join(process.cwd(), 'dist', 'index.js'))
router.use('/graphql', graphQLRouter(userProject))
expressServer.use(express.json())
expressServer.use(router)
expressServer.listen(port)
},
}
Here we are importing userās app, in order to gain access to all the public Booster functions (typed in the UserApp
type).
You can see that the only endpoint at the moment is /graphql
, and thatās what we are gonna configure now:
function graphQLRouter(userApp: UserApp) {
const router = express.Router()
this.router.post('/', async (req, res) => {
const response = await userApp.boosterServeGraphQL(req) // entry point
res.status(200).json(response.result)
})
}
And thatās it, we only have to call boosterServeGraphQL
on the userās app.
Because we already provided all the required methods in the Provider package, Booster has access to all the infrastructure capabilities, and it will use all of them as they need to be, no need to write more code! š
Thatās all folks!
Iām gonna keep working on improving the local provider, like adding nice logging messages, tests, and more goodies š, but you can always check out the complete code in the following folders of the Booster repo:
packages/framework-provider-local
packages/framework-provider-local-infrastructure
Thanks for reading all of this! Have an awesome day,
Nick