From 3f1f7fe6867cae52a439da882ddcaeb839fd2b2c Mon Sep 17 00:00:00 2001 From: gquadrati Date: Tue, 5 Mar 2024 17:36:54 +0100 Subject: [PATCH 01/12] [#IOPID-1450] enable noImplicitThis --- tsconfig.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 866a2386..f4d5182e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,13 @@ "rootDir": ".", "sourceMap": true, "strict": false, + "noImplicitAny": false, + "noImplicitThis": true, + "alwaysStrict": false, + "strictBindCallApply": false, + "strictNullChecks": false, + "strictFunctionTypes": false, + "strictPropertyInitialization": false, "resolveJsonModule": true, "skipLibCheck": true }, @@ -16,4 +23,4 @@ "**/__tests__/*", "Dangerfile.ts" ] -} +} \ No newline at end of file From 4624b8c34e807f7e50a8dacfacdaf43f5dbddfa1 Mon Sep 17 00:00:00 2001 From: gquadrati Date: Tue, 5 Mar 2024 17:37:33 +0100 Subject: [PATCH 02/12] [#IOPID-1450] enable alwaysStrict --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index f4d5182e..d987c9fb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ "strict": false, "noImplicitAny": false, "noImplicitThis": true, - "alwaysStrict": false, + "alwaysStrict": true, "strictBindCallApply": false, "strictNullChecks": false, "strictFunctionTypes": false, From edc3d8031c1cb3be71e2bf1822f93f42a7f63de4 Mon Sep 17 00:00:00 2001 From: gquadrati Date: Tue, 5 Mar 2024 17:38:11 +0100 Subject: [PATCH 03/12] [#IOPID-1450] enable strictBindCallApply --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index d987c9fb..e5e5bc88 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "noImplicitAny": false, "noImplicitThis": true, "alwaysStrict": true, - "strictBindCallApply": false, + "strictBindCallApply": true, "strictNullChecks": false, "strictFunctionTypes": false, "strictPropertyInitialization": false, From 0892e5566cf322b15d69fb89bf015cdeddf7a2c2 Mon Sep 17 00:00:00 2001 From: gquadrati Date: Tue, 5 Mar 2024 18:06:50 +0100 Subject: [PATCH 04/12] [#IOPID-1450] enable strictFunctionTypes --- DeleteUserDataActivity/backupAndDelete.ts | 32 +++++++----- DeleteUserDataActivity/utils.ts | 18 +++++++ ExtractUserDataActivity/handler.ts | 52 ++++++++++--------- GetFailedUserDataProcessingList/handler.ts | 3 +- GetUser/handler.ts | 9 ++-- GetUserSubscriptions/handler.ts | 8 +-- SetUserDataProcessingStatus/handler.ts | 4 +- SetUserSessionLockActivity/handler.ts | 10 +++- UpdateSubscriptionsFeedActivity/handler.ts | 2 +- UpdateUserGroups/handler.ts | 12 ++--- UserDataDownloadOrchestrator/cli.ts | 2 +- UserDataDownloadOrchestrator/handler.ts | 8 +-- .../handler.ts | 8 +-- tsconfig.json | 2 +- utils/random.ts | 2 +- 15 files changed, 101 insertions(+), 71 deletions(-) diff --git a/DeleteUserDataActivity/backupAndDelete.ts b/DeleteUserDataActivity/backupAndDelete.ts index 391c8617..ce17501b 100644 --- a/DeleteUserDataActivity/backupAndDelete.ts +++ b/DeleteUserDataActivity/backupAndDelete.ts @@ -2,12 +2,11 @@ import * as crypto from "crypto"; import { BlobService } from "azure-storage"; import { sequenceT } from "fp-ts/lib/Apply"; -import * as A from "fp-ts/lib/Array"; +import * as RA from "fp-ts/lib/ReadonlyArray"; import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; import * as TE from "fp-ts/lib/TaskEither"; -import { array, flatten, rights } from "fp-ts/lib/Array"; import { MessageContent } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageContent"; import { RetrievedMessage, @@ -40,6 +39,7 @@ import { QueryFailure } from "./types"; import { + isCosmosErrors, saveDataToBlob, toDocumentDeleteFailure, toQueryFailure @@ -67,18 +67,18 @@ const executeRecursiveBackupAndDelete = ( TE.mapLeft(toQueryFailure), TE.chainW(e => e.done - ? TE.of([]) + ? TE.of>([]) : e.value.some(E.isLeft) - ? TE.left( + ? TE.left>( toQueryFailure(new Error("Some elements are not typed correctly")) ) - : TE.of(rights(e.value)) + : TE.of>(RA.rights(e.value)) ), // executes backup&delete for this set of items TE.chainW(items => pipe( items, - A.map((item: T) => + RA.map((item: T) => pipe( sequenceT(TE.ApplicativeSeq)< DataFailure, @@ -104,8 +104,8 @@ const executeRecursiveBackupAndDelete = ( TE.map(([_, __, nextResults]) => [item, ...nextResults]) ) ), - A.sequence(TE.ApplicativePar), - TE.map(flatten) + RA.sequence(TE.ApplicativePar), + TE.map(RA.flatten) ) ) ); @@ -587,10 +587,16 @@ const backupAndDeleteAllMessagesData = ({ pipe( messageModel.findMessages(fiscalCode), TE.mapLeft(toQueryFailure), - TE.chain(iter => - TE.tryCatch(() => asyncIteratorToArray(iter), toQueryFailure) + TE.chainW(iter => + TE.tryCatch( + () => asyncIteratorToArray(iter), + err => + err instanceof Error || isCosmosErrors(err) + ? toQueryFailure(err) + : toQueryFailure(E.toError(err)) + ) ), - TE.map(flatten), + TE.map(RA.flatten), TE.chainW(results => results.some(E.isLeft) ? TE.left( @@ -598,8 +604,8 @@ const backupAndDeleteAllMessagesData = ({ new Error("Cannot decode some element due to decoding errors") ) ) - : array.sequence(TE.ApplicativeSeq)( - rights(results).map(message => { + : RA.sequence(TE.ApplicativeSeq)( + RA.rights(results).map(message => { // cast needed because findMessages has a wrong signature // eslint-disable-next-line @typescript-eslint/no-explicit-any const retrievedMessage = (message as any) as RetrievedMessageWithoutContent; diff --git a/DeleteUserDataActivity/utils.ts b/DeleteUserDataActivity/utils.ts index ab03edc1..53071a18 100644 --- a/DeleteUserDataActivity/utils.ts +++ b/DeleteUserDataActivity/utils.ts @@ -1,8 +1,11 @@ +import * as t from "io-ts"; import { Context } from "@azure/functions"; import { BlobService } from "azure-storage"; import * as TE from "fp-ts/lib/TaskEither"; import { CosmosErrors } from "@pagopa/io-functions-commons/dist/src/utils/cosmosdb_model"; import { pipe } from "fp-ts/lib/function"; + +import { enumType } from "@pagopa/ts-commons/lib/types"; import { ActivityResultFailure, BlobCreationFailure, @@ -11,6 +14,21 @@ import { QueryFailure } from "./types"; +// Cosmos Errors +export enum CosmosErrorsTypes { + "OSMOS_EMPTY_RESPONSE" = "OSMOS_EMPTY_RESPONSE", + "OSMOS_CONFLICT_RESPONSE" = "OSMOS_CONFLICT_RESPONSE", + "OSMOS_DECODING_ERROR" = "OSMOS_DECODING_ERROR", + "OSMOS_ERROR_RESPONSE" = "OSMOS_ERROR_RESPONSE" +} + +const CosmosErrorsTypesC = t.interface({ + kind: enumType(CosmosErrorsTypes, "kind") +}); + +export const isCosmosErrors = (error: unknown): error is CosmosErrors => + CosmosErrorsTypesC.is(error); + /** * To be used for exhaustive checks */ diff --git a/ExtractUserDataActivity/handler.ts b/ExtractUserDataActivity/handler.ts index e3ffd019..d1cd46de 100644 --- a/ExtractUserDataActivity/handler.ts +++ b/ExtractUserDataActivity/handler.ts @@ -8,9 +8,7 @@ import * as t from "io-ts"; import { DeferredPromise } from "@pagopa/ts-commons/lib/promises"; import { sequenceS, sequenceT } from "fp-ts/lib/Apply"; -import * as A from "fp-ts/lib/Array"; import * as ROA from "fp-ts/lib/ReadonlyArray"; -import { flatten, rights } from "fp-ts/lib/Array"; import { Context } from "@azure/functions"; @@ -221,8 +219,8 @@ export const getAllMessageContents = ( > => pipe( messages, - A.map(_ => _.id), - A.map(messageId => + ROA.map(_ => _.id), + ROA.map(messageId => pipe( messageModel.getContentFromBlob(messageContentBlobService, messageId), TE.chainW( @@ -241,7 +239,7 @@ export const getAllMessageContents = ( ) ) ), - A.sequence(TE.ApplicativePar) + ROA.sequence(TE.ApplicativePar) ); /** @@ -253,8 +251,8 @@ export const getAllMessagesStatuses = ( ): TE.TaskEither> => pipe( messages, - A.map(_ => _.id), - A.map(messageId => + ROA.map(_ => _.id), + ROA.map(messageId => pipe( messageStatusModel.findLastVersionByModelId([messageId]), TE.mapLeft(failure => @@ -273,7 +271,7 @@ export const getAllMessagesStatuses = ( ) ) ), - A.sequence(TE.ApplicativePar) + ROA.sequence(TE.ApplicativePar) ); /** @@ -290,8 +288,8 @@ export const findNotificationsForAllMessages = ( > => pipe( messages, - A.map(m => notificationModel.findNotificationForMessage(m.id)), - A.sequence(TE.ApplicativeSeq), + ROA.map(m => notificationModel.findNotificationForMessage(m.id)), + ROA.sequence(TE.ApplicativeSeq), TE.mapLeft(e => ActivityResultQueryFailure.encode({ kind: "QUERY_FAILURE", @@ -304,8 +302,8 @@ export const findNotificationsForAllMessages = ( // We just filter "none" elements TE.map( flow( - A.filter(O.isSome), - A.map(maybeNotification => maybeNotification.value) + ROA.filter(O.isSome), + ROA.map(maybeNotification => maybeNotification.value) ) ) ); @@ -321,14 +319,14 @@ export const findAllNotificationStatuses = ( notifications, // compose a query for every supported channel type - A.reduce([], (queries, { id: notificationId }) => [ + ROA.reduce([], (queries, { id: notificationId }) => [ ...queries, ...Object.values(NotificationChannelEnum).map(channel => [ notificationId, channel ]) ]), - A.map(([notificationId, channel]) => + ROA.map(([notificationId, channel]) => pipe( notificationStatusModel.findOneNotificationStatusByNotificationChannel( notificationId, @@ -344,13 +342,13 @@ export const findAllNotificationStatuses = ( ) ) ), - A.sequence(TE.ApplicativePar), + ROA.sequence(TE.ApplicativePar), // filter empty results (it might not exist a content for a pair notification/channel) TE.map( flow( - A.filter(O.isSome), - A.map(someNotificationStatus => someNotificationStatus.value) + ROA.filter(O.isSome), + ROA.map(someNotificationStatus => someNotificationStatus.value) ) ) ); @@ -380,18 +378,18 @@ export const queryAllUserData = ( // step 0: look for the profile getProfile(profileModel, fiscalCode), // step 1: get messages, which can be queried by only knowing the fiscal code - TE.chain(profile => + TE.chainW(profile => sequenceS(TE.ApplicativePar)({ // queries all messages for the user messages: pipe( messageModel.findMessages(fiscalCode), - TE.chain(iterator => + TE.chainW(iterator => TE.tryCatch( () => asyncIteratorToArray(iterator), toCosmosErrorResponse ) ), - TE.map(flatten), + TE.map(ROA.flatten), TE.mapLeft(_ => ActivityResultQueryFailure.encode({ kind: "QUERY_FAILURE", @@ -408,7 +406,11 @@ export const queryAllUserData = ( reason: "Some messages cannot be decoded" }) ) - : TE.of(rights(results)) + : TE.of( + ROA.rights(results) as ReadonlyArray< + RetrievedMessageWithoutContent + > + ) ) ), messagesView: pipe( @@ -428,7 +430,7 @@ export const queryAllUserData = ( TE.chain(iter => TE.tryCatch(() => asyncIteratorToArray(iter), toCosmosErrorResponse) ), - TE.map(flatten), + TE.map(ROA.flatten), TE.mapLeft(_ => ActivityResultQueryFailure.encode({ kind: "QUERY_FAILURE", @@ -445,7 +447,7 @@ export const queryAllUserData = ( reason: "Some messages cannot be decoded" }) ) - : TE.of(rights(results)) + : TE.of(ROA.rights(results)) ) ), profile: TE.of(profile), @@ -469,7 +471,7 @@ export const queryAllUserData = ( }) ), // step 2: queries notifications and message contents, which need message data to be queried first - TE.chain(({ profile, messages, messagesView, servicesPreferences }) => + TE.chainW(({ profile, messages, messagesView, servicesPreferences }) => sequenceS(TE.ApplicativePar)({ messageContents: getAllMessageContents( messageContentBlobService, @@ -488,7 +490,7 @@ export const queryAllUserData = ( }) ), // step 3: queries notifications statuses - TE.bind("notificationStatuses", ({ notifications }) => + TE.bindW("notificationStatuses", ({ notifications }) => findAllNotificationStatuses(notificationStatusModel, notifications) ), TE.map( diff --git a/GetFailedUserDataProcessingList/handler.ts b/GetFailedUserDataProcessingList/handler.ts index 147c4cb3..5cce9ef6 100644 --- a/GetFailedUserDataProcessingList/handler.ts +++ b/GetFailedUserDataProcessingList/handler.ts @@ -1,5 +1,4 @@ import * as express from "express"; -import * as t from "io-ts"; import * as E from "fp-ts/lib/Either"; import * as TE from "fp-ts/lib/TaskEither"; import { pipe } from "fp-ts/lib/function"; @@ -85,7 +84,7 @@ export const GetFailedUserDataProcessingList = ( const middlewaresWrap = withRequestMiddlewares( ContextMiddleware(), - RequiredParamMiddleware("choice", t.string) + RequiredParamMiddleware("choice", NonEmptyString) ); return wrapRequestHandler(middlewaresWrap(handler)); diff --git a/GetUser/handler.ts b/GetUser/handler.ts index 0158e706..0395e218 100644 --- a/GetUser/handler.ts +++ b/GetUser/handler.ts @@ -113,7 +113,7 @@ export function GetUserHandler( ) ) ), - TE.chain(taskResults => + TE.chainW(taskResults => pipe( taskResults.userList[0].name, NonEmptyString.decode, @@ -130,7 +130,7 @@ export function GetUserHandler( TE.fromEither ) ), - TE.chain(taskResults => + TE.chainW(taskResults => pipe( getUserGroups( taskResults.apiClient, @@ -153,7 +153,7 @@ export function GetUserHandler( return { errorOrGroups }; }), - TE.chain(taskResults => + TE.chainW(taskResults => pipe( getGraphRbacManagementClient(adb2cCredentials), TE.mapLeft(error => @@ -188,8 +188,7 @@ export function GetUserHandler( ) ) ), - - TE.chain(userInfo => + TE.chainW(userInfo => pipe( { // TODO: as both errorOrGroups and errorOrSubscriptions cannot be Left because of the previous checks, diff --git a/GetUserSubscriptions/handler.ts b/GetUserSubscriptions/handler.ts index 8b8ee362..60d75a14 100644 --- a/GetUserSubscriptions/handler.ts +++ b/GetUserSubscriptions/handler.ts @@ -150,7 +150,7 @@ export function GetUserSubscriptionsHandler( ) ) ), - TE.chain(taskResults => + TE.chainW(taskResults => pipe( taskResults.userList[0].name, NonEmptyString.decode, @@ -167,7 +167,7 @@ export function GetUserSubscriptionsHandler( TE.fromEither ) ), - TE.chain(taskResults => + TE.chainW(taskResults => pipe( sequenceT(TE.ApplicativePar)( getUserGroups( @@ -201,7 +201,7 @@ export function GetUserSubscriptionsHandler( )([...subscriptionContracts]); return { errorOrGroups, errorOrSubscriptions }; }), - TE.chain(taskResults => + TE.chainW(taskResults => pipe( getGraphRbacManagementClient(adb2cCredentials), TE.mapLeft(error => @@ -246,7 +246,7 @@ export function GetUserSubscriptionsHandler( ) ) ), - TE.chain(userInfo => + TE.chainW(userInfo => pipe( { // TODO: as both errorOrGroups and errorOrSubscriptions cannot be Left because of the previous checks, diff --git a/SetUserDataProcessingStatus/handler.ts b/SetUserDataProcessingStatus/handler.ts index d05ec82b..bdf6d420 100644 --- a/SetUserDataProcessingStatus/handler.ts +++ b/SetUserDataProcessingStatus/handler.ts @@ -91,10 +91,10 @@ export const setUserDataProcessingStatusHandler = ( return pipe( findLastVersionByModelIdTask, - TE.chain( + TE.chainW( flow( O.map(updateStatusTask), - O.getOrElse(() => + O.getOrElseW(() => TE.left( ResponseErrorNotFound( "Not Found", diff --git a/SetUserSessionLockActivity/handler.ts b/SetUserSessionLockActivity/handler.ts index 63741700..514ef09e 100644 --- a/SetUserSessionLockActivity/handler.ts +++ b/SetUserSessionLockActivity/handler.ts @@ -132,7 +132,10 @@ const callSessionApi = ( action, value ); - return TE.left( + return TE.left< + ApiCallFailure | BadApiRequestFailure, + SuccessResponse + >( BadApiRequestFailure.encode({ kind: "BAD_API_REQUEST_FAILURE", reason: `Session Api called badly, action: ${action} code: ${status}` @@ -144,7 +147,10 @@ const callSessionApi = ( action, value ); - return TE.left( + return TE.left< + ApiCallFailure | BadApiRequestFailure, + SuccessResponse + >( ApiCallFailure.encode({ kind: "API_CALL_FAILURE", reason: `Session Api unexpected error, action: ${action}` diff --git a/UpdateSubscriptionsFeedActivity/handler.ts b/UpdateSubscriptionsFeedActivity/handler.ts index c2c6db9d..a9194802 100644 --- a/UpdateSubscriptionsFeedActivity/handler.ts +++ b/UpdateSubscriptionsFeedActivity/handler.ts @@ -135,7 +135,7 @@ export const updateSubscriptionFeed = async ( ]; }, [] as ReadonlyArray) ), - O.getOrElse(() => []) + O.getOrElseW(() => []) ); const allowInsertIfDeleted = decodedInput.subscriptionKind !== "SERVICE"; diff --git a/UpdateUserGroups/handler.ts b/UpdateUserGroups/handler.ts index debf74fc..bde03509 100644 --- a/UpdateUserGroups/handler.ts +++ b/UpdateUserGroups/handler.ts @@ -197,7 +197,7 @@ export function UpdateUserGroupHandler( apimClient: taskResults.apimClient, userName: taskResults.userList[0].name })), - TE.chain(taskResults => + TE.chainW(taskResults => pipe( taskResults.userName, NonEmptyString.decode, @@ -217,7 +217,7 @@ export function UpdateUserGroupHandler( })) ) ), - TE.chain(taskResults => + TE.chainW(taskResults => pipe( getUserGroups( taskResults.apimClient, @@ -239,7 +239,7 @@ export function UpdateUserGroupHandler( })) ) ), - TE.chain(taskResults => + TE.chainW(taskResults => pipe( getGroups( taskResults.apimClient, @@ -272,7 +272,7 @@ export function UpdateUserGroupHandler( TE.map(() => taskResults) ) ), - TE.chain(taskResults => { + TE.chainW(taskResults => { const groupsClusterization = clusterizeGroups( taskResults.existingGroups, taskResults.currentUserGroups, @@ -326,7 +326,7 @@ export function UpdateUserGroupHandler( })) ); }), - TE.chain(taskResults => + TE.chainW(taskResults => pipe( getUserGroups( taskResults.apimClient, @@ -342,7 +342,7 @@ export function UpdateUserGroupHandler( ) ) ), - TE.chain(groupContracts => + TE.chainW(groupContracts => pipe( [...groupContracts], A.traverse(E.Applicative)(groupContractToApiGroup), diff --git a/UserDataDownloadOrchestrator/cli.ts b/UserDataDownloadOrchestrator/cli.ts index 7cb0fc69..9d2ceca6 100644 --- a/UserDataDownloadOrchestrator/cli.ts +++ b/UserDataDownloadOrchestrator/cli.ts @@ -54,7 +54,7 @@ async function run(): Promise { const fiscalCode = pipe( process.argv[2], FiscalCode.decode, - E.getOrElse(reason => { + E.getOrElseW(reason => { throw new Error(`Invalid input: ${readableReport(reason)}`); }) ); diff --git a/UserDataDownloadOrchestrator/handler.ts b/UserDataDownloadOrchestrator/handler.ts index d173ac11..15cd6177 100644 --- a/UserDataDownloadOrchestrator/handler.ts +++ b/UserDataDownloadOrchestrator/handler.ts @@ -122,7 +122,7 @@ export const handler = function*( } ), SetUserDataProcessingStatusActivityResultSuccess.decode, - E.getOrElse(err => { + E.getOrElseW(err => { throw toActivityFailure(err, "SetUserDataProcessingStatusActivity", { status: UserDataProcessingStatusEnum.WIP }); @@ -151,7 +151,7 @@ export const handler = function*( } ), SendUserDataDownloadMessageActivityResultSuccess.decode, - E.getOrElse(err => { + E.getOrElseW(err => { throw toActivityFailure(err, "SendUserDataDownloadMessageActivity"); }) ); @@ -166,7 +166,7 @@ export const handler = function*( } ), SetUserDataProcessingStatusActivityResultSuccess.decode, - E.getOrElse(err => { + E.getOrElseW(err => { throw toActivityFailure(err, "SetUserDataProcessingStatusActivity", { status: UserDataProcessingStatusEnum.CLOSED }); @@ -213,7 +213,7 @@ export const handler = function*( } ), SetUserDataProcessingStatusActivityResultSuccess.decode, - E.getOrElse(err => { + E.getOrElseW(err => { trackUserDataDownloadException( "unhandled_failed_status", new Error(readableReport(err)), diff --git a/UserDataProcessingRecoveryOrchestrator/handler.ts b/UserDataProcessingRecoveryOrchestrator/handler.ts index 7beda9f4..ef284ab5 100644 --- a/UserDataProcessingRecoveryOrchestrator/handler.ts +++ b/UserDataProcessingRecoveryOrchestrator/handler.ts @@ -116,7 +116,7 @@ function* getLastStatus( return pipe( result, CheckLastStatusActivityResultSuccess.decode, - E.getOrElse(e => { + E.getOrElseW(e => { context.log.error( `${logPrefix}|ERROR|UserDataProcessingCheckLastStatusActivity fail|${readableReport( e @@ -159,7 +159,7 @@ function* searchForFailureReason( return pipe( result, FindFailureReasonActivityResultSuccess.decode, - E.getOrElse(e => { + E.getOrElseW(e => { context.log.error( `${logPrefix}|ERROR|UserDataProcessingFindFailureReasonActivity fail|${readableReport( e @@ -194,7 +194,7 @@ function* saveNewFailedRecordWithReason( return pipe( result, SetUserDataProcessingStatusActivityResultSuccess.decode, - E.getOrElse(e => { + E.getOrElseW(e => { context.log.error( `${logPrefix}|ERROR|SetUserDataProcessingStatusActivity fail|${readableReport( e @@ -287,7 +287,7 @@ export const handler = function*( return pipe( error, OrchestratorFailure.decode, - E.getOrElse(_ => + E.getOrElseW(_ => UnhandledFailure.encode({ kind: "UNHANDLED", reason: printableError(error) diff --git a/tsconfig.json b/tsconfig.json index e5e5bc88..b52ddf67 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "alwaysStrict": true, "strictBindCallApply": true, "strictNullChecks": false, - "strictFunctionTypes": false, + "strictFunctionTypes": true, "strictPropertyInitialization": false, "resolveJsonModule": true, "skipLibCheck": true diff --git a/utils/random.ts b/utils/random.ts index 3b9c769f..116d31a4 100644 --- a/utils/random.ts +++ b/utils/random.ts @@ -26,7 +26,7 @@ export const generateStrongPassword = (): StrongPassword => length: 18 }), StrongPassword.decode, - E.getOrElse(err => { + E.getOrElseW(err => { throw new Error( `Failed generating strong password - ${readableReport(err)}` ); From e2d281e3e4cf67c4d39a3db30d4e59ae9bf49049 Mon Sep 17 00:00:00 2001 From: gquadrati Date: Fri, 8 Mar 2024 19:15:25 +0100 Subject: [PATCH 05/12] [#IOPID-1450] enable noImplicitAny and useUnknownInCatchVariables --- .../__tests__/handler.test.ts | 4 +- .../__tests__/handler.test.ts | 68 ++++++++++++------- .../__tests__/handler.test.ts | 24 +++---- GetSubscriptionKeys/__tests__/handler.test.ts | 6 +- GetUser/__tests__/handler.test.ts | 11 +-- GetUser/handler.ts | 6 +- .../__tests__/handler.test.ts | 10 +-- GetUserSubscriptions/handler.ts | 6 +- GetUsers/__tests__/handler.test.ts | 21 +++--- .../__tests__/handler.test.ts | 4 +- .../__test__/handler.test.ts | 8 ++- .../__tests__/handler.test.ts | 4 +- UpdateService/handler.ts | 6 +- .../__test__/handler.test.ts | 2 +- UploadServiceLogo/__test__/handler.test.ts | 2 +- package.json | 4 +- tsconfig.json | 3 +- utils/__tests__/apim.test.ts | 2 +- utils/__tests__/retry.test.ts | 2 +- utils/conversions.ts | 4 +- yarn.lock | 12 ++++ 21 files changed, 134 insertions(+), 75 deletions(-) diff --git a/GetFailedUserDataProcessing/__tests__/handler.test.ts b/GetFailedUserDataProcessing/__tests__/handler.test.ts index 87a8da17..e2473710 100644 --- a/GetFailedUserDataProcessing/__tests__/handler.test.ts +++ b/GetFailedUserDataProcessing/__tests__/handler.test.ts @@ -13,7 +13,7 @@ const findEntry = ( PartitionKey: UserDataProcessingChoice; RowKey: FiscalCode; }> -) => (choice, fiscalCode) => +) => (choice: UserDataProcessingChoice, fiscalCode: FiscalCode) => entries.length > 0 ? entries .filter(e => e.PartitionKey === choice && e.RowKey === fiscalCode) @@ -56,7 +56,7 @@ const storageTableMock = "FailedUserDataProcessing" as NonEmptyString; const fiscalCode1 = "UEEFON48A55Y758X" as FiscalCode; const fiscalCode2 = "VEEGON48A55Y758Z" as FiscalCode; -const noFailedRequests = []; +const noFailedRequests: typeof failedRequests = []; const failedRequests = [ { diff --git a/GetFailedUserDataProcessingList/__tests__/handler.test.ts b/GetFailedUserDataProcessingList/__tests__/handler.test.ts index b5c0b304..e452dd10 100644 --- a/GetFailedUserDataProcessingList/__tests__/handler.test.ts +++ b/GetFailedUserDataProcessingList/__tests__/handler.test.ts @@ -2,13 +2,16 @@ import { FiscalCode, NonEmptyString } from "@pagopa/ts-commons/lib/strings"; import { TableService } from "azure-storage"; -import { UserDataProcessingChoice, UserDataProcessingChoiceEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/UserDataProcessingChoice"; +import { + UserDataProcessingChoice, + UserDataProcessingChoiceEnum +} from "@pagopa/io-functions-commons/dist/generated/definitions/UserDataProcessingChoice"; import { GetFailedUserDataProcessingListHandler } from "../handler"; const queryEntitiesFailedUserDataProcessingMock = ( entries: ReadonlyArray<{ - PartitionKey: UserDataProcessingChoice, - RowKey: FiscalCode + PartitionKey: UserDataProcessingChoice; + RowKey: FiscalCode; }> ) => jest.fn((_, tableQuery, ___, cb) => { @@ -18,10 +21,14 @@ const queryEntitiesFailedUserDataProcessingMock = ( entries: entries.length > 0 ? entries - .filter(e => tableQuery._where[0] == "PartitionKey eq '" + e.PartitionKey + "'") - .map(e => ({ - RowKey: { _: e.RowKey } - })) + .filter( + e => + tableQuery._where[0] == + "PartitionKey eq '" + e.PartitionKey + "'" + ) + .map(e => ({ + RowKey: { _: e.RowKey } + })) : [] }, { isSuccessful: true } @@ -32,17 +39,21 @@ const storageTableMock = "FailedUserDataProcessing" as NonEmptyString; const fiscalCode = "UEEFON48A55Y758X" as FiscalCode; -const noFailedRequests = []; +const noFailedRequests: typeof oneFailedDeleteRequest = []; -const oneFailedDeleteRequest = [{ - PartitionKey: UserDataProcessingChoiceEnum.DELETE, - RowKey: fiscalCode -}]; +const oneFailedDeleteRequest = [ + { + PartitionKey: UserDataProcessingChoiceEnum.DELETE, + RowKey: fiscalCode + } +]; -const oneFailedDownloadRequest = [{ - PartitionKey: UserDataProcessingChoiceEnum.DOWNLOAD, - RowKey: fiscalCode -}]; +const oneFailedDownloadRequest = [ + { + PartitionKey: UserDataProcessingChoiceEnum.DOWNLOAD, + RowKey: fiscalCode + } +]; const twoFailedDifferentRequests = [ { @@ -60,7 +71,6 @@ beforeEach(() => { }); describe("GetFailedUserDataProcessingListHandler", () => { - it("should return an empty json if no failed user data processing request is present", async () => { const tableServiceMock = ({ queryEntities: queryEntitiesFailedUserDataProcessingMock(noFailedRequests) @@ -86,7 +96,9 @@ describe("GetFailedUserDataProcessingListHandler", () => { it("should return a json with a fiscalcode if failed user data delete request has been found", async () => { const tableServiceMock = ({ - queryEntities: queryEntitiesFailedUserDataProcessingMock(oneFailedDeleteRequest) + queryEntities: queryEntitiesFailedUserDataProcessingMock( + oneFailedDeleteRequest + ) } as any) as TableService; const getFailedUserDataProcessingListHandler = GetFailedUserDataProcessingListHandler( @@ -109,7 +121,9 @@ describe("GetFailedUserDataProcessingListHandler", () => { it("should return an empty json with a fiscalcode because no failed user data delete request has been found", async () => { const tableServiceMock = ({ - queryEntities: queryEntitiesFailedUserDataProcessingMock(oneFailedDownloadRequest) + queryEntities: queryEntitiesFailedUserDataProcessingMock( + oneFailedDownloadRequest + ) } as any) as TableService; const getFailedUserDataProcessingListHandler = GetFailedUserDataProcessingListHandler( @@ -132,7 +146,9 @@ describe("GetFailedUserDataProcessingListHandler", () => { it("should return a json with a fiscalcode if failed user data download request has been found", async () => { const tableServiceMock = ({ - queryEntities: queryEntitiesFailedUserDataProcessingMock(oneFailedDownloadRequest) + queryEntities: queryEntitiesFailedUserDataProcessingMock( + oneFailedDownloadRequest + ) } as any) as TableService; const getFailedUserDataProcessingListHandler = GetFailedUserDataProcessingListHandler( @@ -155,7 +171,9 @@ describe("GetFailedUserDataProcessingListHandler", () => { it("should return an empty json with a fiscalcode because no failed user data download request has been found", async () => { const tableServiceMock = ({ - queryEntities: queryEntitiesFailedUserDataProcessingMock(oneFailedDeleteRequest) + queryEntities: queryEntitiesFailedUserDataProcessingMock( + oneFailedDeleteRequest + ) } as any) as TableService; const getFailedUserDataProcessingListHandler = GetFailedUserDataProcessingListHandler( @@ -178,7 +196,9 @@ describe("GetFailedUserDataProcessingListHandler", () => { it("should return a json with only a fiscalcode because there is only one failed user data delete request", async () => { const tableServiceMock = ({ - queryEntities: queryEntitiesFailedUserDataProcessingMock(twoFailedDifferentRequests) + queryEntities: queryEntitiesFailedUserDataProcessingMock( + twoFailedDifferentRequests + ) } as any) as TableService; const getFailedUserDataProcessingListHandler = GetFailedUserDataProcessingListHandler( @@ -203,7 +223,9 @@ describe("GetFailedUserDataProcessingListHandler", () => { it("should return a json with only a fiscalcode because there is only one failed user data download request", async () => { const tableServiceMock = ({ - queryEntities: queryEntitiesFailedUserDataProcessingMock(twoFailedDifferentRequests) + queryEntities: queryEntitiesFailedUserDataProcessingMock( + twoFailedDifferentRequests + ) } as any) as TableService; const getFailedUserDataProcessingListHandler = GetFailedUserDataProcessingListHandler( diff --git a/GetImpersonateService/__tests__/handler.test.ts b/GetImpersonateService/__tests__/handler.test.ts index 271166ff..4fda0a48 100644 --- a/GetImpersonateService/__tests__/handler.test.ts +++ b/GetImpersonateService/__tests__/handler.test.ts @@ -1,18 +1,13 @@ -import { ApiManagementClient } from "@azure/arm-apimanagement"; -import { GroupContract } from "@azure/arm-apimanagement/esm/models"; +import { + GroupCollection, + GroupContract +} from "@azure/arm-apimanagement/esm/models"; import * as TE from "fp-ts/lib/TaskEither"; import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; import * as ApimUtils from "../../utils/apim"; -import { - ApimRestError, - IAzureApimConfig, - IServicePrincipalCreds -} from "../../utils/apim"; +import { IAzureApimConfig, IServicePrincipalCreds } from "../../utils/apim"; import { GetImpersonateServiceHandler } from "../handler"; import { RestError } from "@azure/ms-rest-js"; -import { pipe } from "fp-ts/lib/function"; -import { mapLeft } from "fp-ts/lib/Either"; -import { errorsToReadableMessages } from "@pagopa/ts-commons/lib/reporters"; jest.mock("@azure/arm-apimanagement"); jest.mock("@azure/graph"); @@ -40,7 +35,10 @@ const mockedSubscription = { ownerId: "/users/userId" }; -const mockedSubscriptionWithoutOwner = { +const mockedSubscriptionWithoutOwner: { + displayName: string; + ownerId: string | undefined; +} = { displayName: "without-woner", ownerId: undefined }; @@ -50,7 +48,7 @@ const anApimGroupContract: GroupContract = { displayName: "groupName" }; -const someValidGroups: ReadonlyArray = [ +const someValidGroups: Array = [ { ...anApimGroupContract, id: "group #1" }, { ...anApimGroupContract, id: "group #2" } ]; @@ -65,7 +63,7 @@ const mockedUserWithoutEmail = { }; const mockUserGroupList = jest.fn().mockImplementation(() => { - const apimResponse = someValidGroups; + const apimResponse: GroupCollection = someValidGroups; // eslint-disable-next-line functional/immutable-data apimResponse["nextLink"] = "next-page"; return Promise.resolve(apimResponse); diff --git a/GetSubscriptionKeys/__tests__/handler.test.ts b/GetSubscriptionKeys/__tests__/handler.test.ts index f4979fc6..60eb094b 100644 --- a/GetSubscriptionKeys/__tests__/handler.test.ts +++ b/GetSubscriptionKeys/__tests__/handler.test.ts @@ -1,8 +1,8 @@ // eslint-disable @typescript-eslint/no-explicit-any -jest.mock('@azure/ms-rest-nodeauth', () => ({ +jest.mock("@azure/ms-rest-nodeauth", () => ({ __esModule: true, - ...jest.requireActual('@azure/ms-rest-nodeauth') + ...jest.requireActual("@azure/ms-rest-nodeauth") })); import { ApiManagementClient } from "@azure/arm-apimanagement"; @@ -32,7 +32,7 @@ const mockedSubscription = { }; mockApiManagementClient.mockImplementation(() => ({ subscription: { - get: (_, __, subscriptionId) => { + get: (_: string, __: string, subscriptionId: string) => { if (subscriptionId === aValidSubscriptionId) { return Promise.resolve(mockedSubscription); } diff --git a/GetUser/__tests__/handler.test.ts b/GetUser/__tests__/handler.test.ts index 6d24ed93..645ffaca 100644 --- a/GetUser/__tests__/handler.test.ts +++ b/GetUser/__tests__/handler.test.ts @@ -1,6 +1,9 @@ // eslint-disable @typescript-eslint/no-explicit-any import { ApiManagementClient } from "@azure/arm-apimanagement"; -import { GroupContract } from "@azure/arm-apimanagement/esm/models"; +import { + GroupCollection, + GroupContract +} from "@azure/arm-apimanagement/esm/models"; import { GraphRbacManagementClient } from "@azure/graph"; import * as E from "fp-ts/lib/Either"; import * as TE from "fp-ts/lib/TaskEither"; @@ -241,11 +244,11 @@ describe("GetUser", () => { type: undefined }; - const someValidGroups: ReadonlyArray = [ + const someValidGroups: Array = [ { ...anApimGroupContract, id: "group #1" }, { ...anApimGroupContract, id: "group #2" } ]; - const someMoreValidGroups: ReadonlyArray = [ + const someMoreValidGroups: Array = [ { ...anApimGroupContract, id: "group #3" }, { ...anApimGroupContract, id: "group #4" } ]; @@ -254,7 +257,7 @@ describe("GetUser", () => { Promise.resolve([{ name: fakeUserName }]) ); mockUserGroupList.mockImplementation(() => { - const apimResponse = someValidGroups; + const apimResponse: GroupCollection = someValidGroups; // eslint-disable-next-line functional/immutable-data apimResponse["nextLink"] = "next-page"; return Promise.resolve(apimResponse); diff --git a/GetUser/handler.ts b/GetUser/handler.ts index 0395e218..fe7a59b2 100644 --- a/GetUser/handler.ts +++ b/GetUser/handler.ts @@ -174,7 +174,11 @@ export function GetUserHandler( ), TE.map(([adb2User]) => ({ ...taskResults, - token_name: adb2User[`${adb2cTokenAttributeName}`] + // Note: This workaround is necessary to enable strict typing. + // `adb2cTokenAttributeName` should be typed with the list of attributes allowed in this scenario, + // ensuring compatibility and adherence to specified attribute constraints. + token_name: + adb2User[`${adb2cTokenAttributeName}` as keyof typeof adb2User] })) ) ), diff --git a/GetUserSubscriptions/__tests__/handler.test.ts b/GetUserSubscriptions/__tests__/handler.test.ts index dfa5bf4d..0cde839f 100644 --- a/GetUserSubscriptions/__tests__/handler.test.ts +++ b/GetUserSubscriptions/__tests__/handler.test.ts @@ -1,7 +1,9 @@ // eslint-disable @typescript-eslint/no-explicit-any import { ApiManagementClient } from "@azure/arm-apimanagement"; import { + GroupCollection, GroupContract, + SubscriptionCollection, SubscriptionContract } from "@azure/arm-apimanagement/esm/models"; import { GraphRbacManagementClient } from "@azure/graph"; @@ -295,7 +297,7 @@ describe("GetUser", () => { type: undefined }; - const someValidGroups: ReadonlyArray = [ + const someValidGroups: Array = [ { ...anApimGroupContract, id: "group #1" }, { ...anApimGroupContract, id: "group #2" } ]; @@ -303,7 +305,7 @@ describe("GetUser", () => { { ...anApimGroupContract, id: "group #3" }, { ...anApimGroupContract, id: "group #4" } ]; - const someValidSubscriptions: ReadonlyArray = [ + const someValidSubscriptions: Array = [ { ...anApimSubscriptionContract, primaryKey: "primaryKey#1", @@ -331,7 +333,7 @@ describe("GetUser", () => { Promise.resolve([{ name: fakeUserName }]) ); mockUserGroupList.mockImplementation(() => { - const apimResponse = someValidGroups; + const apimResponse: GroupCollection = someValidGroups; // eslint-disable-next-line functional/immutable-data apimResponse["nextLink"] = "next-page"; return Promise.resolve(apimResponse); @@ -340,7 +342,7 @@ describe("GetUser", () => { Promise.resolve(someMoreValidGroups) ); mockUserSubscriptionList.mockImplementation(() => { - const apimResponse = someValidSubscriptions; + const apimResponse: SubscriptionCollection = someValidSubscriptions; // eslint-disable-next-line functional/immutable-data apimResponse["nextLink"] = "next-page"; return Promise.resolve(apimResponse); diff --git a/GetUserSubscriptions/handler.ts b/GetUserSubscriptions/handler.ts index 60d75a14..a31ad176 100644 --- a/GetUserSubscriptions/handler.ts +++ b/GetUserSubscriptions/handler.ts @@ -222,7 +222,11 @@ export function GetUserSubscriptionsHandler( ), TE.map(([adb2User]) => ({ ...taskResults, - token_name: adb2User[`${adb2cTokenAttributeName}`] + // Note: This workaround is necessary to enable strict typing. + // `adb2cTokenAttributeName` should be typed with the list of attributes allowed in this scenario, + // ensuring compatibility and adherence to specified attribute constraints. + token_name: + adb2User[`${adb2cTokenAttributeName}` as keyof typeof adb2User] })) ) ), diff --git a/GetUsers/__tests__/handler.test.ts b/GetUsers/__tests__/handler.test.ts index 811c999b..0a091518 100644 --- a/GetUsers/__tests__/handler.test.ts +++ b/GetUsers/__tests__/handler.test.ts @@ -1,8 +1,8 @@ // eslint-disable @typescript-eslint/no-explicit-any -jest.mock('@azure/ms-rest-nodeauth', () => ({ +jest.mock("@azure/ms-rest-nodeauth", () => ({ __esModule: true, - ...jest.requireActual('@azure/ms-rest-nodeauth') + ...jest.requireActual("@azure/ms-rest-nodeauth") })); import { ApiManagementClient } from "@azure/arm-apimanagement"; @@ -82,7 +82,7 @@ const mockedInvalidUserContract = { ], lastName: "Rossi", name: "user-name-1", - note: null, + note: undefined as string | undefined, registrationDate: new Date(), state: "active", type: "type" @@ -111,7 +111,7 @@ describe("GetUsers", () => { it("should return an internal error response if the API management client returns an error", async () => { mockApiManagementClient.mockImplementation(() => ({ user: { - listByService: (_, __, ___) => + listByService: (_: string, __: string, ___: string) => Promise.reject(new Error("API management client error")) } })); @@ -132,7 +132,7 @@ describe("GetUsers", () => { it("should return an internal error response if the API management client returns invalid data", async () => { // eslint-disable-next-line functional/prefer-readonly-type - const mockedApimUsersList: any[] = [ + const mockedApimUsersList: any[] & { nextLink?: string } = [ mockedUserContract1, mockedUserContract2, mockedInvalidUserContract @@ -141,7 +141,8 @@ describe("GetUsers", () => { mockedApimUsersList["nextLink"] = "next-link"; mockApiManagementClient.mockImplementation(() => ({ user: { - listByService: (_, __, ___) => Promise.resolve(mockedApimUsersList) + listByService: (_: string, __: string, ___: string) => + Promise.resolve(mockedApimUsersList) } })); const getUsersHandler = GetUsersHandler( @@ -196,8 +197,10 @@ describe("GetUsers", () => { mockApiManagementClient.mockImplementation(() => ({ user: { - listByService: (_, __, options: { skip: number }) => { - const list: ReadonlyArray = mockedApimUsersList.slice( + listByService: (_: string, __: string, options: { skip: number }) => { + const list: ReadonlyArray & { + nextLink?: string; + } = mockedApimUsersList.slice( options.skip, options.skip + resultsPerPage ); @@ -223,7 +226,7 @@ describe("GetUsers", () => { undefined ); expect(msRestNodeAuth.loginWithServicePrincipalSecret).toBeCalled(); - + expect(responseWithNext).toEqual({ apply: expect.any(Function), kind: "IResponseSuccessJson", diff --git a/IsFailedUserDataProcessingActivity/__tests__/handler.test.ts b/IsFailedUserDataProcessingActivity/__tests__/handler.test.ts index 8a322183..d9e631f3 100644 --- a/IsFailedUserDataProcessingActivity/__tests__/handler.test.ts +++ b/IsFailedUserDataProcessingActivity/__tests__/handler.test.ts @@ -16,7 +16,7 @@ const findEntry = ( PartitionKey: UserDataProcessingChoice; RowKey: FiscalCode; }> -) => (choice, fiscalCode) => +) => (choice: UserDataProcessingChoice, fiscalCode: FiscalCode) => entries.length > 0 ? entries .filter(e => e.PartitionKey === choice && e.RowKey === fiscalCode) @@ -59,7 +59,7 @@ const storageTableMock = "FailedUserDataProcessing" as NonEmptyString; const fiscalCode1 = "UEEFON48A55Y758X" as FiscalCode; const fiscalCode2 = "VEEGON48A55Y758Z" as FiscalCode; -const noFailedRequests = []; +const noFailedRequests: typeof failedRequests = []; const failedRequests = [ { diff --git a/RegenerateSubscriptionKeys/__test__/handler.test.ts b/RegenerateSubscriptionKeys/__test__/handler.test.ts index 020fcc0f..7f2eea4c 100644 --- a/RegenerateSubscriptionKeys/__test__/handler.test.ts +++ b/RegenerateSubscriptionKeys/__test__/handler.test.ts @@ -35,7 +35,11 @@ const mockedSubscription = { }; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -const regenerateKeyImplementation = (_, __, subscriptionId) => { +const regenerateKeyImplementation = ( + _: string, + __: string, + subscriptionId: string +) => { if (subscriptionId === aValidSubscriptionId) { return Promise.resolve(); } @@ -56,7 +60,7 @@ mockRegenerateSecondaryKey.mockImplementation(regenerateKeyImplementation); mockApiManagementClient.mockImplementation(() => ({ subscription: { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - get: (_, __, subscriptionId) => { + get: (_: string, __: string, subscriptionId: string) => { if (subscriptionId === aValidSubscriptionId) { return Promise.resolve(mockedSubscription); } diff --git a/SendUserDataDeleteEmailActivity/__tests__/handler.test.ts b/SendUserDataDeleteEmailActivity/__tests__/handler.test.ts index e69da0e4..8a56bc99 100644 --- a/SendUserDataDeleteEmailActivity/__tests__/handler.test.ts +++ b/SendUserDataDeleteEmailActivity/__tests__/handler.test.ts @@ -82,7 +82,9 @@ describe("SendUserDataDeleteEmailActivity", () => { try { await SendUserDataDeleteEmailActivityHandler(mockContext, input); } catch (e) { - expect(e.message).toBe("Error while sending email: " + errorMessage); + expect(e).toMatchObject({ + message: "Error while sending email: " + errorMessage + }); } }); }); diff --git a/UpdateService/handler.ts b/UpdateService/handler.ts index 125c4bd8..d5843610 100644 --- a/UpdateService/handler.ts +++ b/UpdateService/handler.ts @@ -93,8 +93,10 @@ export function UpdateServiceHandler( - when a service is created/updated using the old APIs a CosmosDBTrigger Azure Function will intercept it an write it into the new container but only if the "cmsTag" field is not present, so when a service is updated using the old APIs the "cmsTag" field needs to be removed. */ - // eslint-disable-next-line fp/no-delete, functional/immutable-data, @typescript-eslint/dot-notation - delete existingService["cmsTag"]; + if ("cmsTag" in existingService) { + // eslint-disable-next-line fp/no-delete, functional/immutable-data, @typescript-eslint/dot-notation + delete existingService["cmsTag"]; + } const errorOrUpdatedService = await serviceModel.update({ ...existingService, diff --git a/UploadOrganizationLogo/__test__/handler.test.ts b/UploadOrganizationLogo/__test__/handler.test.ts index 1a8e83ad..ba791f3d 100644 --- a/UploadOrganizationLogo/__test__/handler.test.ts +++ b/UploadOrganizationLogo/__test__/handler.test.ts @@ -15,7 +15,7 @@ describe("UploadOrganizationLogoHandler", () => { } as Logo; const mockedContext = { bindings: { - logo: undefined + logo: undefined as string } }; diff --git a/UploadServiceLogo/__test__/handler.test.ts b/UploadServiceLogo/__test__/handler.test.ts index 582d3dd2..dd841dce 100644 --- a/UploadServiceLogo/__test__/handler.test.ts +++ b/UploadServiceLogo/__test__/handler.test.ts @@ -68,7 +68,7 @@ describe("UpdateServiceLogoHandler", () => { } as Logo; const mockedContext = { bindings: { - logo: undefined + logo: undefined as string } }; diff --git a/package.json b/package.json index 99c38d4c..f17857ce 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "@pagopa/openapi-codegen-ts": "^10.0.5", "@types/documentdb": "^1.10.8", "@types/express": "^4.17.12", + "@types/html-to-text": "^5.1.1", "@types/jest": "^24.9.1", + "@types/nodemailer": "^4.6.8", "@types/upng-js": "^2.1.1", "auto-changelog": "^2.3.0", "azure-functions-core-tools": "^4.0.5455", @@ -64,8 +66,8 @@ "@pagopa/handler-kit": "^1.1.0", "@pagopa/handler-kit-azure-func": "^1.2.1", "@pagopa/io-backend-session-sdk": "x", - "@pagopa/logger": "^1.0.1", "@pagopa/io-functions-commons": "^28.15.0", + "@pagopa/logger": "^1.0.1", "@pagopa/ts-commons": "^12.5.0", "@types/archiver": "^3.1.1", "@types/randomstring": "^1.1.6", diff --git a/tsconfig.json b/tsconfig.json index b52ddf67..a6f788b7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,13 +6,14 @@ "rootDir": ".", "sourceMap": true, "strict": false, - "noImplicitAny": false, + "noImplicitAny": true, "noImplicitThis": true, "alwaysStrict": true, "strictBindCallApply": true, "strictNullChecks": false, "strictFunctionTypes": true, "strictPropertyInitialization": false, + "useUnknownInCatchVariables": true, "resolveJsonModule": true, "skipLibCheck": true }, diff --git a/utils/__tests__/apim.test.ts b/utils/__tests__/apim.test.ts index d3524ca6..b8362c6c 100644 --- a/utils/__tests__/apim.test.ts +++ b/utils/__tests__/apim.test.ts @@ -3,7 +3,7 @@ import { isErrorStatusCode, parseOwnerIdFullPath } from "../apim"; class ErrorWithCode extends Error { public statusCode: number; - constructor(statusCode, ...args) { + constructor(statusCode: number, ...args: Parameters) { super(...args); this.statusCode = statusCode; } diff --git a/utils/__tests__/retry.test.ts b/utils/__tests__/retry.test.ts index a236554f..e854108f 100644 --- a/utils/__tests__/retry.test.ts +++ b/utils/__tests__/retry.test.ts @@ -16,7 +16,7 @@ const anOperationGoodAtAttemptNth = (n: number) => { }); }; -const sleep = ms => new Promise(done => setTimeout(done, ms)); +const sleep = (ms: number) => new Promise(done => setTimeout(done, ms)); beforeEach(() => { jest.clearAllMocks(); diff --git a/utils/conversions.ts b/utils/conversions.ts index 12498e89..1eca5e2e 100644 --- a/utils/conversions.ts +++ b/utils/conversions.ts @@ -42,10 +42,10 @@ function removeNullProperties(obj: T): unknown { } return Object.keys(obj).reduce( (filteredObj, key) => - obj[key] === null + obj[key as keyof typeof obj] === null ? filteredObj : // eslint-disable-next-line @typescript-eslint/no-explicit-any - { ...(filteredObj as any), [key]: obj[key] }, + { ...(filteredObj as any), [key]: obj[key as keyof typeof obj] }, {} ); } diff --git a/yarn.lock b/yarn.lock index 9a511223..2d6e524d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1855,6 +1855,11 @@ dependencies: "@types/node" "*" +"@types/html-to-text@^5.1.1": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/html-to-text/-/html-to-text-5.1.2.tgz#59a723cce41b573b7fa606414e2ceaa886038098" + integrity sha512-56PHFwRe18fmZ+UsN6GqUaoKfalOwFzxaBMRA2O5URde3d5mxdH1HvnIV7LSugCiO8JWlRdSL2L3xJXykCTKVg== + "@types/http-errors@*": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" @@ -1951,6 +1956,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.66.tgz#dd035d409df322acc83dff62a602f12a5783bbb3" integrity sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw== +"@types/nodemailer@^4.6.8": + version "4.6.8" + resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-4.6.8.tgz#c14356e799fe1d4ee566126f901bc6031cc7b1b5" + integrity sha512-IX1P3bxDP1VIdZf6/kIWYNmSejkYm9MOyMEtoDFi4DVzKjJ3kY4GhOcOAKs6lZRjqVVmF9UjPOZXuQczlpZThw== + dependencies: + "@types/node" "*" + "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" From daabe66e611cfb8ce9a64f4483a8ea199c044998 Mon Sep 17 00:00:00 2001 From: gquadrati Date: Tue, 12 Mar 2024 10:46:23 +0100 Subject: [PATCH 06/12] [#IOPID-1450] enable strictNullChecks --- CreateDevelopmentProfile/handler.ts | 7 ++--- CreateSubscription/__tests__/handler.test.ts | 2 +- CreateUser/handler.ts | 3 +++ .../__tests__/backupAndDelete.test.ts | 2 +- DeleteUserDataActivity/backupAndDelete.ts | 4 +-- .../__tests__/handler.test.ts | 2 +- ExtractUserDataActivity/handler.ts | 16 +++++++----- GetFailedUserDataProcessing/handler.ts | 3 +++ GetFailedUserDataProcessingList/handler.ts | 3 +++ .../__tests__/handler.test.ts | 16 ++++++------ GetImpersonateService/handler.ts | 2 +- GetServices/handler.ts | 3 ++- GetUser/__tests__/handler.test.ts | 2 +- IsFailedUserDataProcessingActivity/handler.ts | 3 +++ SanitizeProfileEmail/handler.ts | 6 ++--- .../__tests__/handler.test.ts | 9 ++++--- SetUserSessionLockActivity/handler.ts | 8 +++--- UpdateUser/handler.ts | 3 +++ UpdateUserGroups/__tests__/handler.ts | 2 +- UpdateUserGroups/handler.ts | 26 ++++++++++++++++++- .../__test__/handler.test.ts | 2 +- UploadServiceLogo/__test__/handler.test.ts | 2 +- .../__tests__/handler.test.ts | 10 +++++-- .../handler.ts | 2 +- .../__tests__/handler.test.ts | 24 ++++++++++++++++- UserDataProcessingTrigger/handler.ts | 4 +-- tsconfig.json | 2 +- utils/conversions.ts | 5 ++-- utils/middlewares/cursorMiddleware.ts | 2 +- utils/sessionApiClient.ts | 10 ++++++- 30 files changed, 133 insertions(+), 52 deletions(-) diff --git a/CreateDevelopmentProfile/handler.ts b/CreateDevelopmentProfile/handler.ts index 2c0370a3..ce843891 100644 --- a/CreateDevelopmentProfile/handler.ts +++ b/CreateDevelopmentProfile/handler.ts @@ -49,9 +49,10 @@ export function toExtendedProfile(profile: RetrievedProfile): ExtendedProfile { accepted_tos_version: profile.acceptedTosVersion, blocked_inbox_or_channels: profile.blockedInboxOrChannels, email: profile.email, - is_email_already_taken: undefined, - is_email_enabled: profile.isEmailEnabled, - is_email_validated: profile.isEmailValidated, + // NOTE: We do NOT check email uniqueness in this context + is_email_already_taken: false, + is_email_enabled: profile.isEmailEnabled !== false, + is_email_validated: profile.isEmailValidated !== false, is_inbox_enabled: profile.isInboxEnabled === true, is_webhook_enabled: profile.isWebhookEnabled === true, preferred_languages: profile.preferredLanguages, diff --git a/CreateSubscription/__tests__/handler.test.ts b/CreateSubscription/__tests__/handler.test.ts index 2183c74b..8b399121 100644 --- a/CreateSubscription/__tests__/handler.test.ts +++ b/CreateSubscription/__tests__/handler.test.ts @@ -51,7 +51,7 @@ const aFakeApimProductContract: ProductContract = { const aFakeApimSubscriptionContract: SubscriptionContract = { allowTracing: false, createdDate: new Date(), - displayName: null, + displayName: undefined, endDate: undefined, expirationDate: undefined, id: "subscription-id", diff --git a/CreateUser/handler.ts b/CreateUser/handler.ts index 37c46517..e4e9aed4 100644 --- a/CreateUser/handler.ts +++ b/CreateUser/handler.ts @@ -120,6 +120,9 @@ export function CreateUserHandler( taskResults.apimClient.user.createOrUpdate( azureApimConfig.apimResourceGroup, azureApimConfig.apim, + // TODO: Implement a validation step to ensure the existence of `objectId` + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore taskResults.objectId, { email: userPayload.email, diff --git a/DeleteUserDataActivity/__tests__/backupAndDelete.test.ts b/DeleteUserDataActivity/__tests__/backupAndDelete.test.ts index 11e26b35..e3160d78 100644 --- a/DeleteUserDataActivity/__tests__/backupAndDelete.test.ts +++ b/DeleteUserDataActivity/__tests__/backupAndDelete.test.ts @@ -37,7 +37,7 @@ const asyncIteratorOf = (items: T[]): AsyncIterator => { const value = data.shift(); return { done: typeof value === "undefined", - value: [value] + value: [value!] }; } }; diff --git a/DeleteUserDataActivity/backupAndDelete.ts b/DeleteUserDataActivity/backupAndDelete.ts index ce17501b..87786b48 100644 --- a/DeleteUserDataActivity/backupAndDelete.ts +++ b/DeleteUserDataActivity/backupAndDelete.ts @@ -365,7 +365,7 @@ const backupAndDeleteMessageView = ({ }): TE.TaskEither> => pipe( messageViewModel.find([message.id, message.fiscalCode]), - TE.chain(TE.fromOption(() => undefined)), + TE.chainW(TE.fromOption(() => undefined)), TE.foldW( _ => // unfortunately, a document not found is threated like a query error @@ -443,7 +443,7 @@ const backupAndDeleteMessageContent = ({ }): TE.TaskEither> => pipe( messageModel.getContentFromBlob(messageContentBlobService, message.id), - TE.chain(TE.fromOption(() => undefined)), + TE.chainW(TE.fromOption(() => undefined)), TE.foldW( _ => // unfortunately, a document not found is threated like a query error diff --git a/ExtractUserDataActivity/__tests__/handler.test.ts b/ExtractUserDataActivity/__tests__/handler.test.ts index 8eace8c9..a6518319 100644 --- a/ExtractUserDataActivity/__tests__/handler.test.ts +++ b/ExtractUserDataActivity/__tests__/handler.test.ts @@ -120,7 +120,7 @@ const asyncIteratorOf = (items: T[]): AsyncIterator => { const value = data.shift(); return { done: typeof value === "undefined", - value: [value] + value: [value!] }; } }; diff --git a/ExtractUserDataActivity/handler.ts b/ExtractUserDataActivity/handler.ts index d1cd46de..c78c2bfe 100644 --- a/ExtractUserDataActivity/handler.ts +++ b/ExtractUserDataActivity/handler.ts @@ -319,13 +319,15 @@ export const findAllNotificationStatuses = ( notifications, // compose a query for every supported channel type - ROA.reduce([], (queries, { id: notificationId }) => [ - ...queries, - ...Object.values(NotificationChannelEnum).map(channel => [ - notificationId, - channel - ]) - ]), + ROA.reduce( + [] as ReadonlyArray, + (queries, { id: notificationId }) => [ + ...queries, + ...Object.values(NotificationChannelEnum).map( + channel => [notificationId, channel] as const + ) + ] + ), ROA.map(([notificationId, channel]) => pipe( notificationStatusModel.findOneNotificationStatusByNotificationChannel( diff --git a/GetFailedUserDataProcessing/handler.ts b/GetFailedUserDataProcessing/handler.ts index 3ec66912..9591b478 100644 --- a/GetFailedUserDataProcessing/handler.ts +++ b/GetFailedUserDataProcessing/handler.ts @@ -62,6 +62,9 @@ export const GetFailedUserDataProcessingHandler = ( failedUserDataProcessingTable, choice, fiscalCode, + // TODO: Refactor for using the new `@azure/data-tables` library + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore null, (error: Error, result: TableEntry, response: ServiceResponse) => response.isSuccessful diff --git a/GetFailedUserDataProcessingList/handler.ts b/GetFailedUserDataProcessingList/handler.ts index 5cce9ef6..6078ce95 100644 --- a/GetFailedUserDataProcessingList/handler.ts +++ b/GetFailedUserDataProcessingList/handler.ts @@ -54,6 +54,9 @@ export const GetFailedUserDataProcessingListHandler = ( tableService.queryEntities( failedUserDataProcessingTable, tableQuery, + // TODO: Refactor for using the new `@azure/data-tables` library + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore null, ( error: Error, diff --git a/GetImpersonateService/__tests__/handler.test.ts b/GetImpersonateService/__tests__/handler.test.ts index 4fda0a48..3cf58f63 100644 --- a/GetImpersonateService/__tests__/handler.test.ts +++ b/GetImpersonateService/__tests__/handler.test.ts @@ -114,8 +114,8 @@ describe("GetImpersonateServiceHandler", () => { const response = await getImpersonateServiceHandler( mockedContext as any, - undefined, - undefined + undefined as any, // Not used + undefined as any // Not used ); expect(response.kind).toEqual("IResponseErrorInternal"); @@ -131,7 +131,7 @@ describe("GetImpersonateServiceHandler", () => { ); const response = await handler( mockedContext as any, - undefined, + undefined as any, // Not used aBreakingApimSubscriptionId ); @@ -150,7 +150,7 @@ describe("GetImpersonateServiceHandler", () => { const response = await handler( mockedContext as any, - undefined, + undefined as any, // Not used aValidSubscriptionIdWithouthOwner ); @@ -173,7 +173,7 @@ describe("GetImpersonateServiceHandler", () => { const response = await handler( mockedContext as any, - undefined, + undefined as any, // Not used aValidSubscriptionIdWithouthOwner ); @@ -191,7 +191,7 @@ describe("GetImpersonateServiceHandler", () => { const response = await handler( mockedContext as any, - undefined, + undefined as any, // Not used aNotExistingSubscriptionId ); @@ -209,7 +209,7 @@ describe("GetImpersonateServiceHandler", () => { const response = await handler( mockedContext as any, - undefined, + undefined as any, // Not used aNotExistingSubscriptionId ); @@ -226,7 +226,7 @@ describe("GetImpersonateServiceHandler", () => { const response = await handler( mockedContext as any, - undefined, + undefined as any, // Not used aNotExistingSubscriptionId ); diff --git a/GetImpersonateService/handler.ts b/GetImpersonateService/handler.ts index 3649a985..2dc009f0 100644 --- a/GetImpersonateService/handler.ts +++ b/GetImpersonateService/handler.ts @@ -42,7 +42,7 @@ type IGetImpersonateService = ( >; const chainNullableWithNotFound = ( - value: string + value: string | undefined ): TE.TaskEither => pipe( value, diff --git a/GetServices/handler.ts b/GetServices/handler.ts index dcab44ec..f3b7e0da 100644 --- a/GetServices/handler.ts +++ b/GetServices/handler.ts @@ -83,7 +83,8 @@ export function GetServicesHandler( // keep only the latest version const isNewer = !prev.has(curr.serviceId) || - curr.version > prev.get(curr.serviceId); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + curr.version > prev.get(curr.serviceId)!; return isNewer ? prev.set(curr.serviceId, curr.version) : prev; } )(items), diff --git a/GetUser/__tests__/handler.test.ts b/GetUser/__tests__/handler.test.ts index 645ffaca..8be18fe0 100644 --- a/GetUser/__tests__/handler.test.ts +++ b/GetUser/__tests__/handler.test.ts @@ -237,7 +237,7 @@ describe("GetUser", () => { builtIn: true, description: "group description", displayName: "groupName", - externalId: null, + externalId: undefined, groupContractType: "custom", id: undefined, name: undefined, diff --git a/IsFailedUserDataProcessingActivity/handler.ts b/IsFailedUserDataProcessingActivity/handler.ts index 0126e39d..6621e255 100644 --- a/IsFailedUserDataProcessingActivity/handler.ts +++ b/IsFailedUserDataProcessingActivity/handler.ts @@ -63,6 +63,9 @@ export const IsFailedUserDataProcessing = ( failedUserDataProcessingTable, i.choice, i.fiscalCode, + // TODO: Refactor for using the new `@azure/data-tables` library + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore null, (error: Error, result: TableEntry, response: ServiceResponse) => response.isSuccessful diff --git a/SanitizeProfileEmail/handler.ts b/SanitizeProfileEmail/handler.ts index b0689f1b..46c471f6 100644 --- a/SanitizeProfileEmail/handler.ts +++ b/SanitizeProfileEmail/handler.ts @@ -75,8 +75,8 @@ const updateProfile = (profile: RetrievedProfile) => ( const trackResetEmailValidationEvent = ( profile: Pick -) => (r: { readonly telemetryClient: TelemetryClient }) => (): void => - r.telemetryClient.trackEvent({ +) => (r: { readonly telemetryClient?: TelemetryClient }) => (): void => + r.telemetryClient?.trackEvent({ name: "io.citizen-auth.reset_email_validation", tagOverrides: { "ai.user.id": hashFiscalCode(profile.fiscalCode), @@ -100,7 +100,7 @@ export const sanitizeProfileEmail = flow( RTE.chainFirstReaderIOKW(trackResetEmailValidationEvent) ) ), - O.getOrElse(() => RTE.right(void 0)) + O.getOrElseW(() => RTE.right(void 0)) ) ) ); diff --git a/SetUserDataProcessingStatus/__tests__/handler.test.ts b/SetUserDataProcessingStatus/__tests__/handler.test.ts index 8d1443e7..42f3b59a 100644 --- a/SetUserDataProcessingStatus/__tests__/handler.test.ts +++ b/SetUserDataProcessingStatus/__tests__/handler.test.ts @@ -17,6 +17,7 @@ import { UserDataProcessingStatusEnum } from "@pagopa/io-functions-commons/dist/ import { none, Option, some } from "fp-ts/lib/Option"; import { UserDataProcessingChoice } from "@pagopa/io-functions-commons/dist/generated/definitions/UserDataProcessingChoice"; import { pipe } from "fp-ts/lib/function"; +import { context } from "../../__mocks__/functions"; let throwError = jest.fn(() => false); @@ -62,7 +63,7 @@ describe("setUserDataProcessingStatusHandler", () => { const result = await setUserDataProcessingStatusHandler( mockUserDataProcessingModel )( - null, + context, aUserDataProcessingChoice, notExistingFiscalCode, UserDataProcessingStatusEnum.CLOSED @@ -97,7 +98,7 @@ describe("setUserDataProcessingStatusHandler", () => { const result = await setUserDataProcessingStatusHandler( mockUserDataProcessingModel )( - null, + context, aUserDataProcessingChoice, aFiscalCode, UserDataProcessingStatusEnum.CLOSED @@ -143,7 +144,7 @@ describe("setUserDataProcessingStatusHandler", () => { const result = await setUserDataProcessingStatusHandler( mockUserDataProcessingModel )( - null, + context, aUserDataProcessingChoice, aFiscalCode, UserDataProcessingStatusEnum.CLOSED @@ -189,7 +190,7 @@ describe("setUserDataProcessingStatusHandler", () => { const result = await setUserDataProcessingStatusHandler( mockUserDataProcessingModel )( - null, + context, aUserDataProcessingChoice, aFiscalCode, // @ts-ignore to force bad behavior diff --git a/SetUserSessionLockActivity/handler.ts b/SetUserSessionLockActivity/handler.ts index 514ef09e..e6747883 100644 --- a/SetUserSessionLockActivity/handler.ts +++ b/SetUserSessionLockActivity/handler.ts @@ -13,9 +13,9 @@ import { SuccessResponse } from "@pagopa/io-backend-session-sdk/SuccessResponse" import { Client } from "../utils/sessionApiClient"; // eslint-disable-next-line prefer-arrow/prefer-arrow-functions -function assertNever(_: never): void { +const assertNever = (_: never): never => { throw new Error("should not have executed this"); -} +}; // Activity input export const ActivityInput = t.interface({ @@ -93,7 +93,7 @@ const callSessionApi = ( case "UNLOCK": return sessionApiClient.unlockUserSession({ fiscalcode }); default: - assertNever(action); + return assertNever(action); } }, error => { @@ -157,7 +157,7 @@ const callSessionApi = ( }) ); default: - assertNever(status); + return assertNever(status); } }) ); diff --git a/UpdateUser/handler.ts b/UpdateUser/handler.ts index fefe417a..5b3342e8 100644 --- a/UpdateUser/handler.ts +++ b/UpdateUser/handler.ts @@ -65,6 +65,9 @@ const updateUser = ( TE.tryCatch( () => client.users.update( + // TODO: Implement a validation step to ensure the existence of `userPrincipalName` + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore user.userPrincipalName, withoutUndefinedValues({ displayName: diff --git a/UpdateUserGroups/__tests__/handler.ts b/UpdateUserGroups/__tests__/handler.ts index 60795d74..8bc0d4f6 100644 --- a/UpdateUserGroups/__tests__/handler.ts +++ b/UpdateUserGroups/__tests__/handler.ts @@ -340,7 +340,7 @@ describe("UpdateUserGroups", () => { ) .mockImplementationOnce(() => Promise.resolve( - updatedGroups.slice(-1, 1).concat([{ displayName: undefined }]) + updatedGroups.slice(-1, 1).concat([{ displayName: undefined as any }]) ) ); mockGroupListByService.mockImplementation(() => diff --git a/UpdateUserGroups/handler.ts b/UpdateUserGroups/handler.ts index bde03509..3cfe6cfd 100644 --- a/UpdateUserGroups/handler.ts +++ b/UpdateUserGroups/handler.ts @@ -112,7 +112,10 @@ function clusterizeGroups( Object.entries(existingGroups), _ => new Map(_), RMAP.reduceWithIndex(S.Ord)( - { toBeAssociated: [], toBeRemoved: [] }, + { + toBeAssociated: [] as ReadonlyArray, + toBeRemoved: [] as ReadonlyArray + }, (displayName, cluster, name) => { if ( currentUserGroups.includes(displayName) && @@ -223,6 +226,9 @@ export function UpdateUserGroupHandler( taskResults.apimClient, azureApimConfig.apimResourceGroup, azureApimConfig.apim, + // TODO: Implement a validation step to ensure the existence of `userName`. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore taskResults.userName ), TE.mapLeft(error => @@ -253,6 +259,9 @@ export function UpdateUserGroupHandler( apimClient: taskResults.apimClient, currentUserGroups: taskResults.currentUserGroups, existingGroups: groupList.reduce>( + // TODO: Implement a validation step to ensure the existence of `curr.name`. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore (prev, curr) => ({ ...prev, [curr.displayName]: curr.name }), {} ), @@ -265,6 +274,9 @@ export function UpdateUserGroupHandler( [...userGroupsPayload.groups], A.traverse(TE.ApplicativePar)( TE.fromPredicate( + // TODO: Add validation + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore groupName => taskResults.existingGroups[groupName] !== undefined, __ => ResponseErrorValidation("Bad request", "Invalid groups") ) @@ -274,6 +286,9 @@ export function UpdateUserGroupHandler( ), TE.chainW(taskResults => { const groupsClusterization = clusterizeGroups( + // TODO: Add validation + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore taskResults.existingGroups, taskResults.currentUserGroups, userGroupsPayload.groups @@ -287,6 +302,9 @@ export function UpdateUserGroupHandler( azureApimConfig.apimResourceGroup, azureApimConfig.apim, groupName, + // TODO: Add validation + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore taskResults.userName ), E.toError @@ -303,6 +321,9 @@ export function UpdateUserGroupHandler( azureApimConfig.apimResourceGroup, azureApimConfig.apim, groupName, + // TODO Add validation + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore taskResults.userName ), E.toError @@ -332,6 +353,9 @@ export function UpdateUserGroupHandler( taskResults.apimClient, azureApimConfig.apimResourceGroup, azureApimConfig.apim, + // TODO: Add validation + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore taskResults.userName ), TE.mapLeft(error => diff --git a/UploadOrganizationLogo/__test__/handler.test.ts b/UploadOrganizationLogo/__test__/handler.test.ts index ba791f3d..1a8e83ad 100644 --- a/UploadOrganizationLogo/__test__/handler.test.ts +++ b/UploadOrganizationLogo/__test__/handler.test.ts @@ -15,7 +15,7 @@ describe("UploadOrganizationLogoHandler", () => { } as Logo; const mockedContext = { bindings: { - logo: undefined as string + logo: undefined } }; diff --git a/UploadServiceLogo/__test__/handler.test.ts b/UploadServiceLogo/__test__/handler.test.ts index dd841dce..582d3dd2 100644 --- a/UploadServiceLogo/__test__/handler.test.ts +++ b/UploadServiceLogo/__test__/handler.test.ts @@ -68,7 +68,7 @@ describe("UpdateServiceLogoHandler", () => { } as Logo; const mockedContext = { bindings: { - logo: undefined as string + logo: undefined } }; diff --git a/UserDataProcessingProcessFailedRecords/__tests__/handler.test.ts b/UserDataProcessingProcessFailedRecords/__tests__/handler.test.ts index 27a3f05e..988c6d35 100644 --- a/UserDataProcessingProcessFailedRecords/__tests__/handler.test.ts +++ b/UserDataProcessingProcessFailedRecords/__tests__/handler.test.ts @@ -130,7 +130,9 @@ const recordsIterator = (query: string | SqlQuerySpec) => ({ async *[Symbol.asyncIterator]() { // I don't care for string queries, but I manage it to keep signatures coherent const queriedRecords = userDataProcessingRecords.filter(r => - typeof query === "string" ? false : r.status === query.parameters[0].value + typeof query === "string" || query.parameters === undefined + ? false + : r.status === query.parameters[0].value ); for (const record of queriedRecords) { // wait for 100ms to not pass the 5000ms limit of jest @@ -145,7 +147,11 @@ const recordsIterator = (query: string | SqlQuerySpec) => ({ record.status, 1 as NonNegativeInteger ), - _ => void 0 + _ => [ + { + message: "error" + } as t.ValidationError + ] ) ]; } diff --git a/UserDataProcessingProcessFailedRecords/handler.ts b/UserDataProcessingProcessFailedRecords/handler.ts index 16946a9c..05e1bef8 100644 --- a/UserDataProcessingProcessFailedRecords/handler.ts +++ b/UserDataProcessingProcessFailedRecords/handler.ts @@ -33,7 +33,7 @@ import { isOrchestratorRunning } from "../utils/orchestrator"; const logPrefix = "UserDataProcessingProcessFailedRecordsHandler"; type IGetFailedUserDataProcessingHandlerResult = - | IResponseSuccessJson> + | IResponseSuccessJson> | IResponseErrorQuery; type IGetFailedUserDataProcessingHandler = ( diff --git a/UserDataProcessingTrigger/__tests__/handler.test.ts b/UserDataProcessingTrigger/__tests__/handler.test.ts index d11e8f01..0055cca0 100644 --- a/UserDataProcessingTrigger/__tests__/handler.test.ts +++ b/UserDataProcessingTrigger/__tests__/handler.test.ts @@ -237,7 +237,29 @@ describe("FailedUserDataProcessing", () => { expect(insertEntity).toBeCalledWith({ PartitionKey: eg.String(failedUserDataProcessing[0].choice), RowKey: eg.String(failedUserDataProcessing[0].fiscalCode), - Reason: eg.String(failedUserDataProcessing[0].reason) + Reason: eg.String(failedUserDataProcessing[0].reason ?? "UNKNOWN") + }); + expect(deleteEntity).not.toBeCalled(); + }); + + it("should process a failed user_data_processing inserting a failed record with unknown reason", async () => { + const failedUserDataProcessing: ReadonlyArray = [ + aFailedUserDataProcessing + ]; + + const input: ReadonlyArray = [...failedUserDataProcessing] + .map(toUndecoded) + .map(v => ({ ...v, reason: undefined })); + + const handler = triggerHandler(insertEntity, deleteEntity); + await handler(context, input); + + expect(insertEntity).toBeCalled(); + expect(insertEntity).toBeCalledTimes(1); + expect(insertEntity).toBeCalledWith({ + PartitionKey: eg.String(failedUserDataProcessing[0].choice), + RowKey: eg.String(failedUserDataProcessing[0].fiscalCode), + Reason: eg.String("UNKNOWN") }); expect(deleteEntity).not.toBeCalled(); }); diff --git a/UserDataProcessingTrigger/handler.ts b/UserDataProcessingTrigger/handler.ts index 2ed1fbfd..8541dba7 100644 --- a/UserDataProcessingTrigger/handler.ts +++ b/UserDataProcessingTrigger/handler.ts @@ -194,7 +194,7 @@ const processFailedUserDataProcessing = async ( ); const { e1: resultOrError, e2: sResponse } = await insertEntityFn({ PartitionKey: eg.String(processable.choice), - Reason: eg.String(processable.reason), + Reason: eg.String(processable.reason ?? "UNKNOWN"), RowKey: eg.String(processable.fiscalCode) }); if (E.isLeft(resultOrError) && sResponse.statusCode !== 409) { @@ -253,7 +253,7 @@ const getAction = ( ? (): Promise => processClosedUserDataProcessing(context, processable, removeFailure) : // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - () => void 0; + async () => void 0; // eslint-disable-next-line prefer-arrow/prefer-arrow-functions, sonarjs/cognitive-complexity export const triggerHandler = ( diff --git a/tsconfig.json b/tsconfig.json index a6f788b7..6fa9c26f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "noImplicitThis": true, "alwaysStrict": true, "strictBindCallApply": true, - "strictNullChecks": false, + "strictNullChecks": true, "strictFunctionTypes": true, "strictPropertyInitialization": false, "useUnknownInCatchVariables": true, diff --git a/utils/conversions.ts b/utils/conversions.ts index 1eca5e2e..0c0a500e 100644 --- a/utils/conversions.ts +++ b/utils/conversions.ts @@ -93,7 +93,8 @@ export function apiServiceToService(service: ApiService): Service { serviceMetadata: { ...commonService.serviceMetadata, category: service.service_metadata.category, - customSpecialFlow: service.service_metadata.custom_special_flow + customSpecialFlow: service.service_metadata.custom_special_flow, + scope: service.service_metadata.scope } } : service.service_metadata @@ -115,7 +116,7 @@ export function apiServiceToService(service: ApiService): Service { // eslint-disable-next-line prefer-arrow/prefer-arrow-functions export function toApiServiceMetadata( service: RetrievedService -): ApiServiceMetadata { +): ApiServiceMetadata | undefined { return service.serviceMetadata ? toServiceMetadata(service.serviceMetadata) : undefined; diff --git a/utils/middlewares/cursorMiddleware.ts b/utils/middlewares/cursorMiddleware.ts index 99f978f1..1ac4dac0 100644 --- a/utils/middlewares/cursorMiddleware.ts +++ b/utils/middlewares/cursorMiddleware.ts @@ -10,7 +10,7 @@ import { ResponseErrorFromValidationErrors } from "@pagopa/ts-commons/lib/respon export const CursorMiddleware: IRequestMiddleware< "IResponseErrorValidation", - NonNegativeInteger + NonNegativeInteger | undefined > = async request => request.query.cursor ? pipe( diff --git a/utils/sessionApiClient.ts b/utils/sessionApiClient.ts index 8714f11d..dea9c34a 100644 --- a/utils/sessionApiClient.ts +++ b/utils/sessionApiClient.ts @@ -21,6 +21,12 @@ import { } from "@pagopa/io-backend-session-sdk/requestTypes"; import { identity } from "fp-ts/lib/function"; +// This is a placeholder for undefined when dealing with object keys +// Typescript doesn't perform well when narrowing a union type which includes string and undefined +// (example: "foo" | "bar" | undefined) +// We use this as a placeholder for type parameters indicating "no key" +type __UNDEFINED_KEY = "_____"; + export type ApiOperation = TypeofApiCall & TypeofApiCall; @@ -54,7 +60,7 @@ export type WithDefaultsT< * Defines a collection of api operations * @param K name of the parameters that the Clients masks from the operations */ -export type Client = { +export type Client = { readonly lockUserSession: TypeofApiCall< ReplaceRequestParams< LockUserSessionT, @@ -70,6 +76,8 @@ export type Client = { >; }; +// TODO(IOPID-1648): Move to generated client + /** * Create an instance of a client * @param params hash map of parameters thata define the client: From 29ce231516d399fd36afc57abc770ec0191f3213 Mon Sep 17 00:00:00 2001 From: gquadrati Date: Tue, 12 Mar 2024 10:56:23 +0100 Subject: [PATCH 07/12] [#IOPID-1450] enable strictPropertyInitialization --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 6fa9c26f..322776fc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "strictBindCallApply": true, "strictNullChecks": true, "strictFunctionTypes": true, - "strictPropertyInitialization": false, + "strictPropertyInitialization": true, "useUnknownInCatchVariables": true, "resolveJsonModule": true, "skipLibCheck": true From 2e2b08fedfc042c32c2e607784b5c28818abcfed Mon Sep 17 00:00:00 2001 From: gquadrati Date: Tue, 12 Mar 2024 10:59:44 +0100 Subject: [PATCH 08/12] [#IOPID-1450] enable strict --- tsconfig.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 322776fc..073cd503 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,14 +5,7 @@ "outDir": "dist", "rootDir": ".", "sourceMap": true, - "strict": false, - "noImplicitAny": true, - "noImplicitThis": true, - "alwaysStrict": true, - "strictBindCallApply": true, - "strictNullChecks": true, - "strictFunctionTypes": true, - "strictPropertyInitialization": true, + "strict": true, "useUnknownInCatchVariables": true, "resolveJsonModule": true, "skipLibCheck": true From f61309af2c7cbd797498c4bab0313f2796f1f315 Mon Sep 17 00:00:00 2001 From: Greta Quadrati <75862507+gquadrati@users.noreply.github.com> Date: Tue, 12 Mar 2024 14:59:08 +0100 Subject: [PATCH 09/12] Update DeleteUserDataActivity/utils.ts Co-authored-by: Rodolfo Viti <62432865+rodoviti@users.noreply.github.com> --- DeleteUserDataActivity/utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DeleteUserDataActivity/utils.ts b/DeleteUserDataActivity/utils.ts index 53071a18..f4dd6267 100644 --- a/DeleteUserDataActivity/utils.ts +++ b/DeleteUserDataActivity/utils.ts @@ -16,10 +16,10 @@ import { // Cosmos Errors export enum CosmosErrorsTypes { - "OSMOS_EMPTY_RESPONSE" = "OSMOS_EMPTY_RESPONSE", - "OSMOS_CONFLICT_RESPONSE" = "OSMOS_CONFLICT_RESPONSE", - "OSMOS_DECODING_ERROR" = "OSMOS_DECODING_ERROR", - "OSMOS_ERROR_RESPONSE" = "OSMOS_ERROR_RESPONSE" + "COSMOS_EMPTY_RESPONSE" = "COSMOS_EMPTY_RESPONSE", + "COSMOS_CONFLICT_RESPONSE" = "COSMOS_CONFLICT_RESPONSE", + "COSMOS_DECODING_ERROR" = "COSMOS_DECODING_ERROR", + "COSMOS_ERROR_RESPONSE" = "COSMOS_ERROR_RESPONSE" } const CosmosErrorsTypesC = t.interface({ From 2e0f07e641fb16e12ad322da065762119eea0fb1 Mon Sep 17 00:00:00 2001 From: gquadrati Date: Wed, 13 Mar 2024 15:32:45 +0100 Subject: [PATCH 10/12] [#IOPID-1450] add tests for isCosmosErrors --- .../__tests__/backupAndDelete.test.ts | 37 +++++++++++++++++ .../__tests__/utils.test.ts | 40 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 DeleteUserDataActivity/__tests__/utils.test.ts diff --git a/DeleteUserDataActivity/__tests__/backupAndDelete.test.ts b/DeleteUserDataActivity/__tests__/backupAndDelete.test.ts index e3160d78..1d899c44 100644 --- a/DeleteUserDataActivity/__tests__/backupAndDelete.test.ts +++ b/DeleteUserDataActivity/__tests__/backupAndDelete.test.ts @@ -43,6 +43,12 @@ const asyncIteratorOf = (items: T[]): AsyncIterator => { }; }; +export async function* errorMessageIterator(error: any) { + //Sonarcloud requires at least one `yield` before `throw` operation + yield [E.right(aRetrievedMessageWithContent)]; + throw error; +} + // MessageContentBlobService const messageContentBlobService = ({} as unknown) as BlobService; @@ -289,6 +295,37 @@ describe(`backupAndDeleteAllUserData`, () => { expect(E.isRight(result)).toBe(true); }); + it("should stop if an error occurred retrieving messages", async () => { + const cosmosError = { kind: "COSMOS_ERROR_RESPONSE" }; + mockFindMessages.mockImplementationOnce(() => + TE.of(errorMessageIterator(cosmosError)) + ); + + const result = await backupAndDeleteAllUserData({ + authenticationLockService, + messageContentBlobService, + messageModel, + messageStatusModel, + messageViewModel, + notificationModel, + notificationStatusModel, + profileEmailsRepository, + profileModel, + servicePreferencesModel, + userDataBackup, + fiscalCode: aFiscalCode + })(); + + console.log(result); + + expect(result).toEqual( + E.left({ + kind: "QUERY_FAILURE", + reason: `CosmosError: ${JSON.stringify(cosmosError)}` + }) + ); + }); + it("should stop if there is an error while looking for a message View (404)", async () => { mockFindMessageView.mockImplementationOnce(() => TE.left({ diff --git a/DeleteUserDataActivity/__tests__/utils.test.ts b/DeleteUserDataActivity/__tests__/utils.test.ts new file mode 100644 index 00000000..9edcb9f9 --- /dev/null +++ b/DeleteUserDataActivity/__tests__/utils.test.ts @@ -0,0 +1,40 @@ +import { CosmosErrors } from "@pagopa/io-functions-commons/dist/src/utils/cosmosdb_model"; + +import { isCosmosErrors } from "../utils"; + +describe("utils", () => { + // -------------- + // Just a bunch of types needed for creating a tuple from an union type + // See https://www.hacklewayne.com/typescript-convert-union-to-tuple-array-yes-but-how + type Contra = T extends any ? (arg: T) => void : never; + type InferContra = [T] extends [(arg: infer I) => void] ? I : never; + type PickOne = InferContra>>>; + type Union2Tuple = PickOne extends infer U // assign PickOne to U + ? Exclude extends never // T and U are the same + ? [T] + : [...Union2Tuple>, U] // recursion + : never; + // -------------- + + type CosmosErrorsTypesTuple = Union2Tuple; + + // NOTE: If a new cosmos error is added, the following initialization will not compile, + // forcing us to update `CosmosErrorsTypes` with the new value + const values: CosmosErrorsTypesTuple = [ + "COSMOS_EMPTY_RESPONSE", + "COSMOS_CONFLICT_RESPONSE", + "COSMOS_DECODING_ERROR", + "COSMOS_ERROR_RESPONSE" + ]; + + it.each(values)( + "isCosmosErrors should return true if error is a CosmosError of type %s", + v => { + expect(isCosmosErrors({ kind: v })).toBe(true); + } + ); + + it("isCosmosErrors should return false if error is not a CosmosError", () => { + expect(isCosmosErrors({ kind: "ANOTHER_ERROR" })).toBe(false); + }); +}); From 13289a70c86f945983ec6e17fee2ccff25ad2f77 Mon Sep 17 00:00:00 2001 From: gquadrati Date: Thu, 14 Mar 2024 09:45:34 +0100 Subject: [PATCH 11/12] [#IOPID-1450] refactor based on comments, part 1 --- .../__tests__/backupAndDelete.test.ts | 19 +++++-------------- DeleteUserDataActivity/backupAndDelete.ts | 16 ++++++++-------- .../__tests__/handler.test.ts | 17 +++++------------ GetServices/handler.ts | 8 ++++---- 4 files changed, 22 insertions(+), 38 deletions(-) diff --git a/DeleteUserDataActivity/__tests__/backupAndDelete.test.ts b/DeleteUserDataActivity/__tests__/backupAndDelete.test.ts index 1d899c44..e5678d26 100644 --- a/DeleteUserDataActivity/__tests__/backupAndDelete.test.ts +++ b/DeleteUserDataActivity/__tests__/backupAndDelete.test.ts @@ -30,18 +30,11 @@ import { IBlobServiceInfo } from "../types"; import { AuthenticationLockServiceMock } from "../../__mocks__/authenticationLockService.mock"; import { IProfileEmailWriter } from "@pagopa/io-functions-commons/dist/src/utils/unique_email_enforcement"; -const asyncIteratorOf = (items: T[]): AsyncIterator => { - const data = [...items]; - return { - next: async () => { - const value = data.shift(); - return { - done: typeof value === "undefined", - value: [value!] - }; - } - }; -}; +export async function* asyncIteratorOf(items: T[]) { + for (const item of items) { + yield [item]; + } +} export async function* errorMessageIterator(error: any) { //Sonarcloud requires at least one `yield` before `throw` operation @@ -316,8 +309,6 @@ describe(`backupAndDeleteAllUserData`, () => { fiscalCode: aFiscalCode })(); - console.log(result); - expect(result).toEqual( E.left({ kind: "QUERY_FAILURE", diff --git a/DeleteUserDataActivity/backupAndDelete.ts b/DeleteUserDataActivity/backupAndDelete.ts index 87786b48..8749ed09 100644 --- a/DeleteUserDataActivity/backupAndDelete.ts +++ b/DeleteUserDataActivity/backupAndDelete.ts @@ -2,7 +2,7 @@ import * as crypto from "crypto"; import { BlobService } from "azure-storage"; import { sequenceT } from "fp-ts/lib/Apply"; -import * as RA from "fp-ts/lib/ReadonlyArray"; +import * as ROA from "fp-ts/lib/ReadonlyArray"; import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; import * as TE from "fp-ts/lib/TaskEither"; @@ -72,13 +72,13 @@ const executeRecursiveBackupAndDelete = ( ? TE.left>( toQueryFailure(new Error("Some elements are not typed correctly")) ) - : TE.of>(RA.rights(e.value)) + : TE.of>(ROA.rights(e.value)) ), // executes backup&delete for this set of items TE.chainW(items => pipe( items, - RA.map((item: T) => + ROA.map((item: T) => pipe( sequenceT(TE.ApplicativeSeq)< DataFailure, @@ -104,8 +104,8 @@ const executeRecursiveBackupAndDelete = ( TE.map(([_, __, nextResults]) => [item, ...nextResults]) ) ), - RA.sequence(TE.ApplicativePar), - TE.map(RA.flatten) + ROA.sequence(TE.ApplicativePar), + TE.map(ROA.flatten) ) ) ); @@ -596,7 +596,7 @@ const backupAndDeleteAllMessagesData = ({ : toQueryFailure(E.toError(err)) ) ), - TE.map(RA.flatten), + TE.map(ROA.flatten), TE.chainW(results => results.some(E.isLeft) ? TE.left( @@ -604,8 +604,8 @@ const backupAndDeleteAllMessagesData = ({ new Error("Cannot decode some element due to decoding errors") ) ) - : RA.sequence(TE.ApplicativeSeq)( - RA.rights(results).map(message => { + : ROA.sequence(TE.ApplicativeSeq)( + ROA.rights(results).map(message => { // cast needed because findMessages has a wrong signature // eslint-disable-next-line @typescript-eslint/no-explicit-any const retrievedMessage = (message as any) as RetrievedMessageWithoutContent; diff --git a/ExtractUserDataActivity/__tests__/handler.test.ts b/ExtractUserDataActivity/__tests__/handler.test.ts index a6518319..b41d0b99 100644 --- a/ExtractUserDataActivity/__tests__/handler.test.ts +++ b/ExtractUserDataActivity/__tests__/handler.test.ts @@ -113,18 +113,11 @@ const messageModelMock = ({ } as any) as MessageModel; // ServicePreferences Model -const asyncIteratorOf = (items: T[]): AsyncIterator => { - const data = [...items]; - return { - next: async () => { - const value = data.shift(); - return { - done: typeof value === "undefined", - value: [value!] - }; - } - }; -}; +export async function* asyncIteratorOf(items: T[]) { + for (const item of items) { + yield [item]; + } +} const mockDeleteServicePreferences = jest.fn< ReturnType["delete"]>, diff --git a/GetServices/handler.ts b/GetServices/handler.ts index f3b7e0da..40e23e20 100644 --- a/GetServices/handler.ts +++ b/GetServices/handler.ts @@ -23,7 +23,7 @@ import { pipe } from "fp-ts/lib/function"; import * as E from "fp-ts/lib/Either"; import * as TE from "fp-ts/lib/TaskEither"; import * as RMAP from "fp-ts/lib/ReadonlyMap"; -import * as RA from "fp-ts/lib/ReadonlyArray"; +import * as ROA from "fp-ts/lib/ReadonlyArray"; import { asyncIteratorToArray, flattenAsyncIterator @@ -70,11 +70,11 @@ export function GetServicesHandler( TE.map(results => pipe( results, - RA.filter(E.isRight), - RA.map(e => e.right), + ROA.filter(E.isRight), + ROA.map(e => e.right), // create a Map (serviceId, lastVersionNumber) items => - RA.reduce( + ROA.reduce( new Map< typeof items[0]["serviceId"], typeof items[0]["version"] From d70ccaa3c845511168ae625d3497e20d15be64ad Mon Sep 17 00:00:00 2001 From: gquadrati Date: Thu, 14 Mar 2024 16:54:25 +0100 Subject: [PATCH 12/12] [#IOPID-1450] refactor based on comments, part 2 --- GetServices/handler.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/GetServices/handler.ts b/GetServices/handler.ts index 40e23e20..3256bd11 100644 --- a/GetServices/handler.ts +++ b/GetServices/handler.ts @@ -19,8 +19,9 @@ import { ResponseErrorQuery } from "@pagopa/io-functions-commons/dist/src/utils/response"; -import { pipe } from "fp-ts/lib/function"; +import { identity, pipe } from "fp-ts/lib/function"; import * as E from "fp-ts/lib/Either"; +import * as O from "fp-ts/Option"; import * as TE from "fp-ts/lib/TaskEither"; import * as RMAP from "fp-ts/lib/ReadonlyMap"; import * as ROA from "fp-ts/lib/ReadonlyArray"; @@ -79,14 +80,19 @@ export function GetServicesHandler( typeof items[0]["serviceId"], typeof items[0]["version"] >(), - (prev, curr: typeof items[0]) => { + (prev, curr: typeof items[0]) => // keep only the latest version - const isNewer = - !prev.has(curr.serviceId) || - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - curr.version > prev.get(curr.serviceId)!; - return isNewer ? prev.set(curr.serviceId, curr.version) : prev; - } + pipe( + prev.has(curr.serviceId), + O.fromPredicate(identity), + O.chainNullableK(() => prev.get(curr.serviceId)), + O.fold( + () => true, + prevVersion => curr.version > prevVersion + ), + isNewer => + isNewer ? prev.set(curr.serviceId, curr.version) : prev + ) )(items), // format into an array of { id, version } RMAP.collect(Ord)((serviceId, version) => ({