UNPKG

6.26 kBPlain TextView Raw
1// Copyright (c) .NET Foundation. All rights reserved.
2// Licensed under the MIT License.
3
4import * as coreTypes from '@azure/functions-core';
5import {
6 CoreInvocationContext,
7 InvocationArguments,
8 RpcBindingInfo,
9 RpcInvocationResponse,
10 RpcLogCategory,
11 RpcLogLevel,
12 RpcTypedData,
13} from '@azure/functions-core';
14import { format } from 'util';
15import { returnBindingKey } from './constants';
16import { fromRpcBindings } from './converters/fromRpcBindings';
17import { fromRpcRetryContext, fromRpcTraceContext } from './converters/fromRpcContext';
18import { fromRpcTriggerMetadata } from './converters/fromRpcTriggerMetadata';
19import { fromRpcTypedData } from './converters/fromRpcTypedData';
20import { toCamelCaseValue } from './converters/toCamelCase';
21import { toRpcHttp } from './converters/toRpcHttp';
22import { toRpcTypedData } from './converters/toRpcTypedData';
23import { InvocationContext } from './InvocationContext';
24import { isHttpTrigger, isTimerTrigger, isTrigger } from './utils/isTrigger';
25import { isDefined, nonNullProp, nonNullValue } from './utils/nonNull';
26
27export class InvocationModel implements coreTypes.InvocationModel {
28 #isDone = false;
29 #coreCtx: CoreInvocationContext;
30 #functionName: string;
31 #bindings: Record<string, RpcBindingInfo>;
32 #triggerType: string;
33
34 constructor(coreCtx: CoreInvocationContext) {
35 this.#coreCtx = coreCtx;
36 this.#functionName = nonNullProp(coreCtx.metadata, 'name');
37 this.#bindings = nonNullProp(coreCtx.metadata, 'bindings');
38 const triggerBinding = nonNullValue(
39 Object.values(this.#bindings).find((b) => isTrigger(b.type)),
40 'triggerBinding'
41 );
42 this.#triggerType = nonNullProp(triggerBinding, 'type');
43 }
44
45 // eslint-disable-next-line @typescript-eslint/require-await
46 async getArguments(): Promise<InvocationArguments> {
47 const req = this.#coreCtx.request;
48
49 const context = new InvocationContext({
50 invocationId: nonNullProp(this.#coreCtx, 'invocationId'),
51 functionName: this.#functionName,
52 logHandler: (level: RpcLogLevel, ...args: unknown[]) => this.#userLog(level, ...args),
53 retryContext: fromRpcRetryContext(req.retryContext),
54 traceContext: fromRpcTraceContext(req.traceContext),
55 triggerMetadata: fromRpcTriggerMetadata(req.triggerMetadata, this.#triggerType),
56 options: fromRpcBindings(this.#bindings),
57 });
58
59 const inputs: unknown[] = [];
60 if (req.inputData) {
61 for (const binding of req.inputData) {
62 const bindingName = nonNullProp(binding, 'name');
63 let input: unknown = fromRpcTypedData(binding.data);
64
65 const bindingType = this.#bindings[bindingName].type;
66 if (isTimerTrigger(bindingType)) {
67 input = toCamelCaseValue(input);
68 }
69
70 if (isTrigger(bindingType)) {
71 inputs.push(input);
72 } else {
73 context.extraInputs.set(bindingName, input);
74 }
75 }
76 }
77
78 return { context, inputs };
79 }
80
81 async invokeFunction(
82 context: InvocationContext,
83 inputs: unknown[],
84 handler: coreTypes.FunctionCallback
85 ): Promise<unknown> {
86 try {
87 return await Promise.resolve(handler(...inputs, context));
88 } finally {
89 this.#isDone = true;
90 }
91 }
92
93 async getResponse(context: InvocationContext, result: unknown): Promise<RpcInvocationResponse> {
94 const response: RpcInvocationResponse = { invocationId: this.#coreCtx.invocationId };
95
96 response.outputData = [];
97 let usedReturnValue = false;
98 for (const [name, binding] of Object.entries(this.#bindings)) {
99 if (binding.direction === 'out') {
100 if (name === returnBindingKey) {
101 response.returnValue = await this.#convertOutput(binding, result);
102 usedReturnValue = true;
103 } else {
104 const outputValue = await this.#convertOutput(binding, context.extraOutputs.get(name));
105 if (isDefined(outputValue)) {
106 response.outputData.push({ name, data: outputValue });
107 }
108 }
109 }
110 }
111
112 // This allows the return value of non-HTTP triggered functions to be passed back
113 // to the host, even if no explicit output binding is set. In most cases, this is ignored,
114 // but e.g., Durable uses this to pass orchestrator state back to the Durable extension, w/o
115 // an explicit output binding. See here for more details: https://github.com/Azure/azure-functions-nodejs-library/pull/25
116 if (!usedReturnValue && !isHttpTrigger(this.#triggerType)) {
117 response.returnValue = toRpcTypedData(result);
118 }
119
120 return response;
121 }
122
123 async #convertOutput(binding: RpcBindingInfo, value: unknown): Promise<RpcTypedData | null | undefined> {
124 if (binding.type?.toLowerCase() === 'http') {
125 return toRpcHttp(value);
126 } else {
127 return toRpcTypedData(value);
128 }
129 }
130
131 #log(level: RpcLogLevel, logCategory: RpcLogCategory, ...args: unknown[]): void {
132 this.#coreCtx.log(level, logCategory, format(...args));
133 }
134
135 #systemLog(level: RpcLogLevel, ...args: unknown[]) {
136 this.#log(level, 'system', ...args);
137 }
138
139 #userLog(level: RpcLogLevel, ...args: unknown[]): void {
140 if (this.#isDone && this.#coreCtx.state !== 'postInvocationHooks') {
141 let badAsyncMsg =
142 "Warning: Unexpected call to 'log' on the context object after function execution has completed. Please check for asynchronous calls that are not awaited. ";
143 badAsyncMsg += `Function name: ${this.#functionName}. Invocation Id: ${this.#coreCtx.invocationId}.`;
144 this.#systemLog('warning', badAsyncMsg);
145 }
146 this.#log(level, 'user', ...args);
147 }
148}