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

[#IC-351] Wrap SDK Cosmos as functional (CRUD + Patch) #243

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@azure/cosmos": "^3.11.5",
"@azure/cosmos": "^3.15.1",
"@pagopa/ts-commons": "^10.0.1",
"applicationinsights": "^1.8.10",
"azure-storage": "^2.10.5",
Expand Down
276 changes: 276 additions & 0 deletions src/utils/__tests__/fp_cosmosdb_model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import * as t from "io-ts";

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

import * as M from "../fp_cosmosdb_model";

import { Container, ErrorResponse, ResourceResponse } from "@azure/cosmos";

import { NonEmptyString } from "@pagopa/ts-commons/lib/strings";

import { BaseModel, CosmosResource } from "../cosmosdb_model";
import { pipe } from "fp-ts/lib/function";

beforeEach(() => {
jest.resetAllMocks();
});

const MyDocument = t.interface({
pk: t.string,
test: t.string
});
type MyDocument = t.TypeOf<typeof MyDocument>;

const NewMyDocument = t.intersection([MyDocument, BaseModel]);
type NewMyDocument = t.TypeOf<typeof NewMyDocument>;

const RetrievedMyDocument = t.intersection([MyDocument, CosmosResource]);
type RetrievedMyDocument = t.TypeOf<typeof RetrievedMyDocument>;

const readMock = jest.fn();
const containerMock = {
item: jest.fn(),
items: {
create: jest.fn(),
upsert: jest.fn()
}
};
const container = (containerMock as unknown) as Container;

const myClient: M.ICosmosClient<
MyDocument,
NewMyDocument,
RetrievedMyDocument,
"id"
> = {
container: container,
newItemT: NewMyDocument,
retrievedItemT: RetrievedMyDocument,
partitionKeyT: "id"
};

const myPartitionedClient: M.ICosmosClient<
MyDocument,
NewMyDocument,
RetrievedMyDocument,
"pk"
> = {
container: container,
newItemT: NewMyDocument,
retrievedItemT: RetrievedMyDocument,
partitionKeyT: "pk"
};

const testId = "test-id-1" as NonEmptyString;
const testPartition = "test-partition";

const aDocument = {
id: testId,
pk: testPartition,
test: "test"
};

export const someMetadata = {
_etag: "_etag",
_rid: "_rid",
_self: "_self",
_ts: 1
};

const errorResponse: ErrorResponse = new Error();
// eslint-disable-next-line functional/immutable-data
errorResponse.code = 500;

describe("create", () => {
it("should create a document", async () => {
containerMock.items.create.mockResolvedValueOnce(
new ResourceResponse(
{
...aDocument,
...someMetadata
},
{},
200,
200
)
);
const result = await pipe(aDocument, M.create(myClient))();
expect(containerMock.items.create).toHaveBeenCalledWith(aDocument, {
disableAutomaticIdGeneration: true
});
expect(E.isRight(result));
if (E.isRight(result)) {
expect(result.right).toEqual({
...aDocument,
...someMetadata
});
}
});

it("should fail on query error", async () => {
containerMock.items.create.mockRejectedValueOnce(errorResponse);

const result = await pipe(aDocument, M.create(myClient))();
expect(E.isLeft(result));
if (E.isLeft(result)) {
expect(result.left.kind).toBe("COSMOS_ERROR_RESPONSE");
if (result.left.kind === "COSMOS_ERROR_RESPONSE") {
expect(result.left.error.code).toBe(500);
}
}
});

it("should fail on empty response", async () => {
containerMock.items.create.mockResolvedValueOnce({});

const result = await pipe(aDocument, M.create(myClient))();
expect(E.isLeft(result));
if (E.isLeft(result)) {
expect(result.left).toEqual({ kind: "COSMOS_EMPTY_RESPONSE" });
}
});
});

describe("upsert", () => {
it("should create a document", async () => {
containerMock.items.upsert.mockResolvedValueOnce({});
await pipe(aDocument, M.upsert(myClient))();
expect(containerMock.items.upsert).toHaveBeenCalledWith(aDocument, {
disableAutomaticIdGeneration: true
});
});

it("should fail on query error", async () => {
containerMock.items.upsert.mockRejectedValueOnce(errorResponse);
const result = await pipe(aDocument, M.upsert(myClient))();
expect(E.isLeft(result));
if (E.isLeft(result)) {
expect(result.left.kind).toBe("COSMOS_ERROR_RESPONSE");
if (result.left.kind === "COSMOS_ERROR_RESPONSE") {
expect(result.left.error.code).toBe(500);
}
}
});

it("should fail on empty response", async () => {
containerMock.items.upsert.mockResolvedValueOnce({});
const result = await pipe(aDocument, M.upsert(myClient))();
expect(E.isLeft(result));
if (E.isLeft(result)) {
expect(result.left).toEqual({ kind: "COSMOS_EMPTY_RESPONSE" });
}
});

it("should return the created document as retrieved type", async () => {
containerMock.items.upsert.mockResolvedValueOnce(
new ResourceResponse(
{
...aDocument,
...someMetadata
},
{},
200,
200
)
);
const result = await pipe(aDocument, M.upsert(myClient))();
expect(E.isRight(result));
if (E.isRight(result)) {
expect(result.right).toEqual({
...aDocument,
...someMetadata
});
}
});
});

// eslint-disable-next-line @typescript-eslint/interface-name-prefix

describe("find", () => {
it("should retrieve an existing document", async () => {
readMock.mockResolvedValueOnce(
new ResourceResponse(
{
...aDocument,
...someMetadata
},
{},
200,
200
)
);
containerMock.item.mockReturnValue({ read: readMock });

const result = await pipe([testId], M.find(myClient))();
expect(containerMock.item).toHaveBeenCalledWith(testId, testId);
expect(E.isRight(result)).toBeTruthy();
if (E.isRight(result)) {
expect(O.isSome(result.right)).toBeTruthy();
expect(O.toUndefined(result.right)).toEqual({
...aDocument,
...someMetadata
});
}
});

it("should retrieve an existing document for a model with a partition", async () => {
readMock.mockResolvedValueOnce(
new ResourceResponse(
{
...aDocument,
...someMetadata
},
{},
200,
200
)
);
containerMock.item.mockReturnValue({ read: readMock });

const result = await pipe(
[testId, testPartition],
M.find(myPartitionedClient)
)();
expect(containerMock.item).toHaveBeenCalledWith(testId, testPartition);
expect(E.isRight(result)).toBeTruthy();
if (E.isRight(result)) {
expect(O.isSome(result.right)).toBeTruthy();
expect(O.toUndefined(result.right)).toEqual({
...aDocument,
...someMetadata
});
}
});

it("should return an empty option if the document does not exist", async () => {
readMock.mockResolvedValueOnce(
// TODO: check whether this is what the client actually returns
new ResourceResponse(undefined, {}, 200, 200)
);
containerMock.item.mockReturnValue({ read: readMock });

const result = await pipe([testId], M.find(myClient))();
expect(E.isRight(result)).toBeTruthy();
if (E.isRight(result)) {
expect(O.isNone(result.right)).toBeTruthy();
}
});

it("should return the query error", async () => {
readMock.mockRejectedValueOnce(
// TODO: check whether this is what the client actually returns
errorResponse
);
containerMock.item.mockReturnValue({ read: readMock });

const result = await pipe([testId], M.find(myClient))();
expect(E.isLeft(result));
if (E.isLeft(result)) {
expect(result.left.kind).toBe("COSMOS_ERROR_RESPONSE");
if (result.left.kind === "COSMOS_ERROR_RESPONSE") {
expect(result.left.error.code).toBe(500);
}
}
});
});
57 changes: 57 additions & 0 deletions src/utils/__tests__/record.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { propertiesToArray } from "../record";

describe("propertiesToArray", () => {
it("no object input", async () => {
const input = "string";
const properties = propertiesToArray(input);
expect(properties).toEqual([]);
});

it("flat object", async () => {
const input = {
flat1: "1",
flat2: "2"
};

const properties = propertiesToArray(input);
expect(properties).toEqual([
{ key: "flat1", value: "1" },
{ key: "flat2", value: "2" }
]);
});
it("single nested object", async () => {
const input = {
flat: "flat",
nested: {
nested1: "1",
nested2: "2"
}
};

const properties = propertiesToArray(input);
expect(properties).toEqual([
{ key: "flat", value: "flat" },
{ key: "nested.nested1", value: "1" },
{ key: "nested.nested2", value: "2" }
]);
});

it("multi nested object", async () => {
const input = {
flat: "flat",
nested1: {
nested11: {
nested111: "1",
nested112: "2"
}
}
};

const properties = propertiesToArray(input);
expect(properties).toEqual([
{ key: "flat", value: "flat" },
{ key: "nested1.nested11.nested111", value: "1" },
{ key: "nested1.nested11.nested112", value: "2" }
]);
});
});
2 changes: 1 addition & 1 deletion src/utils/cosmosdb_model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const toCosmosErrorResponse = (
(e instanceof Error ? e : new Error(String(e))) as ErrorResponse
);

const wrapCreate = <TN, TR>(
export const wrapCreate = <TN, TR>(
newItemT: t.Type<TN, ItemDefinition, unknown>,
retrievedItemT: t.Type<TR, unknown, unknown>,
createItem: (
Expand Down
Loading