dwyl / abase

:house: A (solid) Base for your Web Application.
9 stars 0 forks source link

Modular architecture #50

Open eliasmalik opened 7 years ago

eliasmalik commented 7 years ago

From @nelsonic

Note: I had a quick chat with others regarding the fact that this PR including all the functionality in a single repo without splitting things out into independently tested plugins. This is fine for an MVP but we need to get to the point where we composing from smaller pieces. see: https://en.wikipedia.org/wiki/Unix_philosophy

This issue is to discuss migrating the architecture of the app away from a monolith and towards something more modular (probably with a hierarchy of components).

SimonLab commented 7 years ago

@eliascodes are there already some ideas/plan on how to split the application?

jrans commented 7 years ago

32

eliasmalik commented 7 years ago

Some thoughts:

High Level Description

Abase will consist on a single plugin which itself is composed of several other plugins, which have isolated functionality.

JSON Structure

Ideally, what is currently in the "pages" field would be minimised, and most of the information necessary to render a view would be inferred from the user model.

In addition, we would define a default user model, so that the user would need minimum configuration.

Parsing of the JSON config

TBD

Use
var path = require('path');
var Abase = require('abase');

var schemaPath = path.join(__dirname, 'schema.json');
var dbConnection = process.env.DATABASE_URL;

server.register({
  register: abase, options: { schemaPath: schemaPath, dbConnection: dbConnection }
}, function (err) {
  if (err) {
    throw err;
  }

  // ...
});
Internal Sketch
var fs = require('fs');
var abaseDb = require ('abase-db');
var abaseRoutes = require ('abase-routes');
var abaseValidation = require('abase-validation');
var abaseRender = require('abase-render');
var abasePermissions = require('abase-permissions');

var schemaParser = require('./parser.js');

exports.register = function (server, options, next) {
  fs.readFile(options.schemaPath, 'utf8', function (readErr, result) {
    if (readErr) {
      next(readErr);
    }

    // Does some (potentially arbitrary) processing of the schema and
    // attaches it to the server settings object
    server.settings.app.abase = schemaParser(result);

    // Registers all child plugins
    server.register([
      { register: abaseDb, options: options },
      { register: abaseRoutes, options: options },
      { register: abaseValidation, options: options }
      { register: abaseRender, options: options }
      { register: abasePermissions, options: options }
    ], next);
  });
}

Instantiation & migration of DB from schema

var pg = require('pg');
var db = require('./db.js');

exports.register = function (server, options, next) {
  var connection = db.parse(options.dbConnection);

  var client = new pg.Client(connection);

  // initialise (or migrate) DB
  db.init(client, server.settings.app.abase, function (dbError) {
    client.end();

    if (dbError) {
      next(dbError);
    }

    // Attach db helper functions to the reply object
    // should we use server.decorate instead?
    server.ext('onPreHandler', function (request, reply) {
      reply.abase = { db: db };
      reply.continue();
    });

    next();
  });
}

Generation of routes (borrows ideas from #68)

exports.register = function (server, options, next) {
  server.dependency(
    ['abase-validation', 'abase-render', 'abase-permissions'],
    function (_server, _next) {
      var schema = _server.settings.app.abase;

      server.route({
        method: 'GET',
        path: '/user/details',
        handler: { // abase-render provides an object handler definition interface
          form: {
            email: { label: 'Email:' },
            username: { label: 'Username' }
          }
        },
        config: {
          auth: {
            access: { // abase-permissions decorates server with abasePermissions
              scope: _server.abasePermissions(schema, ['email', 'username']);
            }
          },
          // abase-validation decorates server with abaseValidate
          validation: _server.abaseValidate(schema, ['email', 'username']);
        }
      });
      _next();
    }
  );

  next();
}

Permissions can be handled two ways I think: as above, the /user/details route would be restricted to users who have permissions to see the email and username fields, as defined in the JSON config. The other way is to not restrict views, but instead restrict the fields that are rendered on the view as a function of request scope.

Thoughts welcome.