/Co.Koa

An MVC for NodeJS built on Koa

Primary LanguageCSSMIT LicenseMIT

Home | Documentation | Core | CLI | Install
Co.Koa header

co-koa-core module status

Build Status Coverage Status Greenkeeper badge Test for Vulnerabilities

co-koa-cli module status

Build Status Coverage Status Greenkeeper badge Test for Vulnerabilities

co-koa-mongoose-plugin module status

Build Status Coverage Status Greenkeeper badge Test For Vulnerabilities

Release Notes Page


An opinionated MVC that let's you choose what should/shouldn't be a Model; inspired by Grails MVC, but viewed through the lens of Koa. Just like Koa, Co.Koa is about pragmatism; it encourages developers to interface with the underlying koa.app via Plugins. Mixin different databases and endpoints, it's up to you!
When installed via the CLI, Co.Koa ships with support for MongoDB (via mongoose) and a layout that comfortably interfaces with VueJS or Handlebars (HBS); but, hey, don't let that stop you choosing other toolchains!

Co.Koa obeys convention over configuration. One of Co.Koa's greatest strength comes in its implementation of Dependency Management. Controllers, Models and Services are each supplied with a powerful callback that reads and feels like a JQuery call. No need to worry about requiring reams of files from across your project.


Structure & Installation

To install Co.Koa please visit: the wiki installation page

Co.Koa's directory structure is as below

\api\controllers
    \models
    \services
    \views\helpers
          \layouts
          \partials
\config
\i18n
\node_modules

at the root of the project is an app.js file that loads in the co-koa-core module and configures it accordingly.

Dependency Management

Nearly every component in the Co.Koa toolchain is supplied a Dependency Management tool (signified by a $). The Dependency Management tool allows you to - among other things - easily load in Models, Services and Controllers.

Controllers, Models and their associated Services are all designed around the following boilerplate:

module.exports = function ($) {
  return {};
};

Example Suppose we are using Co.Koa's mongoose functionality OOB; and we have a Book document (table) in our Mongo database, modelled within our Co.Koa project. Furthermore, let's suppose we've setup a BookService that supports our controllers' interactions with the Book model. To expose the Book mongoose model within our Bookservice we simply do as below:

module.exports = function BookService ($) {
  const Book = $('Book');
  return {
    const book = new Book({ ... }).save();
    ...
  };
};

Perhaps a BookController model would like access to the BookService; simple!

module.exports = function BookController ($) {
  const bookService = $('BookService');
  return {
    ...
  };
};

not a require or import statement in sight! Everything is routed for you under the hood thanks to Dependency Management!

As of Co.Koa@1.8.0 the Co.Koa core now supports custom Dependencies via a Dependency Register

Models

Co.Koa models are a pragmatic concept. By default, your build will come installed with a mongoose plugin that is simply a light-touch abstraction of the mongoose API; featuring all the components one would expect to find therein. However, you can easily switch mongoose off and roll out your own persistence mechanisms with Co.Koa's plugin functionality (using local Databases and ORMs, serverless configurations or whatever!)

For the OOB Mongoose approach, you're dealing with the same mongoose API you already know, only you're not having to worry about requirements and which object goes where! Thus, you need only defer to mongoose API's own documentation to know what a component does!

Continuing our example from above let's contrive simple Book and Author models.

Book

module.exports = function Book ($) {
  const Author = $('Author'); // load the Author mongoose model, used in the public method defined below.

  return {
    _modelType: 'mongoose', // tells Co.Koa that this model uses the mongoose model plugin. remove this flag to simply return this object.
    schema: { // represents the mongoose schema like-for-like
      Accredited: {
        authors: [{
          type: 'ForeignKey', // also supprots the keywords 'FK' or 'ObjectId'.  Co.Koa will replace these 3 with the correct reference ObjectId on load!
          ref: 'Author'
        }]
      },
      title: {
        type: String,
        trim: true,
        default: '(Unnamed Book)'
      }
    },
    /* ------ OPTIONAL COMPONENTS ----- */
    index: { // mongoose secondary indexes
      title: 1,
      Accredited: -1
    },
    methods: { // public methods
      async findAssociatedAuthor () {
        const author = await Author.findById(this._doc.Accredited.authors[0]);
        return author;
      }
    },
    options: { runSettersOnQuery: true }, // mongoose options
    statics: { // Static methods
      async findCompleteReferenceByTitle (title) {
        const book = await this.findOne({ title });
        const author = await book.findAssociatedAuthor();
        return Object.assign({}, book._doc, { Accredited: { authors: [author._doc] } });
      }
    }
  };
};

observe that Co.Koa fully supports (and strongly encourages) await/async throughout! No more callback hell! No more hefty promise statements! Just easy-to-read and powerful code!

Author

module.exports = function Author ($) {
  return {
    _modelType: 'mongoose',
    schema: {
      firstName: String,
      lastName: String
    },
    /* ------ OPTIONAL COMPONENTS ----- */
    virtuals: { // virtuals behave exactly as they would if supplied to a mongoose schema
      fullName: {
        get () { return `${this.lastName}, ${this.firstName}`; },
        set (name) {
          const fullName = name.split(' ');
          this.firstName = fullName[0];
          this.lastName = fullName[1];
          return this;
        }
      }
    }
  };
};

Controllers

Controllers are also a piece of cake. Under the hood, your controllers are routed by koa-router (https://www.npmjs.com/package/koa-router).

Continuing with our Bookish theme, let's look at a contrived book example:

'use strict';

module.exports = function BookController ($) {
  const Book = $('Book');
  const bookService = $('BookService');

  return {
    'GET /:id': async (ctx) => { // uri = Book/:id (':id' is variable)
      const book = await Book.findById(ctx.params.id); // ctx.params.id contains the ":id" variable
      ctx.body = book;
    },
    'GET /Author': async (ctx) => { // uri = Book/Author
      const book = await Book.find({ title: 'Harry Potter and the Philosopher\'s Stone' });
      const author = await book[0].findAssociatedAuthor();
      ctx.body = author;
    },
    'GET /HarryPotter': async (ctx) => { // Book/HarryPotter
      const harryPotter = await Book.findCompleteReferenceByTitle('Harry Potter and the Philosopher\'s Stone');
      ctx.body = harryPotter;
    }
  };
};

Services

Services allow you to decouple your business logic from your application's logic. Ideally, we want to keep controller logic svelte; and some code just doesn't sit right in a model method. As we saw in the BookController example above Co.Koa exposes services via the $ DependencyManager. Let's continue the book theme by contriving BookService.js in api\services:

module.exports = function BookService ($) {
  const Author = $('Author');

  return {
    async createAuthor () {
      const author = await new Author({
        fullName: 'J.K Rowling' }).save();
      return author;
    }
  };
};

Co.Koa will sort out the requirements for you! The service is exposed via the $ DependencyManager in your controllers, models and other services as $('BookService').

Vue Integration

Co.koa now ships with @koa/cors and will very happily interface with a vue instance in tow. With the release of co-koa-cli@1.0.0 VueJS is easy to ship as your development front end! For more information please visit the CoKoa Documentation

Views

Co.Koa Supports the handlebars .hbs extension using "koa-hbs-renderer" (https://www.npmjs.com/package/koa-hbs-renderer). supply your views, helpers, layouts and partials within the directories indicated below:

\api\views\helpers
          \layouts
          \partials

rendering .hbs is simple and powerful; suppose we have a view called SampleView.hbs saved in the \api\views directory. The view is expecting a single variable called action to be passed to it:

{% raw %}

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en"><head></head>
<body> <p>I'm a view, I was called by the action: {{action}}</p> </body>
</html>

{% endraw %}

If we add the following to BookController, we're good to go!

'GET /HBSDemo': async (ctx) => {
  await ctx.render('SampleView', { action: '/HBSDemo' });
}

you can add custom helpers with ease! for example, maybe you want to make hbs files handle more complex conditional statements. No prob, suppose we have a file saved at \api\views\helpers\CK.js:

module.exports = {
  eq: (foo, bar) => foo === bar,
  ne: (foo, bar) => foo !== bar,
  lt: (foo, bar) => foo < bar,
  gt: (foo, bar) => foo > bar,
  lte: (foo, bar) => foo <= bar,
  gte: (foo, bar) => foo >= bar,
  and: (foo, bar) => foo && bar,
  or: (foo, bar) => foo || bar
};

now your .hbs file can use custom logic!

{% raw %}

<ul>
  <li>
    {{#if (CK_and (CK_eq parent 'PartialSample')
                  (CK_ne parent 'SampleView')) }}
      I am the product of an "if" condition using embedded operands!
    {{else}}
      I am the product of an "else" condition
    {{/if}}
  </li>
</ul>

{% endraw %}

Note that your helpers are prefixed with CK_. Co.Koa uses koa-hbs-renderer. For more information please navigate to the koa-hbs-renderer github. For more information on how to use Handlebars, please visit: http://handlebarsjs.com/

Testing

as of co-koa-cli@^1.17.0, Co.Koa has baked in support for unit and integration testing with Jest thanks to the app.test.harness.

const testHarness = require('../app.test.harness.js');
const coKoa = testHarness.init({ port: 3004, type: 'integration' });
describe('Integration Test Suite For Example', async () => {
  const $ = coKoa.$
  const Example = $('Example');

  test('An integration test', async () => {
    try {
      const result = await new Example({ }).save();
      expect(typeof result).toBe('object');
    } catch (e) {
      console.error(e.message)
    }
  });

  afterAll(async (done) => {
    await coKoa.app.close() // close the Koa app listener
    await testHarness.destroy(done) // (optional) empty test database
  });
});

for more information please see the testing documentation.