UNPKG

iopa

Version:

API-first, Internet of Things (IoT) stack for Typescript, official implementation of the Internet Open Protocols Alliance (IOPA) reference pattern

501 lines (441 loc) 14.7 kB
/* eslint-disable no-restricted-syntax */ /* * Internet Open Protocol Abstraction (IOPA) * Copyright (c) 2016-2022 Internet Open Protocols Alliance * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import type { FC, IRef, IopaApp, IPlugin, IContextCore, IAppProperties, IAppCapabilityLegacy, IFactory, IContextIopa, IMap, HandlerResult, AppHandler, IRouterApp, IConfig, Next, DefaultHandler } from '@iopa/types' import { URN_APP, VERSION } from '../iopa/constants' import guid from '../util/guid' import FactoryCore from '../iopa/factory-core' import FactoryEdge from '../iopa/factory-edge' import logger from '../iopa/logging' import IopaMap from '../iopa/map' import { ExtendableEventTarget, TraceEvent } from '../util/events' import Middleware from './middleware' type MiddlewareFC<C = any> = FC<C> & { id: string } /** * AppBuilder Class to Compile/Build all Middleware in the Pipeline into single IOPA AppFunc **/ export default class AppBuilder<P = any, C = any> implements IopaApp<P, C> { public properties: IMap<IAppProperties<C>> private _middleware: { invoke: Array<MiddlewareFC> dispatch: Array<MiddlewareFC> } public middlewareProxy: typeof Middleware = Middleware private _factory: IFactory<C, IContextIopa<C>> private _abortController: AbortController public constructor( appProperties?: Partial<IAppProperties<C>>, appOptions: Partial<IAppCapabilityLegacy> = { 'server.Version': VERSION, 'server.Base': 'core' } ) { this._abortController = new AbortController() process.on('exit', this._abort) process.on('SIGINT', this._abort) process.on('uncaughtException', this._exception) this._abortController.signal.addEventListener('abort', this._dispose) const defaults: IAppProperties<C> = { 'iopa.Id': guid(), 'server.Capabilities': { [URN_APP]: appOptions } as unknown as C, 'server.Events': new ExtendableEventTarget(), 'server.Logger': logger, 'server.NotFound': DefaultApp, 'server.ErrorHandler': ErrorHandler, 'server.IsBuilt': false, 'server.Pipeline': undefined, 'server.AbortController': this._abortController, 'server.AbortSignal': this._abortController.signal } as any if (typeof appProperties === 'string') { appProperties = { 'iopa.Id': appProperties as string } as any } if (appProperties) { Object.entries(appProperties).forEach(([key, value]) => { if (key === 'server.Capabilities') { const capabilities = defaults['server.Capabilities'] Object.entries(value!).forEach(([key2, value2]) => { capabilities[key2] = value2 }) } else { defaults[key] = value } }) } this._factory = defaults['server.Factory'] ?? appOptions['server.Base'] === 'request' ? new FactoryEdge() : new FactoryCore() this.properties = new IopaMap<IAppProperties<C>>(defaults) this._middleware = { invoke: [], dispatch: [] } } private _dispose: () => void = () => { this._abortController.signal.removeEventListener('abort', this._dispose) process.off('exit', this._abort) process.off('SIGINT', this._abort) process.off('uncaughtException', this._exception) } private _abort: () => void = () => { this._abortController.abort() } private _exception: (e: any) => void = (e) => { console.log('Uncaught Exception...') console.log(e.stack) this._abortController.abort() process.exit(99) } public capability<T>(keyOrRef: IRef<T>): T | undefined public capability<K extends keyof C>(keyOrRef: K): C[K] { if (typeof keyOrRef === 'string') { return this._capability(keyOrRef as keyof C) as any } else { return this._capability( (keyOrRef as unknown as IRef<any>).id as unknown as keyof C ) as any } } private _capability<K extends keyof C>(keyOrRef: K): C[K] { return this.properties.get('server.Capabilities')[keyOrRef] } public setCapability<K extends keyof C>(keyOrRef: K, value: C[K]) public setCapability<T, I extends T>(keyOrRef: IRef<T>, value: I): void { if (typeof keyOrRef === 'string') { return this._setCapability(keyOrRef as keyof C, value) } else { return this._setCapability( (keyOrRef as unknown as IRef<any>).id as unknown as keyof C, value ) } } private _setCapability<K extends keyof C>(keyOrRef: K, value: any): void { this.properties.get('server.Capabilities')[keyOrRef] = value } public createContext( urlOrRequest?: string | Request, options?: any ): IContextIopa<C> { return this._factory.createContext(urlOrRequest, options) } public fork(when: (context: any) => boolean): IopaApp<P, C> { const subApp = new AppBuilder<P, C>(this.properties.toJSON()) this.use(async function forkFunction( context: IContextCore<C>, next: () => Promise<void> ) { if (!subApp.properties.get('server.IsBuilt')) { subApp.build() } if (when(context)) { await subApp.invoke(context) return Promise.resolve(null) // do not call next on main pipeline after forked pipeline is invoked } return next() }, 'forkFuncton') return subApp } /** * Add Middleware Function to AppBuilder pipeline **/ public use(mw: any, id: string, options: unknown): this public use(mw: any, id: string): this public use(mw: any): this public use(method: 'invoke' | 'dispatch', mw?: any): this public use(arg0: any, arg1?: any, options?: any): this { let id, mw, method /** Fix comnmon es6 module interop issues */ if (typeof arg0 === 'object' && 'default' in arg0) { // eslint-disable-next-line dot-notation arg0 = arg0['default'] } else if (typeof arg1 === 'object' && 'default' in arg1) { // eslint-disable-next-line dot-notation arg1 = arg1['default'] } if (typeof arg0 === 'function' && typeof arg1 === 'string') { /** resilience wrapper style */ id = arg1 mw = arg0 method = 'invoke' } else if (typeof arg0 === 'function' && !arg1) { id = arg0.name || arg0.toString().split(/\r?\n/)[0] mw = arg0 method = 'invoke' } else if (arg0 === undefined) { throw new Error( `app.use called with undefined / empty middleware for ${mw}` ) } else { id = arg1.name || arg1.toString().split(/\r?\n/)[0] method = arg0 mw = arg1 } if (!this._middleware[method]) { throw new Error( `Unknown AppBuilder Category ${JSON.stringify( method, null, 2 )} for ${id}` ) } const params = _getParams(mw) if (params === 'app' || mw.length === 1) { const Mw = mw let mwInstance: any try { mwInstance = new Mw(this, options) } catch (ex) { // this is a js lambda function not a class Instance mwInstance = Mw(this, options) || {} } if (typeof mwInstance.invoke === 'function') { const fn = mwInstance.invoke.bind(mwInstance) fn.id = id this._middleware.invoke.push(fn) } if (typeof mwInstance.dispatch === 'function') { const fn = mwInstance.dispatch.bind(mwInstance) fn.id = id this._middleware.dispatch.push(fn) } } else { if (options) { throw new Error( `Cannot instantiate AppFunc ${id} with options, use a class constructor instead` ) } const fn: any = this.middlewareProxy(this, mw) fn.id = id this._middleware[method].push(fn) } return this } public dispose(): void { /** noop */ } /** * Compile/Build all Middleware in the Pipeline into single IOPA AppFunc **/ public build(): FC<C> & { properties: IMap<IAppProperties<C>> dispatch: MiddlewareFC<C> } { const middleware = this._middleware.invoke.concat( this.properties.get('server.NotFound') as any ) const pipeline: FC<C> & { properties: IMap<IAppProperties<C>> dispatch: FC<C> } = this._compose(middleware) as any if (this._middleware.dispatch.length > 0) { pipeline.dispatch = this._compose(this._middleware.dispatch.reverse()) } else { pipeline.dispatch = (context) => { return Promise.resolve() } } pipeline.properties = this.properties this.properties.set('server.IsBuilt', true) this.properties.set('server.Pipeline', pipeline) return pipeline as any } private _buildDynamic(handler: DefaultHandler): FC<C> { const middleware = this._middleware.invoke.concat(handler as any) return this._compose(middleware) as any } /** Call Dispatch Pipeline to process given context */ public dispatch(context: IContextCore<C>): any { return this.properties.get('server.Pipeline').dispatch.call(this, context) } /** Call App Invoke Pipeline to process given context */ public invoke(context: IContextCore<C>): Promise<void> { context.log = this.properties .get('server.Logger') .child({ tid: context.get('iopa.Id') }) if (!this.properties.get('server.IsBuilt')) { this.build() } return this.properties.get('server.Pipeline').call(this, context) } /** invoke a new child request on the iopa pipeline with added default handler */ public childInvoke( context: IContextIopa<C>, handler: DefaultHandler & { id?: string }, id?: string ): Promise<void> { handler.id = id || 'childInvoke' const pipeline = this._buildDynamic(handler) context.log = this.properties .get('server.Logger') .child({ tid: context.get('iopa.Id') }) return pipeline.call(this, context) } /** Compile/Build all Middleware in the Pipeline into single IOPA AppFunc */ private _compose( middleware: Array<MiddlewareFC> ): (context: IContextCore<C>) => Promise<void> { let i: number let next: (context: IContextCore<C>) => Promise<void> let curr: FC<C> i = middleware.length next = () => { return Promise.resolve() } // eslint-disable-next-line no-plusplus while (i--) { curr = middleware[i] if (!curr) { console.error(middleware, i) throw new Error('Missing middleware') } next = (( fn: AppHandler & { id: string }, prev: FC<C>, context: IContextCore<C> ) => { context.set('server.CurrentMiddleware', fn.id) const _next: Next & { invoke: FC<C> } = () => { context.dispatchEvent( new TraceEvent('trace-next', { label: fn.id }) ) return prev.call(this, context).then(() => { context.dispatchEvent( new TraceEvent('trace-next-resume', { label: fn.id }) ) }) } _next.invoke = prev context.dispatchEvent( new TraceEvent('trace-start', { label: fn.id }) ) const _oldLogger = context.log context.log = context.log.child({ middleware: fn.id }) return Promise.resolve(fn.call(this, context, _next)).then( (res: HandlerResult) => { context.log = _oldLogger if (res || res === false || res === 0 || res === '') { ;(context as IContextIopa).respondWith(res) } context.dispatchEvent( new TraceEvent('trace-end', { label: fn.id }) ) } ) }).bind(this, curr, next) } const rootPipeline = next return (context: IContextCore<C>) => { const capabilities = this.properties.get('server.Capabilities') context.set('server.Capabilities', capabilities) context.set('server.Events', this.properties.get('server.Events')) context.app = this as any return rootPipeline.call(this, context) } } public usePlugin(plugin: IPlugin<P, C>): this { throw new Error('Not Implemented') } } export class RouterApp extends AppBuilder implements IRouterApp { public head: (path: string, handler: AppHandler) => this public post: (path: string, handler: AppHandler) => this public get: (path: string, handler: AppHandler) => this public options: (path: string, handler: AppHandler) => this public put: (path: string, handler: AppHandler) => this public delete: (path: string, handler: AppHandler) => this public patch: (path: string, handler: AppHandler) => this public all: (path: string, handler: AppHandler) => this public config: IConfig public constructor( appProperties: Partial<IAppProperties<any>> = undefined, appOptions: Partial<IAppCapabilityLegacy> = {} ) { super(appProperties, { 'server.Base': 'request', ...appOptions }) } public registerFeatureFlag(flagOptions: { name: string }): this { throw new Error('Method not implemented.') } } /** * Not Found app used at end of pipeline if not handled by any other middleware * Default is no-op, replace 404 responder as appropriate **/ function DefaultApp<C>( context: IContextCore<C> ): HandlerResult | PromiseLike<HandlerResult> { return Promise.resolve() } DefaultApp.id = 'DefaultApp' /** * Not Found app used at end of pipeline if not handled by any other middleware * Default is throw, replace 500 responder as appropriate **/ function ErrorHandler<C>( ex: Error, context: IContextCore<C> ): HandlerResult | PromiseLike<HandlerResult> { throw ex } ErrorHandler.id = 'ErrorHandler' const STRIP_COMMENTS: RegExp = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm /** * Gets the parameter names of a javascript function as a comma separated string **/ function _getParams(func: Function): string { const fnStr = func.toString().replace(STRIP_COMMENTS, '') let result = fnStr .slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')) .match(/([^\s,]+)/g) if (result === null) { result = [] } return result.join() }