Skip to content

Commit

Permalink
add unique email model
Browse files Browse the repository at this point in the history
  • Loading branch information
lucacavallaro committed Dec 4, 2023
1 parent fb48327 commit e990a4a
Show file tree
Hide file tree
Showing 7 changed files with 1,541 additions and 1,452 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,18 @@
"gulp-prettier": "^3.0.0",
"gulp-rename": "^2.0.0",
"gulp-text-simple": "^0.5.5",
"jest": "^24.9.0",
"jest": "^29.7.0",
"json-set-map": "^1.1.2",
"mjml": "^4.9.3",
"npm-run-all": "^4.1.1",
"prettier": "^1.14.3",
"rimraf": "^2.7.1",
"ts-jest": "^24.3.0",
"ts-jest": "^29.1.1",
"typescript": "^4.3.5"
},
"dependencies": {
"@azure/cosmos": "^3.17.1",
"@azure/data-tables": "^13.2.2",
"@pagopa/ts-commons": "^11.0.0",
"applicationinsights": "^1.8.10",
"azure-storage": "^2.10.5",
Expand Down
68 changes: 68 additions & 0 deletions src/utils/unique_email_enforcement/__test__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { TableEntityResult } from "@azure/data-tables";
import { describe, test, expect } from "@jest/globals";

import { EmailString, FiscalCode } from "@pagopa/ts-commons/lib/strings";

import { isEmailAlreadyTaken } from "../index";

const mocks = {
email: "citizen@email.test.pagopa.it" as EmailString
};

jest.mock("@azure/data-tables", () => ({
default: {
TableClient: jest.fn().mockImplementation(() => {
return {
listEntities: async function*() {
yield {
partitionKey: "citizen@email.test.pagopa.it",
rowKey: "AAAAAA00A00A000A"
};
yield {
partitionKey: "citizen@email.test.pagopa.it",
rowKey: "AAAAAA00A00A000A"
};
}
};
})
}
}));

function generateProfileEmails(count: number) {
return async function*(email: EmailString) {
for (let i = 0; i < count; i++) {
yield { email, fiscalCode: "X" as FiscalCode };
}
};
}

describe("isEmailAlreadyTaken", () => {
test.each([
{
entries: 0,
expected: false
},
{
entries: 1,
expected: false
},
{
entries: 100,
expected: true
},
{
entries: 2,
expected: true
}
])(
`${mocks.email} is used by $entries profiles, so isEmailAlreadyTaken should be $expected`,
({ entries, expected }) => {
const result = isEmailAlreadyTaken(mocks.email)({
profileEmailReader: {
profileEmails: generateProfileEmails(entries)
}
});
expect(result).resolves.toBe(expected);
}
);
});
28 changes: 28 additions & 0 deletions src/utils/unique_email_enforcement/__test__/storage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, it, expect } from "@jest/globals";

import { TableEntityResult } from "@azure/data-tables";
import { toProfileEmailsAsyncIterator } from "../storage";

describe("toProfileEmailsAsyncIterator", () => {
it("emits only valid entities", async () => {
async function* listEntities(): AsyncIterableIterator<
TableEntityResult<unknown>
> {
yield {
partitionKey: "citizen@email.test.pagopa.it",
rowKey: "AAAAAA00A00A000A",
etag: "etag"
};
yield {
partitionKey: "not-a-valid-email",
rowKey: "AAAAAA00A00A000A",
etag: "etag"
};
}
let count = 0;
for await (const _ of toProfileEmailsAsyncIterator(listEntities())) {
count++;
}
expect(count).toBe(1);
});
});
38 changes: 38 additions & 0 deletions src/utils/unique_email_enforcement/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { EmailString, FiscalCode } from "@pagopa/ts-commons/lib/strings";

import * as t from "io-ts";

export const ProfileEmail = t.type({
email: EmailString,
fiscalCode: FiscalCode
});

export type ProfileEmail = t.TypeOf<typeof ProfileEmail>;

export interface ProfileEmailReader {
profileEmails(email: EmailString): AsyncIterableIterator<ProfileEmail>;
}

export interface ProfileEmailWriter {
delete(profileEmail: ProfileEmail): Promise<void>;
insert(profileEmail: ProfileEmail): Promise<void>;
}

interface IsEmailAlreadyTakenDependencies {
readonly profileEmailReader: ProfileEmailReader;
}

// Checks if the given e-mail is already taken
// profileEmails returns all the ProfileEmail records that shares
// the same e-mail. If count(records) > 1 then the e-mail is already taken.
export const isEmailAlreadyTaken = (email: EmailString) => async ({
profileEmailReader: { profileEmails }
}: IsEmailAlreadyTakenDependencies): Promise<boolean> => {
let count = 0;
for await (const _ of profileEmails(email)) {
if (++count > 1) {
return true;
}
}
return false;
};
58 changes: 58 additions & 0 deletions src/utils/unique_email_enforcement/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { TableClient, odata, TableEntityResult } from "@azure/data-tables";
import { EmailString } from "@pagopa/ts-commons/lib/strings";

import { ProfileEmail, ProfileEmailReader, ProfileEmailWriter } from "./index";

// Generates AsyncIterable<ProfileEmail> from AsyncIterable<TableEntityResult>
export async function* toProfileEmailsAsyncIterator(
iterator: AsyncIterableIterator<TableEntityResult<unknown>>
): AsyncIterableIterator<ProfileEmail> {
for await (const item of iterator) {
const uniqueEmail = ProfileEmail.decode({
email: item.partitionKey,
fiscalCode: item.rowKey
});
if (uniqueEmail._tag === "Right") {
yield uniqueEmail.right;
}
}
}

export class DataTableProfileEmailsRepository
implements ProfileEmailReader, ProfileEmailWriter {
constructor(private tableClient: TableClient) {}

// Generates an AsyncIterable<ProfileEmail>
async *profileEmails(email: EmailString) {
return toProfileEmailsAsyncIterator(
this.tableClient.listEntities({
queryOptions: {
filter: odata`partitionKey eq ${email}`
}
})
);
}

async insert(p: ProfileEmail) {
try {
await this.tableClient.createEntity({
partitionKey: p.email,
rowKey: p.fiscalCode
});
} catch (cause) {
throw new Error("error inserting ProfileEmail into table storage", {
cause
});
}
}

async delete(p: ProfileEmail) {
try {
await this.tableClient.deleteEntity(p.email, p.fiscalCode);
} catch (cause) {
throw new Error("error deleting ProfileEmail from table storage", {
cause
});
}
}
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2018",
"target": "es2022",
"outDir": "dist",
"module": "commonjs",
"moduleResolution": "node",
Expand Down
Loading

0 comments on commit e990a4a

Please sign in to comment.