@midwayjs/faas
Version:
Midway Framework for FaaS (Function as a Service)
480 lines • 20 kB
JavaScript
"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