1 |
|
2 |
|
3 |
|
4 | import * as coreTypes from '@azure/functions-core';
|
5 | import {
|
6 | CoreInvocationContext,
|
7 | InvocationArguments,
|
8 | RpcBindingInfo,
|
9 | RpcInvocationResponse,
|
10 | RpcLogCategory,
|
11 | RpcLogLevel,
|
12 | RpcTypedData,
|
13 | } from '@azure/functions-core';
|
14 | import { format } from 'util';
|
15 | import { returnBindingKey } from './constants';
|
16 | import { fromRpcBindings } from './converters/fromRpcBindings';
|
17 | import { fromRpcRetryContext, fromRpcTraceContext } from './converters/fromRpcContext';
|
18 | import { fromRpcTriggerMetadata } from './converters/fromRpcTriggerMetadata';
|
19 | import { fromRpcTypedData } from './converters/fromRpcTypedData';
|
20 | import { toCamelCaseValue } from './converters/toCamelCase';
|
21 | import { toRpcHttp } from './converters/toRpcHttp';
|
22 | import { toRpcTypedData } from './converters/toRpcTypedData';
|
23 | import { InvocationContext } from './InvocationContext';
|
24 | import { isHttpTrigger, isTimerTrigger, isTrigger } from './utils/isTrigger';
|
25 | import { isDefined, nonNullProp, nonNullValue } from './utils/nonNull';
|
26 |
|
27 | export 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 |
|
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 |
|
113 |
|
114 |
|
115 |
|
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 | }
|