UNPKG

@midwayjs/faas

Version:

Midway Framework for FaaS (Function as a Service)

480 lines 20 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MidwayFaaSFramework = void 0; const core_1 = require("@midwayjs/core"); const simple_lock_1 = require("@midwayjs/simple-lock"); const logger_1 = require("@midwayjs/logger"); const serverless_http_parser_1 = require("@midwayjs/serverless-http-parser"); const util_1 = require("util"); const debug = (0, util_1.debuglog)('midway:debug'); const { isAnyArrayBuffer, isUint8Array } = util_1.types; const LOCK_KEY = '_faas_starter_start_key'; let MidwayFaaSFramework = class MidwayFaaSFramework extends core_1.BaseFramework { defaultHandlerMethod = 'handler'; funMappingStore = new Map(); lock = new simple_lock_1.default(); isReplaceLogger = process.env['MIDWAY_SERVERLESS_REPLACE_LOGGER'] === 'true'; developmentRun = false; server; respond; applicationAdapter; serverlessFunctionService; httpMiddlewareManager = this.createMiddlewareManager(); eventMiddlewareManager = this.createMiddlewareManager(); legacyVersion = false; loadedFunction = false; configure(options) { const faasConfig = this.configService.getConfiguration('faas') ?? {}; if (options || faasConfig['developmentRun']) { this.developmentRun = true; this.configurationOptions = options; } else { return faasConfig; } } isEnable() { return !this.developmentRun; } async applicationInitialize(options) { if (!this.logger) { this.logger = options.logger || logger_1.loggers.getLogger('appLogger'); } this.applicationAdapter = this.configurationOptions.applicationAdapter || {}; if (this.applicationAdapter.getApplication) { this.legacyVersion = true; } this.app = this.applicationAdapter.getApplication?.() || new serverless_http_parser_1.Application(); this.defineApplicationProperties({ /** * return init context value such as aliyun fc */ getInitializeContext: () => { return this.configurationOptions.initializeContext; }, /** * @deprecated * @param middlewareId */ generateMiddleware: async (middlewareId) => { return this.generateMiddleware(middlewareId); }, getFunctionName: () => { return (process.env.MIDWAY_SERVERLESS_FUNCTION_NAME || this.configurationOptions.applicationAdapter?.getFunctionName() || ''); }, /** * get function service/group in runtime */ getFunctionServiceName: () => { return (process.env.MIDWAY_SERVERLESS_SERVICE_NAME || this.configurationOptions.applicationAdapter?.getFunctionServiceName() || ''); }, useEventMiddleware: middleware => { return this.useEventMiddleware(middleware); }, getEventMiddleware: () => { return this.getEventMiddleware(); }, getServerlessInstance: async () => { throw new core_1.MidwayFeatureNotImplementedError('Please use it in by @midwayjs/mock in test.'); }, invokeTriggerFunction: (context, handlerMapping, options) => { return this.invokeTriggerFunction(context, handlerMapping, options); }, }); // hack use method this.app.originUse = this.app.use; this.app.use = this.app.useMiddleware; if (this.configurationOptions.applicationAdapter?.runAppHook) { this.configurationOptions.applicationAdapter.runAppHook(this.app); } } async run() { if (this.legacyVersion) { return this.loadFunction(); } } async loadFunction() { if (!this.loadedFunction) { this.loadedFunction = true; return this.lock.sureOnce(async () => { // set app keys this.app['keys'] = this.configService.getConfiguration('keys') ?? ''; // store all http function entry this.serverlessFunctionService = await this.applicationContext.getAsync(core_1.MidwayServerlessFunctionService); const functionList = await this.serverlessFunctionService.getFunctionList(); debug(`[faas]: load ${functionList.length} function list`); for (const funcInfo of functionList) { debug(`[faas]: load function ${funcInfo.funcHandlerName}, router = ${funcInfo.fullUrl}`); // store handler this.funMappingStore.set(funcInfo.funcHandlerName, funcInfo); } this.respond = this.app.callback(); }, LOCK_KEY); } } /** * @deprecated * @param handlerMapping */ handleInvokeWrapper(handlerMapping) { const funOptions = this.funMappingStore.get(handlerMapping); return async (...args) => { if (args.length === 0) { throw new Error('first parameter must be function context'); } if (!funOptions) { throw new Error(`function handler = ${handlerMapping} not found`); } const context = this.getContext(args.shift()); const isHttpFunction = !!(context.headers && context.get); const globalMiddlewareFn = await this.applyMiddleware(); const middlewareManager = new core_1.ContextMiddlewareManager(); middlewareManager.insertLast(globalMiddlewareFn); middlewareManager.insertLast(async (ctx, next) => { const fn = await this.middlewareService.compose([ ...(isHttpFunction ? this.httpMiddlewareManager : this.eventMiddlewareManager), ...funOptions.controllerMiddleware, ...funOptions.middleware, async (ctx, next) => { if (funOptions.controllerClz && typeof funOptions.method === 'string') { const isPassed = await this.app .getFramework() .runGuard(ctx, funOptions.controllerClz, funOptions.method); if (!isPassed) { throw new core_1.httpError.ForbiddenError(); } } if (isHttpFunction) { args = [ctx]; } // invoke handler const result = await this.invokeHandler(funOptions, ctx, args, isHttpFunction); if (isHttpFunction && result !== undefined) { if (result === null) { // 这样设置可以绕过 koa 的 _explicitStatus 赋值机制 ctx.response._body = null; } else { ctx.body = result; } } return result; }, ], this.app); return await fn(ctx, next); }); const composeMiddleware = await this.middlewareService.compose(middlewareManager, this.app); return await composeMiddleware(context); }; } async invokeTriggerFunction(context, handlerMapping, options) { let funOptions = this.funMappingStore.get(handlerMapping); const isHttpFunction = options.isHttpFunction; if (!funOptions && isHttpFunction) { funOptions = await this.serverlessFunctionService.getMatchedRouterInfo(context.path, context.method); if (funOptions) { const matchRes = core_1.PathToRegexpUtil.match(funOptions.fullUrlFlattenString)(context.path); context.req.pathParameters = matchRes['params'] || {}; } else { // options request pass through to middleware if (context.method?.toLowerCase() === 'options') { funOptions = { url: context.path, method: 'options', requestMethod: 'options', controllerMiddleware: [], middleware: [], }; } } } if (!funOptions) { throw new Error(`function handler = ${handlerMapping} not found`); } context = this.getContext(context); if (this.configurationOptions.applicationAdapter?.runContextHook) { this.configurationOptions.applicationAdapter.runContextHook(context); } const traceService = this.applicationContext.get(core_1.MidwayTraceService); const traceMetaResolver = this.configurationOptions?.tracing?.meta; const traceEnabled = this.configurationOptions?.tracing?.enable !== false; const traceExtractor = this.configurationOptions?.tracing ?.extractor; const entryCarrierDefault = isHttpFunction ? context.headers || {} : context.originEvent?.headers || context.originEvent?.properties || context.originEvent || {}; const entryCarrier = typeof traceExtractor === 'function' ? traceExtractor({ ctx: context, request: context.originEvent, response: context.originContext, custom: { handlerMapping, isHttpFunction, }, }) : entryCarrierDefault; const result = await traceService.runWithEntrySpan(`faas ${handlerMapping}`, { enable: traceEnabled, carrier: entryCarrier ?? entryCarrierDefault, attributes: { 'midway.protocol': isHttpFunction ? 'faas-http' : 'faas-event', 'midway.faas.handler': handlerMapping, }, meta: traceMetaResolver, metaArgs: { ctx: context, carrier: entryCarrier ?? entryCarrierDefault, request: context.originEvent, response: context.originContext, custom: { handlerMapping, isHttpFunction, }, }, }, async () => await (await this.applyMiddleware(async (ctx, next) => { const fn = await this.middlewareService.compose([ ...(isHttpFunction ? this.httpMiddlewareManager : this.eventMiddlewareManager), ...funOptions.controllerMiddleware, ...funOptions.middleware, async (ctx, next) => { let args; if (isHttpFunction) { args = [ctx]; } else { args = [ctx.originEvent, ctx.originContext]; } // invoke handler const result = await this.invokeHandler(funOptions, ctx, args, isHttpFunction); if (isHttpFunction && result !== undefined) { if (result === null) { // 这样设置可以绕过 koa 的 _explicitStatus 赋值机制 ctx.response._body = null; } else { ctx.body = result; } } // http 靠 ctx.body,否则会出现状态码不正确的问题 if (!isHttpFunction) { return result; } }, ], this.app); return await fn(ctx, next); }))(context)); if (isHttpFunction) { if (options.isCustomHttpResponse) { return context.body; } else { return this.formatHttpResponse(context, options); } } else { return result; } } formatHttpResponse(context, options = {}) { if (!context.response?._explicitStatus) { if (context.body === null || context.body === 'undefined') { context.body = ''; context.type = 'text'; context.status = 204; } } let encoded = false; const data = context.body; if (typeof data === 'string') { if (!context.type) { context.type = 'text/plain'; } context.body = data; } else if (isAnyArrayBuffer(data) || isUint8Array(data)) { if (!context.type) { context.type = 'application/octet-stream'; } if (options.supportBufferResponse) { context.body = data; } else { encoded = true; // data is reserved as buffer context.body = Buffer.from(data).toString('base64'); context.length = data.byteLength; } } else if (typeof data === 'object') { if (!context.type) { context.type = 'application/json'; } // set data to string context.body = JSON.stringify(data); } else { if (!context.type) { context.type = 'text/plain'; } // set data to string context.body = data + ''; } // middleware return value and will be got 204 status if (context.body === undefined && !context.response._explicitStatus && context._matchedRoute) { // 如果进了路由,重新赋值,防止 404 context.body = undefined; } return { isBase64Encoded: encoded, statusCode: context.status, headers: context.res.headers, body: context.body, }; } async wrapHttpRequest(req, res, options) { const newReq = res ? new serverless_http_parser_1.HTTPRequest(req, res) : req; const newRes = new serverless_http_parser_1.HTTPResponse(options); return this.createHttpContext(newReq, newRes); } /** * @deprecated * @param middlewareId */ async generateMiddleware(middlewareId) { const mwIns = await this.getApplicationContext().getAsync(middlewareId); return mwIns.resolve(); } getContext(context = {}) { if (!context.env) { context.env = this.environmentService.getCurrentEnvironment(); } if (this.isReplaceLogger || !context.logger) { context._serverlessLogger = this.createContextLogger(context); /** * 由于 fc 的 logger 有 bug,FC公有云环境我们会默认替换掉,其他平台后续视情况而定 */ Object.defineProperty(context, 'logger', { get() { return context._serverlessLogger; }, }); } this.app.createAnonymousContext(context); return context; } async invokeHandler(routerInfo, context, args, isHttpFunction) { if (typeof routerInfo.method !== 'string') { if (!isHttpFunction) { args.unshift(context); } return routerInfo.method(...args); } else { const funModule = await context.requestContext.getAsync(routerInfo.controllerId); const handlerName = this.getFunctionHandler(context, args, funModule, routerInfo.method) || this.defaultHandlerMethod; if (funModule[handlerName]) { // invoke real method const result = await funModule[handlerName](...args); // implement response decorator const routerResponseData = routerInfo.responseMetadata; if (isHttpFunction) { for (const routerRes of routerResponseData) { switch (routerRes.type) { case core_1.WEB_RESPONSE_HTTP_CODE: context.status = routerRes.code; break; case core_1.WEB_RESPONSE_HEADER: for (const key in routerRes?.setHeaders || {}) { context.set(key, routerRes.setHeaders[key]); } break; case core_1.WEB_RESPONSE_CONTENT_TYPE: context.type = routerRes.contentType; break; case core_1.WEB_RESPONSE_REDIRECT: context.status = routerRes.code; context.redirect(routerRes.url); return; } } } return result; } } } getFunctionHandler(ctx, args, target, method) { if (method && typeof target[method] !== 'undefined') { return method; } const handlerMethod = this.defaultHandlerMethod; if (handlerMethod && typeof target[handlerMethod] !== 'undefined') { return handlerMethod; } throw new Error(`no handler setup on ${target.name}#${method || this.defaultHandlerMethod}`); } createLogger(name, option = {}) { // 覆盖基类的创建日志对象,函数场景下的日志,即使自定义,也只启用控制台输出 return (0, logger_1.createConsoleLogger)(name, option); } getFrameworkName() { return 'faas'; } getServer() { return this.server; } async beforeStop() { if (this.server) { new Promise(resolve => { this.server.close(resolve); }); } } async createHttpContext(req, res) { return new Promise(resolve => { this.respond(req, res, resolve); }); } useMiddleware(middleware) { this.httpMiddlewareManager.insertLast(middleware); } useEventMiddleware(middleware) { this.eventMiddlewareManager.insertLast(middleware); } getEventMiddleware() { return this.eventMiddlewareManager; } getAllHandlerNames() { return Array.from(this.funMappingStore.keys()); } }; exports.MidwayFaaSFramework = MidwayFaaSFramework; exports.MidwayFaaSFramework = MidwayFaaSFramework = __decorate([ (0, core_1.Framework)() ], MidwayFaaSFramework); //# sourceMappingURL=framework.js.map