UNPKG

ynn

Version:

Ynn is a framework for building more flexible and portable web applications.

244 lines (200 loc) 7.76 kB
/****************************************************************** * Copyright (C) 2021 LvChengbin * * File: lib/rsc.js * Author: LvChengbin<lvchengbin59@gmail.com> * Time: 07/06/2021 * Description: ******************************************************************/ const url = require( 'url' ); const querystring = require( 'querystring' ); const Events = require( 'events' ); const request = require( 'request' ); const typeis = require( 'type-is' ); const is = require( '@lvchengbin/is' ); const Console = require( './console' ); const output = require( './output' ); const YNN_PORT = Symbol.for( 'ynn#port' ); const YNN_UDS = Symbol.for( 'ynn#uds' ); const GET_REQUEST_OPTIONS = Symbol( 'get#request#options' ); const REQUEST = Symbol( 'request' ); const QUERY_PERFIX = '$ynn_rsc__'; class RSC extends Events { constructor( app, options = {} ) { super(); if( is.undefined( options.debugging ) ) { options.debugging = app.debugging; } this.app = app; this.console = new Console( this, 'rsc' ); this.logger = app.rscLogger; Object.assign( this, options ); this.output = output( this ); if( !this.requestInterceptor ) { this.requestInterceptor = app.rscRequestInterceptor; } } client() { return ''; } /** * to get service config till reach the top */ serviceConf( service, defaultValue ) { let m = this.app; while( m ) { const c = m.config( `rsc.service.${service}` ); if( !is.undefined( c ) ) return c; m = m.parent; } return defaultValue; } /** * 1. parse the URI * 2. detect the method * 3. add the params to options */ [ GET_REQUEST_OPTIONS ]( uri, params, opt = {}, rawurl ) { const options = Object.assign( {}, opt ); const api = {}; let config = {}; if( rawurl ) { options.uri = uri } else { const i = uri.indexOf( ':' ); const group = uri.substr( 0, i ); let pa = uri.substr( i + 1 ); config = this.serviceConf( group ); if( !config ) { throw new Error( `cannot find service group "${group}" in RSC config.` ); } const a = this.serviceConf( `${group}.api.${pa}` ); if( a ) { a.path && ( pa = a.path ); Object.assign( api, a ); } if( config.prefix ) { pa = `${config.prefix}/${pa}`.replace( /\/+/g, '/' ); } pa = pa.replace( /{{([a-zA-Z_$][a-zA-Z0-9_$]*)}}/g, ( m, n ) => { return ( { ...a.params || {}, ...opt.params || {} } )[ n ]; } ); if( config.host ) { options.uri = url.resolve( `${config.protocol||'http'}://${config.host}`, pa ); } else { const local = this.app.top[ YNN_UDS ] ? `unix:${this.app.top[YNN_UDS]}` : `127.0.0.1:${this.app.top[YNN_PORT]}`; options.uri = url.resolve( `${config.protocol||'http'}://${local}`, pa ); } } options.timeout = opt.timeout || api.timeout || config.timeout || 5000; options.method = ( opt.method || api.method || 'GET' ).toUpperCase(); options.headers = {}; const headers = Object.assign( {}, config.headers || {}, api.headers || {}, opt.headers || {} ); for( const name of Object.keys( headers ) ) { options.headers[ name.toLowerCase() ] = headers[ name ]; } // params can be set to null and to set full options for request in options if( params ) { params = Object.assign( {}, config.params || {}, api.params || {}, params ); if( options.method === 'GET' || options.method === 'HEAD' ) { options.qs = { ...params }; } else { const ct = options.headers[ 'content-type' ]; if( typeis.is( ct, 'application/json' ) ) { options.body = params; } else if( typeis.is( ct, 'multipart/form-data' ) ) { options.formData = params; } else if( typeis.is( ct, 'multipart/related' ) ) { options.multipart = params; } else if( typeis.is( ct, 'text/xml' ) ) { options.body = params.body; } else { options.form = params; } options.qs = {}; } } if( opt.qs ) { Object.assign( options.qs, opt.qs ); } options.json = is.undefined( opt.json ) ? true : opt.json; return options; } async request( uri, params = {}, opt = {} ) { return this[ REQUEST ]( uri, params, opt, true ); } async call( uri, params = {}, opt = {} ) { return this[ REQUEST ]( uri, params, opt ); } [ REQUEST ]() { let options; try { options = this[ GET_REQUEST_OPTIONS ]( ...arguments ); } catch( e ) { this.output.error( e.message ); return; } if( is.function( this.requestInterceptor ) ) { options = this.requestInterceptor( options, this ); } return new Promise( ( resolve, reject ) => { const start = new Date; const callback = ( err, res, body ) => { const slices = [ `${new Date} "${options.method}: ${options.uri}" ${res?res.statusCode:'-'}`, `"${options.qs?querystring.stringify(options.qs):'-'}"`, `${new Date-start}ms`, ]; if( err ) { this.output.info( `[ERROR] ${slices.join( ' ' )}` ); this.emit( 'failure', err, options ); return reject( res ); } const str = res.body ? ( is.plainObject( res.body ) ? JSON.stringify( res.body ) : res.body ) : '-'; /** * the request lib will response a buffer is the encoding is set to null. */ if( is.string( str ) ) { slices.push( `"${str.substr(0,300).replace( /[\n\r]+/g, '\\n' )}"`); } this.output.info( `[INFO] ${slices.join(' ')}` ); if( res.statusCode !== 200 ) { this.emit( 'failure', res, options ); return reject( res ); } resolve( body ); this.emit( 'success', body, options ); } ; request( options, callback ); } ); } callStream( uri, params = {}, opt = {} ) { const options = this[ GET_REQUEST_OPTIONS ]( uri, params, opt ); this.output.info( [ '[INFO]', `${new Date} "${options.method}: ${options.uri}" STREAM`, `"${options.qs?querystring.stringify(options.qs):'-'}"`, ].join( ' ' ) ); return request( options ); } get( uri, qs = {}, opts = {} ) { return this.call( uri, qs, Object.assign( { method : 'GET' }, opts ) ); } post( uri, form = {}, opts = {} ) { return this.call( uri, form, Object.assign( { method : 'POST' }, opts ) ); } static middleware() { return async ( ctx, next ) => { if( ctx.rsc ) return next(); ctx.rsc = new RSC( ctx.app, { ctx } ); return next(); }; } } RSC.QUERY_PERFIX = QUERY_PERFIX; module.exports = RSC;