Skip to content

Commit

Permalink
chore: updated handler-kit-azure-func
Browse files Browse the repository at this point in the history
Reviewed-by: @lucacavallaro 
Refs: #32 SFEQS-2025
  • Loading branch information
silvicir authored Nov 2, 2023
1 parent 0be9437 commit b229e4e
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 278 deletions.
5 changes: 5 additions & 0 deletions .changeset/khaki-glasses-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pagopa/handler-kit-azure-func": major
---

handler-kit-azure-func updated to work with the new @azure/functions programming model
2 changes: 1 addition & 1 deletion packages/handler-kit-azure-func/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"io-ts": "^2.2.20"
},
"devDependencies": {
"@azure/functions": "^3.5.0",
"@azure/functions": "^4.0.1",
"@pagopa/eslint-config": "^3.0.0",
"@rushstack/eslint-patch": "^1.2.0",
"eslint": "^8.36.0",
Expand Down
137 changes: 40 additions & 97 deletions packages/handler-kit-azure-func/src/__test__/function.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { describe, it, expect, vi } from "vitest";

import * as t from "io-ts";

import { Context, Form, AzureFunction } from "@azure/functions";
import { InvocationContext } from "@azure/functions";

import * as O from "fp-ts/Option";
import * as E from "fp-ts/Either";
import * as RTE from "fp-ts/ReaderTaskEither";

import { pipe, flow } from "fp-ts/function";
Expand All @@ -15,80 +14,31 @@ import * as H from "@pagopa/handler-kit";

import { azureFunction, httpAzureFunction } from "../function";

function logger() {}
logger.error = vi.fn(() => {});
logger.warn = () => {};
logger.info = () => {};
logger.verbose = () => {};

const baseCtx: Context = {
invocationId: "my-id",
executionContext: {
invocationId: "my-id",
functionName: "Greet",
functionDirectory: "./my-dir",
retryContext: null,
},
bindings: {},
bindingData: {
invocationId: "my-id",
},
traceContext: {
traceparent: null,
tracestate: null,
attributes: null,
},
bindingDefinitions: [],
log: logger,
done() {},
};

const invoke =
<P>(trigger: "httpTrigger" | "queueTrigger", name: string, payload: P) =>
(func: AzureFunction) =>
func({
...baseCtx,
bindingDefinitions: [
...baseCtx.bindingDefinitions,
{
type: trigger,
direction: "in",
name,
},
],
bindings: {
...baseCtx.bindings,
[name]: payload,
},
});

const invokeFromHttpRequest = (req: {
const HttpRequest = (req: {
method?: "GET" | "POST";
url?: string;
query?: Record<string, string>;
params?: Record<string, string>;
headers?: Record<string, string>;
body?: unknown;
}) =>
invoke("httpTrigger", "req", {
user: null,
get: () => undefined,
parseFormBody: () => ({} as Form),
body: undefined,
headers: {},
params: {},
query: {},
url: "https://my-test.url.com/api",
method: "GET",
...req,
});

const HttpResponseC = t.partial({
statusCode: t.union([t.number, t.string]),
body: t.any,
headers: t.record(t.string, t.string),
}) => ({
user: null,
get: () => undefined,
parseFormBody: () => ({} as unknown),
body: undefined,
headers: {},
params: {},
query: {},
url: "https://my-test.url.com/api",
method: "GET",
...req,
});

const ctx = {
error: console.error,
debug: console.debug,
} as InvocationContext;

describe("httpAzureFunction", () => {
const GreetHandler = H.of((req: H.HttpRequest) =>
pipe(
Expand All @@ -106,56 +56,49 @@ describe("httpAzureFunction", () => {
RTE.orElseW(flow(H.toProblemJson, H.problemJson, RTE.right))
)
);

it("wires the http request correctly", async () => {
const GreetFunction = httpAzureFunction(GreetHandler)({
lang: "it",
});
await expect(
pipe(
GreetFunction,
invokeFromHttpRequest({
query: {
name: "luca",
},
})
)
).resolves.toEqual(
const message = HttpRequest({
query: {
name: "luca",
},
});
const response = await GreetFunction(message, ctx);
expect(response.json()).resolves.toEqual(
expect.objectContaining({
body: {
message: "Ciao luca",
},
message: "Ciao luca",
})
);
});
it("returns an Azure Http Response", async () => {
const GreetFunction = httpAzureFunction(GreetHandler)({
lang: "en",
});
const response = await pipe(GreetFunction, invokeFromHttpRequest({}));
expect(pipe(response, H.parse(HttpResponseC), E.isRight)).toBe(true);
});

it("recovers from uncaught errors", async () => {
const CtxErrorSpy = vi.spyOn(ctx, "error");
const ErrorFunction = httpAzureFunction(
H.of((_) => RTE.left(new Error("unhandled error")))
)({});
const response = await pipe(ErrorFunction, invokeFromHttpRequest({}));
expect(logger.error).toHaveBeenCalled();
expect(pipe(response, H.parse(HttpResponseC))).toEqual(
const response = await ErrorFunction({}, ctx);
expect(CtxErrorSpy).toHaveBeenCalled();
expect(response.json()).resolves.toEqual(
expect.objectContaining({
right: expect.objectContaining({
statusCode: 500,
}),
status: 500,
})
);
});
});

describe("azureFunction", () => {
it("returns the same value of the handler", async () => {
it("returns the same value of the handler", () => {
const ctx = {
debug: console.debug,
} as InvocationContext;
const EchoFunc = azureFunction(H.of((str: string) => RTE.right(str)))({
inputDecoder: t.string,
});
const response = pipe(EchoFunc, invoke("queueTrigger", "str", "ping"));
await expect(response).resolves.toBe("ping");
const response = EchoFunc("ping", ctx);

expect(response).resolves.toBe("ping");
});
});
58 changes: 0 additions & 58 deletions packages/handler-kit-azure-func/src/__test__/trigger.spec.ts

This file was deleted.

72 changes: 36 additions & 36 deletions packages/handler-kit-azure-func/src/function.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,50 @@
import * as azure from "@azure/functions";
import { InvocationContext, HttpResponse } from "@azure/functions";

import * as t from "io-ts";

import * as T from "fp-ts/Task";
import * as RE from "fp-ts/ReaderEither";
import * as TE from "fp-ts/TaskEither";

import { sequenceS } from "fp-ts/Apply";
import { pipe, flow } from "fp-ts/function";

import * as H from "@pagopa/handler-kit";
import * as L from "@pagopa/logger";

import { getLogger } from "./logger";
import { getTriggerBindingData } from "./trigger";

const hasLogger = <R, I>(
const isHandlerEnvironment = <R, I>(
u: unknown
): u is R & { logger: L.Logger } & H.HandlerEnvironment<I> =>
typeof u === "object" && u !== null && "logger" in u;
): u is R & H.HandlerEnvironment<I> =>
typeof u === "object" && u !== null && "logger" in u && "input" in u;

// Populates Handler dependencies reading from azure.Context
const azureFunctionTE = <I, A, R>(
h: H.Handler<I, A, R>,
deps: Omit<R, "logger"> & { inputDecoder: t.Decoder<unknown, I> }
) =>
flow(
sequenceS(RE.Apply)({
logger: RE.fromReader(getLogger),
input: getTriggerBindingData(),
}),
TE.fromEither,
TE.map(({ input, logger }) => ({ input, logger, ...deps })),
TE.filterOrElse(hasLogger<R, I>, () => new Error("Unmeet dependencies")),
TE.chainW(h)
);
const azureFunctionTE =
<I, A, R>(
h: H.Handler<I, A, R>,
deps: Omit<R, "logger" | "input"> & { inputDecoder: t.Decoder<unknown, I> }
) =>
(input: unknown, ctx: InvocationContext) =>
pipe(
TE.right({
input,
logger: getLogger(ctx),
...deps,
}),
TE.filterOrElse(
isHandlerEnvironment<R, I>,
() => new Error("Unmet dependencies")
),
TE.chainW(h)
);

// Adapts an Handler to an Azure Function that can be triggered by
// queueTrigger, cosmosDBTrigger, eventsHubTrigger and other event-based binding
// Adapts an Handler to an Azure Function
export const azureFunction =
<I, A, R>(h: H.Handler<I, A, R>) =>
(
deps: Omit<R, "logger"> & { inputDecoder: t.Decoder<unknown, I> }
): azure.AzureFunction =>
(ctx) => {
const result = pipe(ctx, azureFunctionTE(h, deps), TE.toUnion)();
deps: Omit<R, "logger" | "input"> & { inputDecoder: t.Decoder<unknown, I> }
) =>
(input: unknown, ctx: InvocationContext) => {
const result = pipe(azureFunctionTE(h, deps)(input, ctx), TE.toUnion)();
// we have to throws here to ensure that "retry" mechanism of Azure
// can be executed
if (result instanceof Error) {
Expand Down Expand Up @@ -90,11 +90,12 @@ const HttpRequestFromAzure = AzureHttpRequestC.pipe(

const toAzureHttpResponse = (
res: H.HttpResponse<unknown, H.HttpStatusCode>
): azure.HttpResponse => ({
statusCode: res.statusCode,
body: res.body,
headers: res.headers,
});
): HttpResponse =>
new HttpResponse({
status: res.statusCode,
jsonBody: res.body,
headers: res.headers,
});

// Prevent HTTP triggered Azure Functions from crashing
// If an handler returns with an error (RTE.left),
Expand All @@ -118,14 +119,13 @@ export const httpAzureFunction =
<R>(
h: H.Handler<H.HttpRequest, H.HttpResponse<unknown, H.HttpStatusCode>, R>
) =>
(deps: Omit<R, "logger">): azure.AzureFunction =>
(ctx) =>
(deps: Omit<R, "logger" | "input">) =>
(input: unknown, ctx: InvocationContext) =>
pipe(
ctx,
azureFunctionTE(h, {
...deps,
inputDecoder: HttpRequestFromAzure,
}),
})(input, ctx),
TE.getOrElseW((e) =>
logErrorAndReturnHttpResponse(e)({
logger: getLogger(ctx),
Expand Down
Loading

0 comments on commit b229e4e

Please sign in to comment.