import * as napa from 'napajs' ; import * as path from 'path' ; import * as assert from 'assert' ; import { RequestContext } from './request-context' ; import { Request } from './request' ; import { Response , ResponseHelper } from './response' ; import { Settings , Application , ApplicationConfig } from './application' ; import { RequestTemplate , RequestTemplateManager , RequestTemplateFileLoader , RequestTemplateLoader } from './request-template' ; import { NamedObjectDef , NamedObjectConfig , ScopedObjectContextDef , ScopedObjectContext , TypeDef , ProviderDef , TypeConfig , ProviderConfig , } from './object-model' ; import * as utils from './utils' ; export interface HostSettings extends Settings { throwExceptionOnError : boolean ; } export interface Host { register ( appModulePath , appInstanceNames , zone ? ) : void ; serve ( request ) : Promise < Response > ; applicationInstanceNames : string [ ] ; } export class Leaf implements Host { private _applications : Map < string , Application > = new Map < string , Application > ( ) ; private _requestTemplateManager : RequestTemplateManager ; private _applicationInstanceNames : string [ ] = [ ] ; private _settings : HostSettings ; private _objectContext : ScopedObjectContext ; public constructor ( settings = null ) { this . _settings = settings ; this . _objectContext = new ScopedObjectContext ( "global" , this . _settings . baseDir , null , settings . objectContextDef ) ; this . _requestTemplateManager = new RequestTemplateManager ( new RequestTemplateFileLoader ( ) , ( name ) : Application => { return this . _applications . get ( name . toLowerCase ( ) ) ; } ) ; } public register ( appModulePath , appInstanceNames , zone ? ) { if ( zone != null ) { throw new Error ( "LeafHost doesn't support register on a remote Zone." ) ; } let appConfigPath = require . resolve ( appModulePath + '/app.json' ) ; let app = new Application ( this . _objectContext , ApplicationConfig . fromConfig ( this . _settings , appConfigPath ) ) ; for ( let instanceName of appInstanceNames ) { let lowerCaseName = instanceName . toLowerCase ( ) ; if ( this . _applications . has ( lowerCaseName ) ) { throw new Error ( `Already registered with application name: '${ instanceName }'.` ) ; } } for ( let instanceName of appInstanceNames ) { let lowerCaseName = instanceName . toLowerCase ( ) ; this . _applications . set ( lowerCaseName , app ) ; this . _applicationInstanceNames . push ( instanceName ) ; } } public serve ( request ) { return new Promise < RequestContext > ( resolve => { if ( typeof request === 'string' ) { request = utils . appendMessageOnException ( ". Fail to parse request string." , ( ) => { return JSON . parse ( < string > request ) ; } ) ; } let base = undefined ; if ( ( < Request > request ) . base != null ) { base = this . _requestTemplateManager . getOrLoad ( ( < Request > request ) . base ) ; } else { let appName = ( < Request > request ) . application ; if ( appName == null ) { throw new Error ( "Either 'application' or 'base' should be present in request." ) ; } let app = this . getApplication ( appName ) ; base = app . defaultRequestTemplate ; } resolve ( new RequestContext ( base , < Request > request ) ) ; } ) . then ( ( context ) => { return context . execute ( ) ; } ) ; } public get applicationInstanceNames ( ) : string [ ] { return this . _applicationInstanceNames ; } public get objectContext ( ) : ScopedObjectContext { return this . _objectContext ; } public getApplication ( appName ) { let loweredName = appName . toLowerCase ( ) ; if ( this . _applications . has ( loweredName ) ) { return this . _applications . get ( loweredName ) ; } throw new Error ( "'" + appName + "' is not a known application" ) ; } public get settings ( ) : Settings { return this . _settings ; } } export class Proxy implements Host { private _zone : napa . zone . Zone ; private _applicationInstanceNames : string [ ] = [ ] ; public constructor ( zone ) { assert ( zone != null ) ; this . _zone = zone ; } public register ( appModulePath , appInstanceNames , zone ? ) { if ( zone != null && zone != this . _zone ) { throw Error ( "HostProxy cannot register application for a different zone." ) ; } this . _zone . broadcastSync ( ( baseDir , appModulePath , instanceNames ) => { require ( baseDir + '/index' ) . hub ( ) . register ( appModulePath , instanceNames ) ; } , [ __dirname , appModulePath , appInstanceNames ] ) ; this . _applicationInstanceNames = this . _applicationInstanceNames . concat ( appInstanceNames ) ; } public async serve ( request ) { let zone = this . _zone ; return zone . execute ( ( request ) : Response => { return require ( __dirname + '/index' ) . hub ( ) . serve ( request ) ; } , [ request ] ) . then ( ( result ) => { return Promise . resolve ( ResponseHelper . parse ( result . payload ) ) ; } ) ; } public get applicationInstanceNames ( ) : string [ ] { return this . _applicationInstanceNames ; } } export class Hub implements Host { private _localHost : Host ; private _proxies : Map < napa . zone . Zone , Host > = new Map < napa . zone . Zone , Host > ( ) ; private _settings : HostSettings ; private _applicationInstanceNames : string [ ] = [ ] ; private _hostMap : Map < string , Host > = new Map < string , Host > ( ) ; private _templateLoader : RequestTemplateLoader = new RequestTemplateFileLoader ( ) ; private _templateToAppNameMap : Map < string , string > = new Map < string , string > ( ) ; public constructor ( settings = null ) { this . _settings = settings ; } public register ( appModuleName , appInstanceNames , zone ? ) { if ( appModuleName . startsWith ( '.' ) ) { let callSites = napa . v8 . currentStack ( 2 ) ; if ( callSites . length > 1 ) { appModuleName = path . resolve ( path . dirname ( callSites [ 1 ] . getFileName ( ) ) , appModuleName ) ; } } let appModulePath = path . dirname ( require . resolve ( appModuleName + '/app.json' ) ) ; let host = this . findOrCreateHost ( zone ) ; host . register ( appModulePath , appInstanceNames , undefined ) ; this . _applicationInstanceNames = this . _applicationInstanceNames . concat ( appInstanceNames ) ; for ( let instanceName of appInstanceNames ) { let lowerCaseName = instanceName . toLowerCase ( ) ; this . _hostMap . set ( lowerCaseName , host ) ; } } private findOrCreateHost ( zone ) { if ( zone == null ) { if ( this . _localHost == null ) { this . _localHost = new Leaf ( this . _settings ) ; } return this . _localHost ; } else { if ( this . _proxies . has ( zone ) ) { return this . _proxies . get ( zone ) ; } else { let host = new Proxy ( zone ) ; this . _proxies . set ( zone , host ) ; return host ; } } } public async serve ( request ) { return new Promise < Host > ( resolve => { if ( typeof request === 'string' ) { request = utils . appendMessageOnException ( ". Fail to parse request string." , ( ) => { return JSON . parse ( < string > request ) ; } ) ; } let appName = ( < Request > request ) . application ; if ( appName == null ) { let baseUri = ( < Request > request ) . base ; if ( baseUri == null ) { throw new Error ( "Either 'application' or 'base' should be present in request." ) ; } let lcUri = baseUri . toLowerCase ( ) ; if ( this . _templateToAppNameMap . has ( lcUri ) ) { appName = this . _templateToAppNameMap . get ( lcUri ) ; } else { appName = this . _templateLoader . getApplicationName ( baseUri ) ; this . _templateToAppNameMap . set ( lcUri , appName ) ; } } let lowerCaseName = appName . toLowerCase ( ) ; if ( ! this . _hostMap . has ( lowerCaseName ) ) { throw new Error ( "Application '" + appName + "' is not registered for serving" ) ; } resolve ( this . _hostMap . get ( lowerCaseName ) ) ; } ) . then ( ( host ) => { return host . serve ( request ) ; } ) ; } public get applicationInstanceNames ( ) : string [ ] { return this . _applicationInstanceNames ; } } const SCHEMA_DIR = path . resolve ( __dirname , '../schema' ) ; export class HostConfig { private static readonly SETTINGS_SCHEMA : utils . JsonSchema = new utils . JsonSchema ( path . resolve ( SCHEMA_DIR , "host-config.schema.json" ) ) ; public static fromConfigObject ( jsValue , basePath ) { utils . ensureSchema ( jsValue , this . SETTINGS_SCHEMA ) ; let typeDefinitions = [ ] ; if ( jsValue . objectTypes != null ) { for ( let fileName of < string [ ] > ( jsValue . objectTypes ) ) { let filePath = path . resolve ( basePath , fileName ) ; typeDefinitions = typeDefinitions . concat ( TypeConfig . fromConfig ( filePath ) ) ; } } let providerDefinitions = [ ] ; if ( jsValue . objectProviders != null ) { for ( let fileName of < string [ ] > ( jsValue . objectProviders ) ) { let filePath = path . resolve ( basePath , fileName ) ; providerDefinitions = providerDefinitions . concat ( ProviderConfig . fromConfig ( filePath ) ) ; } } let namedObjectDefinitions = [ ] ; if ( jsValue . namedObjects != null ) { for ( let fileName of < string [ ] > ( jsValue . namedObjects ) ) { let filePath = path . resolve ( basePath , fileName ) ; namedObjectDefinitions = namedObjectDefinitions . concat ( NamedObjectConfig . fromConfig ( filePath ) ) ; } } return { baseDir : basePath , allowPerRequestOverride : jsValue . allowPerRequestOverride , throwExceptionOnError : jsValue . throwExceptionOnError , defaultExecutionStack : jsValue . defaultExecutionStack , objectContextDef : new ScopedObjectContextDef ( null , typeDefinitions , providerDefinitions , namedObjectDefinitions , true ) } ; } public static fromConfig ( hostConfigFile ) { return utils . appendMessageOnException ( "Error found in winery setting file: '" + hostConfigFile + "'." , ( ) => { return this . fromConfigObject ( utils . readConfig ( hostConfigFile ) , path . dirname ( hostConfigFile ) ) ; } ) ; } }