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

Refactor product orchestration and update mocks for product add and … #156

Merged
merged 2 commits into from
May 29, 2024
Merged
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
1 change: 1 addition & 0 deletions src/functions/customer-product.function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,5 @@ app.http("customerProductDestroy", {
authLevel: "anonymous",
route: "customer/{customerId?}/product/{productId?}",
handler: CustomerProductControllerDestroy,
extraInputs: [df.input.durableClient()],
});
2 changes: 1 addition & 1 deletion src/functions/customer-upload.orchestrators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ df.app.orchestration("upload", function* (context: OrchestrationContext) {
yield context.df.callActivity(
updateArticleName,
activityType<typeof updateArticle>({
user,
customerId: user.customerId,
})
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/functions/customer/controllers/product/add.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import {

require("~/library/jest/mongoose/mongodb.jest");

jest.mock("../../orchestrations/product/update", () => ({
CustomerProductUpdateOrchestration: () => ({
jest.mock("../../orchestrations/product/add", () => ({
CustomerProductAddOrchestration: () => ({
request: jest.fn(),
}),
}));
Expand Down
4 changes: 2 additions & 2 deletions src/functions/customer/controllers/product/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { HttpRequest, InvocationContext } from "@azure/functions";
import { ScheduleProductZodSchema } from "~/functions/schedule";
import { _ } from "~/library/handler";
import { GidFormat, StringOrObjectId } from "~/library/zod";
import { CustomerProductUpdateOrchestration } from "../../orchestrations/product/update";
import { CustomerProductAddOrchestration } from "../../orchestrations/product/add";
import { CustomerProductServiceAdd } from "../../services/product/add";

export type CustomerProductControllerAddRequest = {
Expand Down Expand Up @@ -53,7 +53,7 @@ export const CustomerProductControllerAdd = _(
validateBody
);

await CustomerProductUpdateOrchestration(
await CustomerProductAddOrchestration(
{ productId: product.productId, customerId: validateQuery.customerId },
context
);
Expand Down
18 changes: 3 additions & 15 deletions src/functions/customer/controllers/product/destroy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {

import { getProductObject } from "~/library/jest/helpers/product";
import { createScheduleWithProducts } from "~/library/jest/helpers/schedule";
import { shopifyAdmin } from "~/library/shopify";
import {
CustomerProductControllerDestroy,
CustomerProductControllerDestroyRequest,
Expand All @@ -18,14 +17,12 @@ import {

require("~/library/jest/mongoose/mongodb.jest");

jest.mock("~/library/shopify", () => ({
shopifyAdmin: jest.fn().mockReturnValue({
jest.mock("../../orchestrations/product/destroy", () => ({
CustomerProductDestroyOrchestration: () => ({
request: jest.fn(),
}),
}));

const mockRequest = shopifyAdmin().request as jest.Mock;

describe("CustomerProductControllerDestroy", () => {
let context: InvocationContext;
let request: HttpRequest;
Expand Down Expand Up @@ -53,21 +50,12 @@ describe("CustomerProductControllerDestroy", () => {
products: [product],
});

mockRequest.mockResolvedValueOnce({
data: {
productDelete: {
deletedProductId: {
id: "123",
},
},
},
});

request = await createHttpRequest<CustomerProductControllerDestroyRequest>({
query: {
customerId: newSchedule.customerId,
productId: product.productId,
},
context,
});

const res: HttpSuccessResponse<CustomerProductControllerDestroyResponse> =
Expand Down
12 changes: 11 additions & 1 deletion src/functions/customer/controllers/product/destroy.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { InvocationContext } from "@azure/functions";
import { z } from "zod";
import {
ScheduleProductZodSchema,
ScheduleZodSchema,
} from "~/functions/schedule/schedule.types";
import { _ } from "~/library/handler";
import { CustomerProductDestroyOrchestration } from "../../orchestrations/product/destroy";
import { CustomerProductServiceDestroy } from "../../services/product/destroy";
import { CustomerProductServiceGet } from "../../services/product/get";

export type CustomerProductControllerDestroyRequest = {
query: z.infer<typeof CustomerProductControllerDestroyQuerySchema>;
context: InvocationContext;
};

const CustomerProductControllerDestroyQuerySchema = z.object({
Expand All @@ -20,9 +24,15 @@ export type CustomerProductControllerDestroyResponse = Awaited<
>;

export const CustomerProductControllerDestroy = _(
({ query }: CustomerProductControllerDestroyRequest) => {
async ({ query, context }: CustomerProductControllerDestroyRequest) => {
const validateQuery =
CustomerProductControllerDestroyQuerySchema.parse(query);

// we must find the product before throwing it to durable function, since destroy may delete the product before
// we can get hold of the product.options in the durable activate functions.
const product = await CustomerProductServiceGet(validateQuery);
await CustomerProductDestroyOrchestration({ product }, context);

return CustomerProductServiceDestroy(validateQuery);
}
);
1 change: 0 additions & 1 deletion src/functions/customer/controllers/product/update.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ describe("CustomerProductControllerUpdate", () => {
description: "hej med dig",
},
context,
request,
});

const res: HttpSuccessResponse<CustomerProductControllerUpdateResponse> =
Expand Down
11 changes: 3 additions & 8 deletions src/functions/customer/controllers/product/update.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from "zod";
import { ScheduleProductZodSchema } from "~/functions/schedule/schedule.types";

import { HttpRequest, InvocationContext } from "@azure/functions";
import { InvocationContext } from "@azure/functions";
import { _ } from "~/library/handler";
import { GidFormat } from "~/library/zod";
import { CustomerProductUpdateOrchestration } from "../../orchestrations/product/update";
Expand All @@ -11,7 +11,6 @@ export type CustomerProductControllerUpdateRequest = {
query: z.infer<typeof CustomerProductControllerUpdateQuerySchema>;
body: z.infer<typeof CustomerProductControllerUpdateBodySchema>;
context: InvocationContext;
request: HttpRequest;
};

const CustomerProductControllerUpdateQuerySchema = z.object({
Expand All @@ -33,12 +32,7 @@ export type CustomerProductControllerUpdateResponse = Awaited<
>;

export const CustomerProductControllerUpdate = _(
async ({
query,
body,
request,
context,
}: CustomerProductControllerUpdateRequest) => {
async ({ query, body, context }: CustomerProductControllerUpdateRequest) => {
const validateQuery =
CustomerProductControllerUpdateQuerySchema.parse(query);
const validateBody = CustomerProductControllerUpdateBodySchema.parse(body);
Expand All @@ -49,6 +43,7 @@ export const CustomerProductControllerUpdate = _(
);

await CustomerProductUpdateOrchestration(validateQuery, context);

return product;
}
);
2 changes: 1 addition & 1 deletion src/functions/customer/orchestrations/customer/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const orchestrator: df.OrchestrationHandler = function* (
yield context.df.callActivity(
updateArticleName,
activityType<typeof updateArticle>({
user,
customerId: user.customerId,
})
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { CustomerServiceGet } from "~/functions/customer/services/customer/get";
import { CustomerLocationServiceList } from "~/functions/customer/services/location/list";
import { ScheduleModel } from "~/functions/schedule";
import { User } from "~/functions/user";
import { shopifyRest } from "~/library/shopify/rest";

export const updateArticleName = "updateArticle";
export const updateArticle = async ({
user,
customerId,
}: {
user: User;
customerId: number;
}): Promise<RootObject> => {
const user = await CustomerServiceGet({ customerId });

const schedules = await ScheduleModel.find({
customerId: user.customerId,
});
Expand Down
57 changes: 57 additions & 0 deletions src/functions/customer/orchestrations/product/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { InvocationContext } from "@azure/functions";
import * as df from "durable-functions";
import { OrchestrationContext } from "durable-functions";
import { activityType } from "~/library/orchestration";
import {
updateArticle,
updateArticleName,
} from "../customer/update/update-article";
import { updatePrice, updatePriceName } from "./update/update-price";
import { updateProduct, updateProductName } from "./update/update-product";

const orchestrator: df.OrchestrationHandler = function* (
context: OrchestrationContext
) {
const input = context.df.getInput() as Input;

const productUpdated: Awaited<ReturnType<typeof updateProduct>> =
yield context.df.callActivity(
updateProductName,
activityType<typeof updateProduct>(input)
);

const priceUpdated: Awaited<ReturnType<typeof updatePrice>> =
yield context.df.callActivity(
updatePriceName,
activityType<typeof updatePrice>(input)
);

const article: Awaited<ReturnType<typeof updateArticle>> =
yield context.df.callActivity(
updateArticleName,
activityType<typeof updateArticle>(input)
);

return { article, productUpdated, priceUpdated };
};

df.app.orchestration("CustomerProductAddOrchestration", orchestrator);

type Input = { productId: number; customerId: number };

export const CustomerProductAddOrchestration = async (
input: Input,
context: InvocationContext
): Promise<string> => {
const client = df.getClient(context);
const instanceId: string = await client.startNew(
"CustomerProductAddOrchestration",
{
input,
}
);

context.log(`Started orchestration with ID = '${instanceId}'.`);

return instanceId;
};
58 changes: 58 additions & 0 deletions src/functions/customer/orchestrations/product/destroy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { InvocationContext } from "@azure/functions";
import * as df from "durable-functions";
import { OrchestrationContext } from "durable-functions";
import { ScheduleProduct } from "~/functions/schedule";
import { activityType } from "~/library/orchestration";
import {
destroyProductOption,
destroyProductOptionName,
} from "../product-options/destroy/destroy-option";
import { destroyProduct, destroyProductName } from "./destroy/destroy-product";

df.app.activity(destroyProductName, { handler: destroyProduct });

const orchestrator: df.OrchestrationHandler = function* (
context: OrchestrationContext
) {
const input = context.df.getInput() as Input;

for (const productOption of input.product.options || []) {
yield context.df.callActivity(
destroyProductOptionName,
activityType<typeof destroyProductOption>({
productOptionId: productOption.productId,
})
);
}

const productDestroyed: Awaited<ReturnType<typeof destroyProduct>> =
yield context.df.callActivity(
destroyProductName,
activityType<typeof destroyProduct>(input.product)
);

return { productDestroyed };
};

df.app.orchestration("CustomerProductDestroyOrchestration", orchestrator);

type Input = {
product: ScheduleProduct;
};

export const CustomerProductDestroyOrchestration = async (
input: Input,
context: InvocationContext
): Promise<string> => {
const client = df.getClient(context);
const instanceId: string = await client.startNew(
"CustomerProductDestroyOrchestration",
{
input,
}
);

context.log(`Started orchestration with ID = '${instanceId}'.`);

return instanceId;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { shopifyAdmin } from "~/library/shopify";
import { ProductOptionDestroyMutation } from "~/types/admin.generated";
import { destroyProduct, PRODUCT_DESTROY } from "./destroy-product";

require("~/library/jest/mongoose/mongodb.jest");

jest.mock("~/library/shopify", () => ({
shopifyAdmin: jest.fn().mockReturnValue({
request: jest.fn(),
}),
}));

const mockRequest = shopifyAdmin().request as jest.Mock;

const productDelete: ProductOptionDestroyMutation = {
productDelete: {
deletedProductId: `gid://shopify/Product/123`,
},
};

describe("CustomerProductDestroyOrchestration", () => {
beforeAll(async () => {
jest.clearAllMocks();
});

it("destroyProduct", async () => {
mockRequest.mockResolvedValueOnce({
data: {
productDelete: {
deletedProductId: {
id: "123",
},
},
},
});

await destroyProduct({
productId: 123,
});

expect(mockRequest).toHaveBeenCalledTimes(1);
expect(mockRequest).toHaveBeenNthCalledWith(1, PRODUCT_DESTROY, {
variables: {
productId: `gid://shopify/Product/123`,
},
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { shopifyAdmin } from "~/library/shopify";

export const destroyProductName = "destroyProductName";
export const destroyProduct = async ({ productId }: { productId: number }) => {
const { data } = await shopifyAdmin().request(PRODUCT_DESTROY, {
variables: {
productId: `gid://shopify/Product/${productId}`,
},
});

return data?.productDelete?.deletedProductId;
};

export const PRODUCT_DESTROY = `#graphql
mutation productDestroy($productId: ID!) {
productDelete(input: {id: $productId}) {
deletedProductId
}
}
` as const;
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ describe("CustomerProductUpdateOrchestration", () => {
id: mockProductUpdate.productUpdate?.product?.id,
title: product.title,
descriptionHtml: product.descriptionHtml,
handle: product.productHandle,
metafields: [
{
id: product?.hideFromProfileMetafieldId,
Expand Down
Loading
Loading