Skip to content
Chad Wingrave edited this page Oct 26, 2019 · 13 revisions

WoveonService is a microservice development platform, designed for REST/GraphQL endpoints, with strict opinions on layering interface, app and state functionality. It is designed to simplify error-prone complexities in development by being opinionated about naming and organization of code (possible only because microservices are small). Additionally, it's model-based organization and approach to data decomposition enables loose coupling of data, robustness in incomplete knowledge of models, and a novel approach to data decomposition that can aid microservice decomposition.

Opinions: Microservices should be simple, so you can focus on three things:

  • handling incoming requests, data and schemas and errors handling
  • performing application computations
  • a clear, organized and small API

Origins

We first built on ExpressJS, with clear and understandable code until the API grew. We wanted more syntactic sugar and standardized coding practices among other features, which we built into WoveonService. WovService is designed for WovTools projects but is not needed.

Layers

Interface Layer

The interface layer handles/organizes the IO with the external world/microservices. It uses two main protocols, REST and GraphQL, with GraphQL largely being generated by our WovModels (see below). Endpoints are are split between RESTful protect (for authorization) and routes files and GraphQL schemas, and controllers and resolver files implement the handler function. Both are designed for the auto-documentation of routes.

App Layer

Application specific functionality (non-CRUD operations) belongs here. This is generally where reusable code for the handlers/resolvers is places.

State Layer

Data is handled here, in databases and remotely in the microservice architecture. A model (WovModel) represents a first-class entity in the system, that has a connection to persistent data and CRUD operations through a client (WovClientLocal/WovClientRemote). Supported databases are Mongo and Postgres. (see Schema_Opinionations). Remote models (models on other microservices) are accessed via generated GraphQL queries.

Endpoints ({msname}/{ver}:pub/prot/model/priv/doc)

These are useful ways of organizing your microservice endpoints. With orchestration services, it is often useful to group endpoints by pub/prot/priv to manage Ingress (i.e. WovTools explicitly does not allow prot/priv/doc externally in production).

  • {msname} - name of the microservice, to help organize your orchestration
  • {ver} - enables you to manage API versions explicitly in your routes
  • pub - public-facing functionality
  • prot - internal facing functionality to your microservices
  • model/graphql - a graphql endpoint for CRUD operations on models
  • priv - debug-level and should be treated as such
  • doc - useful auto-generated documentation from the code (i.e. free from maintenance) in WovService to help developers understand the microservice

Logger (this.l)

I try to always place a logger on each major object. You should be logging a microservice regularly.

Examples

These examples are what could exist in your own project directory.

Service Example

  try {

    // NOTE: WOV_mymsname_port is literally the name given to your microservice.
    const service = new MS({
      logger     : logger,
      db         : mymsdb,
      port       : C.get('WOV_"mymsname"_port'),
      ver        : C.get('WOV_"mymsname"_ver'),
      protect    : require('./protect'),
      routes     : require('./routes'),
      controller : require('./controller'),
      applayer   : Object.assign({}, require('./myappcode_foo'), require('./myappcode_bar')),
    });
    await service.init();
    await service.startup();
  }
  catch (e) {
    logger.rethrowError(e);
    mymsdb.disconnect();
  }

Interface Layer Examples

protect.js

// a protected route that uses the DocMethod to generate documentation AND define IO contracts.
moduel.exports = async function() {
  this.listener.onProtect('/pub/account/prot', new DocMethod({
    summary : 'Protects account information behind a session',

    // params/paramspost define data required to be passed to the route and data required to exist post-route
    params  : [ new DocParam({name : 'sessiontoken', required : true})],
    paramspost : [
      new DocParam({name : 'sessiontoken', required : true}),
      new DocParam({name : 'user', required : true, desc : 'Loads the User for the session'}),
    ],

    // the handler, which reads the User model in the state layer using the sessiontoken.
    handler : async function(_args, _res) {

      // read from the User object in the state layer (sl)
      // Notice "this", since all handlers are bound to the microservice.
      let result = await this.sl.User.readBySessionToken(_args.sessiontoken);
      if ( result == null ) { retval = WovReturn.retCodedError('UNAUTHORIZED', _args.sessiontoken); }
      else { retval = WR.retSuccess({user : result}); }
      return retval;
    }.bind(this)}),

  // passed to onProtect, for debugging spew
   __filename);
};

routes.js

module.exports = async function() {
  
  // onLogin - defines the endpoint and handler function in the controller.js file
  this.listener.onPost('/pub/account/session', this.onLogin, __filename);

  // onGetSession - returns user information in the onGetSession function
  // NOTE: the route /pub/account/prot/session is after the protected route so authorization is required
  this.listener.onGet('/pub/account/prot/session', this.onGetSession, __filename);
};

controller.js

module.exports = {

  onLogin : new DocMethod({
    summary : 'Logs people in with email and password, creating a session.',
    params  : ['email', 'password'],  // required params
    handler : async function(_args) {
      let retval = null;

      if ( retval == null ) {
        let result = await this.sl.Session.createOne(_args.email, _args.password);
        if ( result == null ) { retval = WR.retError(email, 'ERROR: failed to create session'); }
        else if ( result instanceof  WS.WovModel ) {
          retval = await this.onGetSession.handler({sessiontoken : result.get('sessiontoken'), user : _args.me});
        }
        else if ( WR.isValidWovReturn(result) ) { retval = result; }
        else { retval = WR.retError(result, 'ERROR: what the heck happened?'); }
      }
      return retval;
    }}),

  // since we know they are logged in, return the User model 'user' that is passed in.
  onGetSession : new DocMethod({
    params  : [
      new DocParam({name : 'sessiontoken', required : 'true'}),
      new DocParam({name : 'user', required : 'false', desc : 'passed in via protection route, but not needed'}),
    ],

    // flatten converts the User model to an object, stripping any sensitive data (i.e. password hashes)
    handler : async function(_args) { return new WovReturn({user : user.flatten()}); },
  }),

}

GraphQL Schemas / Resolvers

The WovModel object generates this for read operations, which is called by the WovModelClient for all GraphQL routes. You only need to implement additional queries and mutations, merging that file's functions when the GraphQL server launches. - How this works?!?

  // functions that are called
  WovModel.getGraphQLSchema();
  WovModel.getGraphQLModelResolver();

App Layer Examples

It's your app code so it's going to be called by handlers on 'this', since App Layer code is placed on to the service, just like handlers. This is really just to help separate out related bits of code into separate files. If you have many many files that are long and confusing... you will need to divide it into separate microservices anyway, so separate files can help you begin that process.

// myappcode_foo.js
module.exports = {
  foo : function() { this.foo = 'foo'; return this.foo; },
}
// myappcode_bar.js
module.exports = {
  bar : function() { this.bar = 'bar'; return this.bar; },
}

State Layer Examples

StateLayer

module.exports = class MyStateLayer extends WS.WovStateLayer {
  constructor(_logger, _wovdb) {
    super(_logger, [
      new MyModelClient(_logger, _wovdb),
      new MyRemoteModelClient(_logger),
    ]);
  };
};

Model Client Example

class MyModelClient extends WS.WovModelClient {
  constructor(_logger, _wovdb) {
    // load the models and the WovDB they use, directly into the client
    super(_logger, _wovdb, [ require('./user.js'), require('./account.js'), ...]);
  };
};

"User" Model

This would be in your project as src/state/User.js.

class User extends WS.WovModel {

  // database table
  static tablename = 'users';

  // required method
  static async createOne(_email, _password, _account_ref) { ...; return new User(data); }

  // example of running a query via the WovClient (this.cl)
  static async readBySessionToken(_sessiontoken) {
    let retval = null;
    let q = `SELECT u.*
             FROM session s
             INNER JOIN users u ON u.id = s._user_ref
             WHERE s.sessiontoken=$1::uuid`;
    let d = [_sessiontoken];
    let data = await this.cl._runSingularQuery(q, d, 'User_readBySessionToken');
    if ( data != null ) { retval = new this(data); }
    return retval;
  }
};

// Added to the "class" definition of User... used for GraphQL and to compare to database server
User.updateSchema({
  xid          : 'uuid',  // external id
  username     : 'text',
  email        : 'text',
  passwordhash : 'text',
  created_at   : 'timestamp without time zone',
  _account_ref : 'integer',
  sensitive    : ['passwordhash'],  // 'passwordhash' will not be sent in queries, only used in the model
});

WovDatabase

This takes parameters ONLY from environment variables that are named after the database's name.

  /** 
   * WOV_'mymsdb'_username - ex. WOV_mymicroservice_username='postgres'
   * WOV_'mymsdb'_endpoint - ex. WOV_mymicroservice_endpoint='locahost'
   * WOV_'mymsdb'_database - ex. WOV_mymicroservice_database='projectx'
   * WOV_'mymsdb'_port     - ex. WOV_mymicroservice_port=5432 
   * WOV_'mymsdb'_password - ex. WOV_mymicroservice_password='password'
   */
  let mymsdb = new WS.WovDBPostgres('mymsdb', logger);
  await mymsdb.connect();