Copyright (C) 2021-2023 The Open Library Foundation
This software is distributed under the terms of the Apache License, Version 2.0. See the file "LICENSE" for more information.
folio-vertx-lib is a library for developing FOLIO modules based on Vert.x. This is a library, not a framework, with utilities such as:
- OpenAPI support
- Tenant API 2.0 support
- PostgreSQL utilities
- CQL support
The Vert.x OpenAPI unlike many OpenAPI implementations does not generate any code for you. Everything happens at run-time. Only requests are validated, not responses.
Place your OpenAPI specification and auxiliary files somewhere in resources
,
such as resources/openapi
.
In the following example, we will use OpenAPI spec books-1.0.yaml. The code snippets shown are from: MainVerticle , BookService and BookStorage.
Unlike RMB, you define MainVerticle yourself - no fancy initializers - you decide.
Example:
public class MainVerticle extends AbstractVerticle {
@Override
public void start(Promise<Void> promise) {
TenantPgPool.setModule("mod-mymodule"); // PostgreSQL - schema separation
final int port = Integer.parseInt( // listening port
Config.getSysConf("http.port", "port", "8081", config()));
MyApi myApi = new MyApi(); // your API, construct the way you like
// routes for your stuff, tenant API and health
RouterCreator [] routerCreators = {
myApi,
new Tenant2Api(myApi),
new HealthApi(),
};
HttpServerOptions so = new HttpServerOptions()
.setHandle100ContinueAutomatically(true);
// combine all routes and start server
RouterCreator.mountAll(vertx, routerCreators, "mod-mymodule")
.compose(router ->
vertx.createHttpServer(so)
.requestHandler(router)
.listen(port).mapEmpty())
.<Void>mapEmpty()
.onComplete(promise);
}
}
Your API must implement RouterCreator and, optionally, TenantInitHooks if your implementation has storage and that storage must be prepared for a tenant.
With the API there is a corresponding OpenAPI specification.
The RouterCreator
interface has just one method createRouter
where you
return a Router for your implementation. Normally that's created for you by the
OpenAPI library, but you can also define it yourself.
For an OpenAPI based implementation it could look as follows:
public MyApi implements RouterCreator, TenantInitHooks {
@Override
public Future<Router> createRouter(Vertx vertx) {
return RouterBuilder.create(vertx, "openapi/myapi-1.0.yaml")
.map(routerBuilder -> {
handlers(vertx, routerBuilder);
return routerBuilder.createRouter();
});
}
private void handlers(Vertx vertx, RouterBuilder routerBuilder) {
routerBuilder
.operation("postTitles") // operationId in spec
.handler(ctx -> {
// doesn't do anything at the moment!
ctx.response().setStatusCode(204);
ctx.response().end();
});
routerBuilder
.operation("getTitles")
.handler(ctx -> getTitles(vertx, ctx)
.onFailure(cause -> {
ctx.response().setStatusCode(500);
ctx.response().end(cause.getMessage());
}));
}
}
To support tenant init, your module should implement preInit
and postInit
.
These methods takes tenant ID and tenant attributes object.
The preInit
job should be "fast" and is a way for the module to check if the
operation can be started.. ("pre-check"). The postInit should perform the
actual migration.
The Tenant2Api implementation deals with purge (removes schema with cascade).
Your implementation should only consider upgrade/downgrade. On purge,
preInit
is called, but postInit
is not.
The PostgreSQL support is minimal. There's just enough to perform tenant
separation and most
DB_
environment variables that are also recognized by RMB
such as DB_HOST
, DB_PORT
, DB_USERNAME
, DB_PASSWORD
, DB_DATABASE
,
DB_MAXPOOLSIZE
, DB_RECONNECTATTEMPTS
, DB_RECONNECTINTERVAL
, DB_SERVER_PEM
.
The class TenantPgPool is
a small extension to the PgPool interface. The key method is TenantPgPool.pool
for constructing a pool for the current tenant. From that point, rest is plain
Vert.x pg client. However, the schema should be used when referring to tables, etc.
Use the getSchema
method for that.
The TenantPgPool.setModule
must be called before first use as is done in
MainVerticle example earlier.
To illustrate these things, consider a module that prepares a table in tenant init.
@Override
public Future<Void> postInit(Vertx vertx, String tenant, JsonObject tenantAttributes) {
if (!tenantAttributes.containsKey("module_to")) {
return Future.succeededFuture(); // doing nothing for disable
}
TenantPgPool pool = TenantPgPool.pool(vertx, tenant);
return pool.query(
"CREATE TABLE IF NOT EXISTS " + pool.getSchema() + ".mytable "
+ "(id UUID PRIMARY key, title text)")
.execute().mapEmpty();
}
For CQL support all fields recognized must be explicitly defined. Undefined CQL fields are rejected.
Example definition:
PgCqlDefinition pgCqlDefinition = PgCqlDefinition.create();
pgCqlDefinition.addField("cql.allRecords", new PgCqlFieldAlwaysMatches());
pgCqlDefinition.addField("id", new PgCqlFieldUuid());
pgCqlDefinition.addField("title", new PgCqlFieldText().withFullText());
This definition can then be used in a handler to get books:
private Future<Void> getBooks(Vertx vertx, RoutingContext ctx) {
RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY);
String tenant = params.headerParameter(XOkapiHeaders.TENANT).getString();
RequestParameter query = params.queryParameter("query");
PgCqlQuery pgCqlQuery = pgCqlDefinition.parse(query == null ? null : query.getString());
TenantPgPool pool = TenantPgPool.pool(vertx, tenant);
String sql = "SELECT * FROM " + pool.getSchema() + ".mytable";
String where = pgCqlQuery.getWhereClause();
if (where != null) {
sql = sql + " WHERE " + where;
}
String orderBy = pgCqlQuery.getOrderByClause();
if (orderBy != null) {
sql = sql + " ORDER BY " + orderBy;
}
return pool.query(sql).execute().onSuccess(rows -> {
RowIterator<Row> iterator = rows.iterator();
JsonArray books = new JsonArray();
while (iterator.hasNext()) {
Row row = iterator.next();
books.add(new JsonObject()
.put("id", row.getUUID("id").toString())
.put("title", row.getString("title"))
);
}
ctx.response().putHeader("Content-Type", "application/json");
ctx.response().setStatusCode(200);
JsonObject result = new JsonObject().put("books", books);
ctx.response().end(result.encode());
}).mapEmpty();
}
CQL queries of the form FIELD=""
have a special meaning; they find all records where the named field is NOT NULL. (This behaviour is the same as in the old RAML Module Builder.) To search for records where the field is present but empty, the double-equal operator can be used: FIELD==""
.
See project VERTXLIB at the FOLIO issue tracker.
Refer to the Wiki FOLIO Code of Conduct.
API descriptions:
Generated API documentation.
The built artifacts for this module are available. See configuration for repository access.