Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#172621882] Get all data for a specific user #42

Merged
merged 29 commits into from
May 13, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
88113b6
update libs
balanza May 7, 2020
72a3437
implemented SetUserDataProcessingStatusActivityHandler
balanza May 7, 2020
42d6145
Merge branch 'master' into 172621882-accesso-ai-dati
balanza May 8, 2020
1cab5c3
ExtractUserDataActivity
balanza May 11, 2020
a497dbb
better comments
balanza May 11, 2020
ff9a056
better failure handling
balanza May 11, 2020
3b800f8
fix exhaustive check
balanza May 11, 2020
691b857
fix lint
balanza May 11, 2020
87d4032
refactor
balanza May 11, 2020
1febd9a
retrieving notifications using message id
balanza May 12, 2020
6a5092b
fix comments
balanza May 12, 2020
98dd1f4
add query notification status
balanza May 12, 2020
31a6259
message blobs as separate entities
balanza May 12, 2020
e2b542c
use t.exact instead of manually skims types
balanza May 12, 2020
a876b03
refactor sequences
balanza May 12, 2020
975e3e5
using flatten
balanza May 12, 2020
4b0cf46
avoid extract webhook urls
balanza May 12, 2020
0bd30e8
refactor
balanza May 13, 2020
e6cdd0f
Update ExtractUserDataActivity/index.ts
balanza May 13, 2020
bffd99e
Merge branch '172621882-accesso-ai-dati' of github.com:pagopa/io-func…
balanza May 13, 2020
c4fb9ae
using bimap
balanza May 13, 2020
caf973f
Update ExtractUserDataActivity/handler.ts
balanza May 13, 2020
daadc33
Update ExtractUserDataActivity/notification.ts
balanza May 13, 2020
ab9cd6b
Update ExtractUserDataActivity/handler.ts
balanza May 13, 2020
44ce7be
Update ExtractUserDataActivity/handler.ts
balanza May 13, 2020
3b55c55
refactor
balanza May 13, 2020
36d8728
Merge branch '172621882-accesso-ai-dati' of github.com:pagopa/io-func…
balanza May 13, 2020
eec952f
refactor
balanza May 13, 2020
919c475
added message status to export
balanza May 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions ExtractUserDataActivity/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* tslint:disable: no-any */

import { Either, right } from "fp-ts/lib/Either";
import { fromNullable, Option, some } from "fp-ts/lib/Option";

import { context as contextMock } from "../../__mocks__/durable-functions";
import { aFiscalCode, aProfile } from "../../__mocks__/mocks";

import {
ActivityInput,
ActivityResultSuccess,
createExtractUserDataActivityHandler
} from "../handler";

import { QueryError } from "documentdb";
import { MessageModel } from "io-functions-commons/dist/src/models/message";
import { ProfileModel } from "io-functions-commons/dist/src/models/profile";
import { SenderServiceModel } from "io-functions-commons/dist/src/models/sender_service";
import {
aRetrievedMessageWithContent,
aRetrievedNotification,
aRetrievedSenderService
} from "../../__mocks__/mocks";
import { NotificationModel } from "../notification"; // we use the local-defined model

const createMockIterator = <T>(a: ReadonlyArray<T>) => {
const data = Array.from(a);
return {
async executeNext(): Promise<Either<QueryError, Option<readonly T[]>>> {
const next = data.shift();
return right(fromNullable(next ? [next] : undefined));
}
};
};

const messageModelMock = ({
findMessages: jest.fn(() =>
createMockIterator([aRetrievedMessageWithContent])
)
} as any) as MessageModel;

const senderServiceModelMock = ({
findSenderServicesForRecipient: jest.fn(() =>
createMockIterator([aRetrievedSenderService])
)
} as any) as SenderServiceModel;

const profileModelMock = ({
findOneProfileByFiscalCode: jest.fn(async () => right(some(aProfile)))
} as any) as ProfileModel;

const notificationModelMock = ({
findNotificationsForRecipient: jest.fn(() =>
createMockIterator([aRetrievedNotification])
)
} as any) as NotificationModel;

describe("createExtractUserDataActivityHandler", () => {
it("should handle export for existing user", async () => {
const handler = createExtractUserDataActivityHandler(
messageModelMock,
notificationModelMock,
profileModelMock,
senderServiceModelMock
);
const input: ActivityInput = {
fiscalCode: aFiscalCode
};

const result = await handler(contextMock, input);

result.fold(
response => fail(`Failing result, response: ${JSON.stringify(response)}`),
response => {
ActivityResultSuccess.decode(response).fold(
err =>
fail(`Failing decondig result, response: ${JSON.stringify(err)}`),
e => {
expect(e.kind).toBe("SUCCESS");
expect(e.value.profile).toEqual(aProfile);
expect(e.value.messages).toEqual([aRetrievedMessageWithContent]);
expect(e.value.senderServices).toEqual([aRetrievedSenderService]);
expect(e.value.notifications).toEqual([aRetrievedNotification]);
}
);
}
);
});
});
10 changes: 10 additions & 0 deletions ExtractUserDataActivity/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"bindings": [
{
"name": "name",
"type": "activityTrigger",
"direction": "in"
}
],
"scriptFile": "../dist/ExtractUserDataActivity/index.js"
}
263 changes: 263 additions & 0 deletions ExtractUserDataActivity/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/**
* This activity extracts all the data about a user contained in our db.
*
*/

import * as t from "io-ts";

import { sequenceS } from "fp-ts/lib/Apply";
import { Either, fromOption, left } from "fp-ts/lib/Either";
import {
fromEither,
TaskEither,
taskEither,
tryCatch
} from "fp-ts/lib/TaskEither";

import { Context } from "@azure/functions";

import { QueryError } from "documentdb";
import {
Message,
MessageModel
} from "io-functions-commons/dist/src/models/message";
import { Notification } from "io-functions-commons/dist/src/models/notification";
import {
Profile,
ProfileModel
} from "io-functions-commons/dist/src/models/profile";
import {
SenderService,
SenderServiceModel
} from "io-functions-commons/dist/src/models/sender_service";
import { iteratorToArray } from "io-functions-commons/dist/src/utils/documentdb";
import { readableReport } from "italia-ts-commons/lib/reporters";
import { FiscalCode } from "italia-ts-commons/lib/strings";
import { NotificationModel } from "./notification";

// the shape of the dataset to be extracted
const AllUserData = t.interface({
messages: t.readonlyArray(Message, "MessageList"),
notifications: t.readonlyArray(Notification, "NotificationList"),
profile: Profile,
senderServices: t.readonlyArray(SenderService, "SenderServiceList")
});
export type AllUserData = t.TypeOf<typeof AllUserData>;

// Activity input
export const ActivityInput = t.interface({
fiscalCode: FiscalCode
});
export type ActivityInput = t.TypeOf<typeof ActivityInput>;

// Activity success result
export const ActivityResultSuccess = t.interface({
kind: t.literal("SUCCESS"),
value: AllUserData
});
export type ActivityResultSuccess = t.TypeOf<typeof ActivityResultSuccess>;

// Activity failed because of invalid input
const ActivityResultInvalidInputFailure = t.interface({
kind: t.literal("INVALID_INPUT_FAILURE"),
reason: t.string
});
export type ActivityResultInvalidInputFailure = t.TypeOf<
typeof ActivityResultInvalidInputFailure
>;

// Activity failed because of an error on a query
const ActivityResultQueryFailure = t.intersection([
t.interface({
kind: t.literal("QUERY_FAILURE"),
reason: t.string
}),
t.partial({ query: t.string })
]);
export type ActivityResultQueryFailure = t.TypeOf<
typeof ActivityResultQueryFailure
>;

// activity failed for user not found
const ActivityResultUserNotFound = t.interface({
kind: t.literal("USER_NOT_FOUND_FAILURE")
});
export type ActivityResultUserNotFound = t.TypeOf<
typeof ActivityResultUserNotFound
>;

export const ActivityResultFailure = t.taggedUnion("kind", [
ActivityResultUserNotFound,
ActivityResultQueryFailure,
ActivityResultInvalidInputFailure
]);
export type ActivityResultFailure = t.TypeOf<typeof ActivityResultFailure>;

export const ActivityResult = t.taggedUnion("kind", [
ActivityResultSuccess,
ActivityResultFailure
]);
export type ActivityResult = t.TypeOf<typeof ActivityResult>;

const logPrefix = `ExtractUserDataActivity`;

/**
* Converts a Promise<Either> into a TaskEither
* This is needed because our models return unconvenient type. Both left and rejection cases are handled as a TaskEither left
* @param lazyPromise a lazy promise to convert
* @param queryName an optional name for the query, for logging purpose
*
* @returns either the query result or a query failure
*/
const fromQueryEither = <R>(
lazyPromise: () => Promise<Either<QueryError, R>>,
queryName: string = ""
) =>
tryCatch(lazyPromise, (err: Error) =>
ActivityResultQueryFailure.encode({
kind: "QUERY_FAILURE",
query: queryName,
reason: err.message
})
).chain((queryErrorOrRecord: Either<QueryError, R>) =>
fromEither(
queryErrorOrRecord.mapLeft(queryError =>
ActivityResultQueryFailure.encode({
kind: "QUERY_FAILURE",
query: queryName,
reason: JSON.stringify(queryError)
})
)
)
);

function assertNever(_: never): void {
throw new Error("should not have executed this");
}

/**
* Logs depending on failure type
* @param context the Azure functions context
* @param failure the failure to log
*/
const logFailure = (context: Context) => (
failure: ActivityResultFailure
): void => {
switch (failure.kind) {
case "INVALID_INPUT_FAILURE":
context.log.error(
`${logPrefix}|Error decoding input|ERROR=${failure.reason}`
);
break;
case "QUERY_FAILURE":
context.log.error(
`${logPrefix}|Error ${failure.query} query error |ERROR=${failure.reason}`
balanza marked this conversation as resolved.
Show resolved Hide resolved
);
break;
case "USER_NOT_FOUND_FAILURE":
context.log.error(`${logPrefix}|Error user not found |ERROR=`);
balanza marked this conversation as resolved.
Show resolved Hide resolved
break;
default:
assertNever(failure);
}
};

export const createExtractUserDataActivityHandler = (
messageModel: MessageModel,
notificationModel: NotificationModel,
profileModel: ProfileModel,
senderServiceModel: SenderServiceModel
) => (context: Context, input: unknown) => {
/**
* Look for a profile from a given fiscal code
* @param fiscalCode a fiscal code identifying the user
*
* @returns either a user profile, a query error or a user-not-found error
*/
const taskifiedFindProfile = (
fiscalCode: FiscalCode
): TaskEither<
ActivityResultUserNotFound | ActivityResultQueryFailure,
Profile
> =>
fromQueryEither(
() => profileModel.findOneProfileByFiscalCode(fiscalCode),
"findOneProfileByFiscalCode"
).foldTaskEither<
ActivityResultUserNotFound | ActivityResultQueryFailure,
Profile
>(
failure => fromEither(left(failure)),
maybeProfile =>
fromEither<ActivityResultUserNotFound, Profile>(
fromOption(
ActivityResultUserNotFound.encode({
kind: "USER_NOT_FOUND_FAILURE"
})
)(maybeProfile)
)
);

/**
* Perform all the queries to extract all data for a given user
* @param fiscalCode user identifier
*
* @returns Either a failure or a hash set with all the information regarding the user
*/
const queryAllUserData = (
fiscalCode: FiscalCode
): TaskEither<
ActivityResultUserNotFound | ActivityResultQueryFailure,
AllUserData
> =>
// queries the profile
taskifiedFindProfile(fiscalCode)
// on profile found, queries all other data sets
.chain(profile =>
sequenceS(taskEither)({
// queries all messages for the user
messages: fromQueryEither<ReadonlyArray<Message>>(
() => iteratorToArray(messageModel.findMessages(fiscalCode)),
"findMessages"
),
notifications: fromQueryEither<ReadonlyArray<Notification>>(
() =>
iteratorToArray(
notificationModel.findNotificationsForRecipient(fiscalCode)
),
"findNotificationsForRecipient"
),
// just previous profile data
profile: taskEither.of(profile),
// queries all services that sent a message to the user
senderServices: fromQueryEither<ReadonlyArray<SenderService>>(
() =>
iteratorToArray(
senderServiceModel.findSenderServicesForRecipient(fiscalCode)
),
"findSenderServicesForRecipient"
)
})
);

// the actual handler
return fromEither(ActivityInput.decode(input))
.mapLeft<ActivityResultFailure>((reason: t.Errors) =>
ActivityResultInvalidInputFailure.encode({
kind: "INVALID_INPUT_FAILURE",
reason: readableReport(reason)
})
)
.chain(({ fiscalCode }) => queryAllUserData(fiscalCode))
.map(allUserData =>
ActivityResultSuccess.encode({
kind: "SUCCESS",
value: allUserData
})
)
.mapLeft(failure => {
logFailure(context)(failure);
return failure;
})
.run();
};
Loading