@azure/functions
Version:
Microsoft Azure Functions NodeJS Framework
149 lines (130 loc) • 6.26 kB
text/typescript
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.
import * as coreTypes from '@azure/functions-core';
import {
CoreInvocationContext,
InvocationArguments,
RpcBindingInfo,
RpcInvocationResponse,
RpcLogCategory,
RpcLogLevel,
RpcTypedData,
} from '@azure/functions-core';
import { format } from 'util';
import { returnBindingKey } from './constants';
import { fromRpcBindings } from './converters/fromRpcBindings';
import { fromRpcRetryContext, fromRpcTraceContext } from './converters/fromRpcContext';
import { fromRpcTriggerMetadata } from './converters/fromRpcTriggerMetadata';
import { fromRpcTypedData } from './converters/fromRpcTypedData';
import { toCamelCaseValue } from './converters/toCamelCase';
import { toRpcHttp } from './converters/toRpcHttp';
import { toRpcTypedData } from './converters/toRpcTypedData';
import { InvocationContext } from './InvocationContext';
import { isHttpTrigger, isTimerTrigger, isTrigger } from './utils/isTrigger';
import { isDefined, nonNullProp, nonNullValue } from './utils/nonNull';
export class InvocationModel implements coreTypes.InvocationModel {
#isDone = false;
#coreCtx: CoreInvocationContext;
#functionName: string;
#bindings: Record<string, RpcBindingInfo>;
#triggerType: string;
constructor(coreCtx: CoreInvocationContext) {
this.#coreCtx = coreCtx;
this.#functionName = nonNullProp(coreCtx.metadata, 'name');
this.#bindings = nonNullProp(coreCtx.metadata, 'bindings');
const triggerBinding = nonNullValue(
Object.values(this.#bindings).find((b) => isTrigger(b.type)),
'triggerBinding'
);
this.#triggerType = nonNullProp(triggerBinding, 'type');
}
// eslint-disable-next-line @typescript-eslint/require-await
async getArguments(): Promise<InvocationArguments> {
const req = this.#coreCtx.request;
const context = new InvocationContext({
invocationId: nonNullProp(this.#coreCtx, 'invocationId'),
functionName: this.#functionName,
logHandler: (level: RpcLogLevel, ...args: unknown[]) => this.#userLog(level, ...args),
retryContext: fromRpcRetryContext(req.retryContext),
traceContext: fromRpcTraceContext(req.traceContext),
triggerMetadata: fromRpcTriggerMetadata(req.triggerMetadata, this.#triggerType),
options: fromRpcBindings(this.#bindings),
});
const inputs: unknown[] = [];
if (req.inputData) {
for (const binding of req.inputData) {
const bindingName = nonNullProp(binding, 'name');
let input: unknown = fromRpcTypedData(binding.data);
const bindingType = this.#bindings[bindingName].type;
if (isTimerTrigger(bindingType)) {
input = toCamelCaseValue(input);
}
if (isTrigger(bindingType)) {
inputs.push(input);
} else {
context.extraInputs.set(bindingName, input);
}
}
}
return { context, inputs };
}
async invokeFunction(
context: InvocationContext,
inputs: unknown[],
handler: coreTypes.FunctionCallback
): Promise<unknown> {
try {
return await Promise.resolve(handler(...inputs, context));
} finally {
this.#isDone = true;
}
}
async getResponse(context: InvocationContext, result: unknown): Promise<RpcInvocationResponse> {
const response: RpcInvocationResponse = { invocationId: this.#coreCtx.invocationId };
response.outputData = [];
let usedReturnValue = false;
for (const [name, binding] of Object.entries(this.#bindings)) {
if (binding.direction === 'out') {
if (name === returnBindingKey) {
response.returnValue = await this.#convertOutput(binding, result);
usedReturnValue = true;
} else {
const outputValue = await this.#convertOutput(binding, context.extraOutputs.get(name));
if (isDefined(outputValue)) {
response.outputData.push({ name, data: outputValue });
}
}
}
}
// This allows the return value of non-HTTP triggered functions to be passed back
// to the host, even if no explicit output binding is set. In most cases, this is ignored,
// but e.g., Durable uses this to pass orchestrator state back to the Durable extension, w/o
// an explicit output binding. See here for more details: https://github.com/Azure/azure-functions-nodejs-library/pull/25
if (!usedReturnValue && !isHttpTrigger(this.#triggerType)) {
response.returnValue = toRpcTypedData(result);
}
return response;
}
async #convertOutput(binding: RpcBindingInfo, value: unknown): Promise<RpcTypedData | null | undefined> {
if (binding.type?.toLowerCase() === 'http') {
return toRpcHttp(value);
} else {
return toRpcTypedData(value);
}
}
#log(level: RpcLogLevel, logCategory: RpcLogCategory, ...args: unknown[]): void {
this.#coreCtx.log(level, logCategory, format(...args));
}
#systemLog(level: RpcLogLevel, ...args: unknown[]) {
this.#log(level, 'system', ...args);
}
#userLog(level: RpcLogLevel, ...args: unknown[]): void {
if (this.#isDone && this.#coreCtx.state !== 'postInvocationHooks') {
let badAsyncMsg =
"Warning: Unexpected call to 'log' on the context object after function execution has completed. Please check for asynchronous calls that are not awaited. ";
badAsyncMsg += `Function name: ${this.#functionName}. Invocation Id: ${this.#coreCtx.invocationId}.`;
this.#systemLog('warning', badAsyncMsg);
}
this.#log(level, 'user', ...args);
}
}