atma-server

Server Application

Can be used as a Connect Middleware

This module uses:

To setup a bootstrap project use Atma.Toolkit - $ atma gen server

var atma = require('atma-server');
atma
    .server
    .Application({
        base:__dirname,
        configs: '/server/config/**.yml'
    })
    .done(function(app){
        // configuration and resources are loaded 
        app
            .processor({
                // pipeline is executed on every request 
                before: [
                    function(reqresnext){ next() },
                ]
                // this pipeline is executed only if the application finds any endpoint 
                // (server, handler, page, subapp) 
                moddleware: [
                    // refer to connectjs middleware documentation 
                    function(reqresnext){ next() },
                    require('body-parser').json(),
                ],
                // otherwise, if response was not completed by any middleware or any endpoint before 
                // continue with this middleware pipeline. 
                after: [
                    function(reqresnext){ next() },
                    atma.server.middleware.static
                ]
            })
            
            // start server, portnumber is taken from the configuration 
            .listen();
        
        // or start the server manually: 
        var server = require('http')
            .createServer(app.process)
            .listen(app.config.$get('port'));
            
        if (app.config.debug)
            app.autoreload(server);
    });

appcfg module is used to load the configurations and the routings. Default path is the /server/config/**.yml.

The default configuration can be viewed here - link

scripts / styles for the NodeJS application itself and for the web pages. They are defined in:

  • config.both.scripts<Object|Array>

    config/env/both.yml - shared resources

  • config.both.scripts<Object|Array>

    config/env/server.yml - resources for the nodejs application, e.g. server side components paths.

  • config.both.scripts<Object|Array>, config.both.styles<Object|Array>

    config/env/client.yml - resources, that should be loaded on the client.

    In the DEV Mode all client-side scripts/styles/components are served to browsers without concatenation. For the production compile resources with atma custom node_modules/atma-server/tools/compile

  • Define scripts and styles for a particular page in page routing.

  • subapps config/app.yml

    subapps:
        // all `rest/*` requests are piped to the Api Application
        // `Api.js` should export the `atma.server.Application` instance
        'rest': '/../Api.js'
  • handlers config/handlers.yml

    handler:
        location: /server/http/handler/{0}.js
        #< default
    handlers:
        # route - resource that exports a HttpHandler
        '/foo': 'baz'
            # path is '/server/http/handler/baz.js'
            # method is '*'
        
        '$post /qux': 'qux/postHandler'
            # path is '/server/http/handler/quz/postHander.js'
            # method is 'POST'
  • services config/services.yml

    service:
        location: /server/http/service/{0}.js
        #< default
    services:
        # route - resource that exports a HttpService @see HttpService
        '/user': 'User'
            # path is '/server/http/service/User.js'
            # method is '*'
            # futher routing is handled by the service, like '/user/:id'
  • pages config/pages.yml

    page:
        # see default config to see the default page paths
     
    pages:
        # route - Page Definition
        
        /:
            id: index #optional, or is generated from the route
                
            template: quz #optional, or is equal to `id`
                # path is `/server/http/page/quz/quz.mask
            master: simple #optional, or is `default`
                # path is `/server/http/master/simple.mask`
            
            # optional
            secure:
                # optional, default - any logged in user
                role: 'admin'
            
            scripts:
                # scripts for the page
            styles:
                # styles for the page
            
            # any other data, which then is accessable via javascript or mask
            # `ctx.page.data.title`
            title: String
     
            # rewrite the page request to some other route
            rewrite: String
     
            # redirect the page request to some other route
            redirect: String

There are 4 types of endpoints in route lookup order

We support application nesting, that means you can bind another server application instance for the route e.g. /api/ and all /api/** requests are piped to the app. Each application instance has its own settings and configurations. This allows to create highly modular and composit web-applications.

To declare a Handler is as simple as to define a Class/Constructor with Deferred(Promise) Interface and process function in prototypes, like this

// server/http/handler/hello.js 
module.exports = Class({
    Base: Class.Deferred,
    processfunction(reqres){
        this.resolve(
            data String | Object | Buffer,
            ?statusCode Number,
            ?mimeType String,
            ?headers Object
        );
        this.reject(error)
    }
});

To bind for a route(server/config/handlers.yml):

handler:
    location: '/server/http/handler/{0}.js'
    # <- default
handlers:
    '/say/hello': Hello
    '(\.less(\.map)?$)': LessHandler
    '(\.es6(\.map)?$)': TraceurHandler

Usually, this are the low level handlers, like 'less' preprocessor. But the interface (Deferred + process(req, res)) is same also for HttpService and HttpPage

For the route docs refer to RutaJS

Sample:

module.exports = atma.server.HttpService({
    '$get /': Function | Endpoint
    '$post /': ...
    '$get /:name(foo|bar|qux)': ...
    '$put /user': ...
})
atma.server.HttpService(/* endpoints */ {
    // route:handler 
    'route'function(reqresparams){
        this.resolve(/* @see Handler */);
        this.reject(...);
    },
    
    'route': {
        processfunction(){ ... }
    }
})
  • help - list all endpoints of a service with there meta information. http://127.0.0.1/rest/user?help
  • validation - when sending data with post/put, httpservice will validate it before processing
    atma.server.HttpService({
        '/route': {
            meta: {
                description: 'Lorem...',
                
                /* For request validating and the documentation */
                arguments: {
                    // required, not empty string 
                    foo: 'string',
                    // required, validate with regexp 
                    age: /^\d+$/,
                    
                    // optional, of type 'number' 
                    '?baz': 'number',
                    
                    // unexpect 
                    '-quz': null,
                    
                    // validate subobject 
                    jokers: {
                        left: 'number',
                        right: 'number'
                    },
                    
                    // validate arrays 
                    collection: [ {_id: 'string', username: 'string'} ]
                },
                // allow only properties which are listed in `arguments` object 
                strict: false,
                
                /* Documentation purpose only*/
                response: {
                    baz: 'string',
                    ...
                }
            },
            processfunction(reqresparams) { ... }
        }
    })
atma.server.HttpService({
    // route - Barricade (Middleware pattern) 
    '/route': [
        function(reqresparamsnext){
            // error example 
            if (req.body.name == null){
                next('Name argument expected');
                return;
            }
            
            // continue 
            req.name = req.body.name;
            next();
            
            // stop processing 
            this.resolve(...);
            this.reject(...);
        },
        function(reqresparamsnext){
            ...
        },
        ...
    ],
    
    // same with `help` 
    '/other/route': {
        meta: { ... }
        process: [
            fooFunction,
            bazFunction,
            ...
        ]
    }
})
// server/http/service/time.js 
module.exports = atma.server.HttpService({
    '/'function(reqres){
        this.resolve('This is a time service');
    },
    '/:transport(console|file|client)'function(reqresparams){
        var time = new Date().toString(),
            that = this;
        switch(params.transport){
            case 'console':
                console.log(' > time', time);
                this.resolve('Pushed to console');
                return;
            case 'file':
                io
                    .File
                    .writeAsync('someFile.txt')
                    .pipe(this, 'fail')
                    .done(() => {
                        this.resolve('Saved to file');
                    });
                return;
            case 'client':
                this.resolve(time);
                return;
        }
    }
})
# server/config/services.yml
 
service:
    location: /server/http/service/{0}.js'
    # <- default
 
services:
    '/time': time

HttpPage consists of 3 parts

  • Controller
  • Master View Template
  • View Template

You would rare redefine the default controller, as each Page should consist of a component composition, so that the logic could be moved to each component. We wont explain what a component is, as you should refer to MaskJS and MaskJS.Node Some things we remind:

  • Context
    { req: <Request>, res: <Response>, page: <HttpPage (current instance)> }
  • Render-mode
    •   mode: 'server' | 'client' | 'both' // @default is 'both' 
        modeModel: 'server' // if `server` is defined, the model wont be serialized 
  • Cache Each components output could be cached and the conditions could be defined.
    • byProperty: For each unique value from model or ct

Example

mask.registerHandler(':requestedUrl', Compo({
    mode: 'server:all'
    modelMode: 'server:all'
    cache: {
        byProperty: 'ctx.req.url'
    },
 
    onRenderStartfunction(modelctx){
        this.nodes = jmask('h4').text(ctx.req.url);
    }
}))

Going back to the HttpPage, lets start from a master view template

Refer to the layout component

Example:

// server/http/master/default.mask
layout:master #default {
    :document {
        
        head {
            meta http-equiv="Content-Type" content="text/html;charset=utf-8";
            meta name="viewport" content="maximum-scale=1.5, minimum-scale=.8, initial-scale=1, user-scalable=1";
            title > "Atma.js"
 
            atma:styles;
        }
        body {
            
            @placeholder #body;
            
            atma:scripts;
        }
    }
}
// server/http/page/hello.mask
layout:view master=default {
    @content #body {
        'Hello World'
    }
}

The routing is also made via the configuration files

# server/config/pages.yml
 
pages: 
    '/hello': 
        id: hello

E.g., to use ES6 or Less files, please install server plugins

# atma.toolkit, is only a helper util to intall plugins (further on is not required)
$ npm i atma -g
$ atma plugin install atma-loader-traceur
$ atma plugin install atma-loader-less

:copyright: 2014-2015; MIT; The Atma.js Project