From 9e65219e11e12473b7ac7c48ef57c82ede7668c4 Mon Sep 17 00:00:00 2001 From: Jamal Soueidan Date: Tue, 12 Mar 2024 17:45:33 +0100 Subject: [PATCH 01/10] Add webhook processing for product updates with Azure functions --- src/functions/webhook-product.function.ts | 47 +++++++++++++++++++ src/functions/webhook/product/product.dumb.ts | 47 +++++++++++++++++++ src/functions/webhook/product/product.spec.ts | 21 +++++++++ src/functions/webhook/product/product.ts | 37 +++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 src/functions/webhook-product.function.ts create mode 100644 src/functions/webhook/product/product.dumb.ts create mode 100644 src/functions/webhook/product/product.spec.ts create mode 100644 src/functions/webhook/product/product.ts diff --git a/src/functions/webhook-product.function.ts b/src/functions/webhook-product.function.ts new file mode 100644 index 00000000..a6041eef --- /dev/null +++ b/src/functions/webhook-product.function.ts @@ -0,0 +1,47 @@ +import "@shopify/shopify-api/adapters/node"; +import "module-alias/register"; + +import { + app, + HttpRequest, + HttpResponseInit, + InvocationContext, + output, +} from "@azure/functions"; +import { + productUpdateSchema, + webhookProductProcess, +} from "./webhook/product/product"; + +export const productQueueName = "webhook-product"; +export const productQueueOutput = output.storageQueue({ + queueName: productQueueName, + connection: "QueueStorage", +}); + +app.storageQueue("webhookProductUpdateProcess", { + queueName: productQueueName, + connection: "QueueStorage", + handler: webhookProductProcess, +}); + +export async function webhookProduct( + request: HttpRequest, + context: InvocationContext +): Promise { + const body = await request.json(); + const parser = productUpdateSchema.safeParse(body); + if (parser.success) { + context.extraOutputs.set(productQueueOutput, parser.data); + context.log(`Started storageQueue with ID = '${productQueueName}'.`); + } + return { body: "Created queue item." }; +} + +app.http("webhookProductUpdate", { + methods: ["POST"], + authLevel: "anonymous", + route: "webhooks/product", + extraOutputs: [productQueueOutput], + handler: webhookProduct, +}); diff --git a/src/functions/webhook/product/product.dumb.ts b/src/functions/webhook/product/product.dumb.ts new file mode 100644 index 00000000..a633b816 --- /dev/null +++ b/src/functions/webhook/product/product.dumb.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; +import { productUpdateSchema } from "./product"; + +export const productDumbData: z.infer = { + admin_graphql_api_id: "gid://shopify/Product/8022088646930", + handle: "borneklip-fra-6-ar", + id: 8022088646930, + title: "Børneklip (fra 6 år)", + variants: [ + { + admin_graphql_api_id: "gid://shopify/ProductVariant/46718525899079", + id: 46718525899079, + }, + { + admin_graphql_api_id: "gid://shopify/ProductVariant/46718525931847", + id: 46718525931847, + }, + { + admin_graphql_api_id: "gid://shopify/ProductVariant/46718525964615", + id: 46718525964615, + }, + { + admin_graphql_api_id: "gid://shopify/ProductVariant/46727191036231", + id: 46727191036231, + }, + { + admin_graphql_api_id: "gid://shopify/ProductVariant/49207559356743", + id: 49207559356743, + }, + { + admin_graphql_api_id: "gid://shopify/ProductVariant/49210092323143", + id: 49210092323143, + }, + { + admin_graphql_api_id: "gid://shopify/ProductVariant/49216099909959", + id: 49216099909959, + }, + { + admin_graphql_api_id: "gid://shopify/ProductVariant/49216108167495", + id: 49216108167495, + }, + { + admin_graphql_api_id: "gid://shopify/ProductVariant/49221362712903", + id: 49221362712903, + }, + ], +}; diff --git a/src/functions/webhook/product/product.spec.ts b/src/functions/webhook/product/product.spec.ts new file mode 100644 index 00000000..bba4c06f --- /dev/null +++ b/src/functions/webhook/product/product.spec.ts @@ -0,0 +1,21 @@ +import { InvocationContext } from "@azure/functions"; +import { createContext } from "~/library/jest/azure"; +import { webhookProductProcess } from "./product"; +import { productDumbData } from "./product.dumb"; + +require("~/library/jest/mongoose/mongodb.jest"); + +jest.mock("~/library/application-insight", () => ({ + telemetryClient: { + trackException: jest.fn(), + }, +})); + +describe("webhookOrderProcess", () => { + let context: InvocationContext = createContext(); + + it("Should be able to add body to order and update it", async () => { + let res = await webhookProductProcess(productDumbData, context); + console.log(res); + }); +}); diff --git a/src/functions/webhook/product/product.ts b/src/functions/webhook/product/product.ts new file mode 100644 index 00000000..d8529c21 --- /dev/null +++ b/src/functions/webhook/product/product.ts @@ -0,0 +1,37 @@ +import { InvocationContext } from "@azure/functions"; +import { z } from "zod"; + +import { telemetryClient } from "~/library/application-insight"; +import { connect } from "~/library/mongoose"; + +export const variantSchema = z.object({ + admin_graphql_api_id: z.string(), + id: z.number(), +}); + +export const productUpdateSchema = z.object({ + admin_graphql_api_id: z.string(), + handle: z.string(), + id: z.number(), + title: z.string(), + variants: z.array(variantSchema), +}); + +export async function webhookProductProcess( + queueItem: unknown, + context: InvocationContext +) { + try { + await connect(); + console.log(queueItem); + context.log("webhook product success"); + } catch (exception: unknown) { + telemetryClient.trackException({ + exception: exception as Error, + }); + context.error( + `webhook order error ${(queueItem as any).order_id}`, + exception + ); + } +} From 21ae0e719b0b63a800b66af0bea8b4a2b81bac86 Mon Sep 17 00:00:00 2001 From: Jamal Soueidan Date: Tue, 12 Mar 2024 20:46:49 +0100 Subject: [PATCH 02/10] Refactor product webhook and types, remove product spec, add unused variants logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Refactored product webhook logic to use ProductUpdateSchema from new types.ts. • Removed product.spec.ts test file. • Added unused.ts to handle retrieval of unused variant IDs. • Updated product.ts to delete unused variants using new GraphQL mutation. --- src/functions/webhook/product/product.dumb.ts | 7 +- src/functions/webhook/product/product.spec.ts | 21 ---- src/functions/webhook/product/product.ts | 53 ++++++--- src/functions/webhook/product/types.ts | 16 +++ src/functions/webhook/product/unused.spec.ts | 87 +++++++++++++++ src/functions/webhook/product/unused.ts | 42 +++++++ src/types/admin.generated.d.ts | 104 +++++------------- 7 files changed, 216 insertions(+), 114 deletions(-) delete mode 100644 src/functions/webhook/product/product.spec.ts create mode 100644 src/functions/webhook/product/types.ts create mode 100644 src/functions/webhook/product/unused.spec.ts create mode 100644 src/functions/webhook/product/unused.ts diff --git a/src/functions/webhook/product/product.dumb.ts b/src/functions/webhook/product/product.dumb.ts index a633b816..41683c93 100644 --- a/src/functions/webhook/product/product.dumb.ts +++ b/src/functions/webhook/product/product.dumb.ts @@ -1,8 +1,7 @@ -import { z } from "zod"; -import { productUpdateSchema } from "./product"; +import { ProductUpdateSchema } from "./types"; -export const productDumbData: z.infer = { - admin_graphql_api_id: "gid://shopify/Product/8022088646930", +export const productDumbData: ProductUpdateSchema = { + admin_graphql_api_id: "gid://shopify/Product/802208864693", handle: "borneklip-fra-6-ar", id: 8022088646930, title: "Børneklip (fra 6 år)", diff --git a/src/functions/webhook/product/product.spec.ts b/src/functions/webhook/product/product.spec.ts deleted file mode 100644 index bba4c06f..00000000 --- a/src/functions/webhook/product/product.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { InvocationContext } from "@azure/functions"; -import { createContext } from "~/library/jest/azure"; -import { webhookProductProcess } from "./product"; -import { productDumbData } from "./product.dumb"; - -require("~/library/jest/mongoose/mongodb.jest"); - -jest.mock("~/library/application-insight", () => ({ - telemetryClient: { - trackException: jest.fn(), - }, -})); - -describe("webhookOrderProcess", () => { - let context: InvocationContext = createContext(); - - it("Should be able to add body to order and update it", async () => { - let res = await webhookProductProcess(productDumbData, context); - console.log(res); - }); -}); diff --git a/src/functions/webhook/product/product.ts b/src/functions/webhook/product/product.ts index d8529c21..d521680c 100644 --- a/src/functions/webhook/product/product.ts +++ b/src/functions/webhook/product/product.ts @@ -1,21 +1,10 @@ import { InvocationContext } from "@azure/functions"; -import { z } from "zod"; import { telemetryClient } from "~/library/application-insight"; import { connect } from "~/library/mongoose"; - -export const variantSchema = z.object({ - admin_graphql_api_id: z.string(), - id: z.number(), -}); - -export const productUpdateSchema = z.object({ - admin_graphql_api_id: z.string(), - handle: z.string(), - id: z.number(), - title: z.string(), - variants: z.array(variantSchema), -}); +import { shopifyAdmin } from "~/library/shopify"; +import { ProductUpdateSchema } from "./types"; +import { ProductWebHookGetUnusedVariantIds } from "./unused"; export async function webhookProductProcess( queueItem: unknown, @@ -23,9 +12,27 @@ export async function webhookProductProcess( ) { try { await connect(); - console.log(queueItem); + const product = queueItem as ProductUpdateSchema; + const unusedVariantIds = await ProductWebHookGetUnusedVariantIds({ + product, + }); + + const response = await shopifyAdmin.query({ + data: { + query: MUTATION_DESTROY_VARIANTS, + variables: { + productId: product.admin_graphql_api_id, + variantsIds: unusedVariantIds.map( + (l) => `gid://shopify/ProductVariant/${l}` + ), + }, + }, + }); + + console.log(response); context.log("webhook product success"); } catch (exception: unknown) { + console.log(exception); telemetryClient.trackException({ exception: exception as Error, }); @@ -35,3 +42,19 @@ export async function webhookProductProcess( ); } } + +const MUTATION_DESTROY_VARIANTS = `#graphql + mutation productVariantsBulkDelete($productId: ID!, $variantsIds: [ID!]!) { + productVariantsBulkDelete(productId: $productId, variantsIds: $variantsIds) { + product { + id + title + } + userErrors { + code + field + message + } + } + } +` as const; diff --git a/src/functions/webhook/product/types.ts b/src/functions/webhook/product/types.ts new file mode 100644 index 00000000..0d05c899 --- /dev/null +++ b/src/functions/webhook/product/types.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +export const variantSchema = z.object({ + admin_graphql_api_id: z.string(), + id: z.number(), +}); + +export const productUpdateSchema = z.object({ + admin_graphql_api_id: z.string(), + handle: z.string(), + id: z.number(), + title: z.string(), + variants: z.array(variantSchema), +}); + +export type ProductUpdateSchema = z.infer; diff --git a/src/functions/webhook/product/unused.spec.ts b/src/functions/webhook/product/unused.spec.ts new file mode 100644 index 00000000..49286cb9 --- /dev/null +++ b/src/functions/webhook/product/unused.spec.ts @@ -0,0 +1,87 @@ +import { InvocationContext } from "@azure/functions"; +import { Schedule, ScheduleModel } from "~/functions/schedule"; +import { createContext } from "~/library/jest/azure"; +import { getProductObject } from "~/library/jest/helpers/product"; +import { productDumbData } from "./product.dumb"; +import { ProductWebHookGetUnusedVariantIds } from "./unused"; + +require("~/library/jest/mongoose/mongodb.jest"); + +jest.mock("~/library/application-insight", () => ({ + telemetryClient: { + trackException: jest.fn(), + }, +})); + +describe("webhookUpdateProcess", () => { + let context: InvocationContext = createContext(); + const schedule3: Omit = { + name: "schedula b", + customerId: 1, + slots: [ + { + day: "monday", + intervals: [ + { + from: "15:00", + to: "20:00", + }, + ], + }, + ], + products: [ + getProductObject({ + productId: productDumbData.id, //correct + variantId: productDumbData.variants[0].id, //correct + }), + getProductObject({ + productId: 1, + variantId: 49207559356743, + }), + ], + }; + + const schedule4: Omit = { + name: "schedule a", + customerId: 2, + slots: [ + { + day: "saturday", + intervals: [ + { + from: "17:00", + to: "20:00", + }, + ], + }, + ], + products: [ + getProductObject({ + productId: 1, + variantId: 1, + }), + getProductObject({ + productId: productDumbData.id, //correct + variantId: productDumbData.variants[1].id, //correct + }), + ], + }; + + beforeEach(async () => { + await ScheduleModel.create(schedule3); + await ScheduleModel.create(schedule4); + }); + + it("'should confirm that specific variant IDs are not in the array'", async () => { + let unusedVariantIds = await ProductWebHookGetUnusedVariantIds({ + product: productDumbData, + }); + const variantIdsToCheck = [ + productDumbData.variants[0].id, + productDumbData.variants[1].id, + ]; + variantIdsToCheck.forEach((id) => { + expect(unusedVariantIds).not.toContain(id); + }); + }); +}); diff --git a/src/functions/webhook/product/unused.ts b/src/functions/webhook/product/unused.ts new file mode 100644 index 00000000..e4831f9d --- /dev/null +++ b/src/functions/webhook/product/unused.ts @@ -0,0 +1,42 @@ +import { ScheduleModel } from "~/functions/schedule"; +import { ProductUpdateSchema } from "./types"; + +export async function ProductWebHookGetUnusedVariantIds({ + product, +}: { + product: ProductUpdateSchema; +}) { + let unusedVariantIds = product.variants.map((variant) => variant.id); + + const results = await ScheduleModel.aggregate([ + { + $match: { + "products.productId": product.id, + }, + }, + { + $unwind: "$products", + }, + { + $match: { + "products.productId": product.id, + "products.variantId": { $in: unusedVariantIds }, + }, + }, + { + $group: { + _id: "$products.productId", + variantIds: { $addToSet: "$products.variantId" }, + }, + }, + ]); + + if (results.length > 0) { + const variantIds = results[0].variantIds; + unusedVariantIds = unusedVariantIds.filter( + (id) => !variantIds.includes(id) + ); + } + + return unusedVariantIds; +} diff --git a/src/types/admin.generated.d.ts b/src/types/admin.generated.d.ts index 203759a0..2ca84b6d 100644 --- a/src/types/admin.generated.d.ts +++ b/src/types/admin.generated.d.ts @@ -1,105 +1,61 @@ /* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable eslint-comments/no-unlimited-disable */ /* eslint-disable */ -import * as AdminTypes from "./admin.types"; +import * as AdminTypes from './admin.types'; export type ProductVariantCreateMutationVariables = AdminTypes.Exact<{ input: AdminTypes.ProductVariantInput; }>; -export type ProductVariantCreateMutation = { - productVariantCreate?: AdminTypes.Maybe<{ - product?: AdminTypes.Maybe>; - productVariant?: AdminTypes.Maybe< - Pick< - AdminTypes.ProductVariant, - | "createdAt" - | "displayName" - | "id" - | "inventoryPolicy" - | "inventoryQuantity" - | "price" - | "title" - > & { product: Pick } - >; - userErrors: Array>; - }>; -}; + +export type ProductVariantCreateMutation = { productVariantCreate?: AdminTypes.Maybe<{ product?: AdminTypes.Maybe>, productVariant?: AdminTypes.Maybe<( + Pick + & { product: Pick } + )>, userErrors: Array> }> }; export type FileCreateMutationVariables = AdminTypes.Exact<{ files: Array | AdminTypes.FileCreateInput; }>; -export type FileCreateMutation = { - fileCreate?: AdminTypes.Maybe<{ - files?: AdminTypes.Maybe< - Array< - | Pick - | Pick - | Pick - > - >; - userErrors: Array>; - }>; -}; + +export type FileCreateMutation = { fileCreate?: AdminTypes.Maybe<{ files?: AdminTypes.Maybe | Pick | Pick>>, userErrors: Array> }> }; export type FileGetQueryVariables = AdminTypes.Exact<{ - query: AdminTypes.Scalars["String"]["input"]; + query: AdminTypes.Scalars['String']['input']; }>; -export type FileGetQuery = { - files: { - nodes: Array<{ - preview?: AdminTypes.Maybe<{ - image?: AdminTypes.Maybe< - Pick - >; - }>; - }>; - }; -}; + +export type FileGetQuery = { files: { nodes: Array<{ preview?: AdminTypes.Maybe<{ image?: AdminTypes.Maybe> }> }> } }; export type StagedUploadsCreateMutationVariables = AdminTypes.Exact<{ input: Array | AdminTypes.StagedUploadInput; }>; -export type StagedUploadsCreateMutation = { - stagedUploadsCreate?: AdminTypes.Maybe<{ - stagedTargets?: AdminTypes.Maybe< - Array< - Pick & { - parameters: Array< - Pick - >; - } - > - >; - userErrors: Array>; - }>; -}; + +export type StagedUploadsCreateMutation = { stagedUploadsCreate?: AdminTypes.Maybe<{ stagedTargets?: AdminTypes.Maybe + & { parameters: Array> } + )>>, userErrors: Array> }> }; + +export type ProductVariantsBulkDeleteMutationVariables = AdminTypes.Exact<{ + productId: AdminTypes.Scalars['ID']['input']; + variantsIds: Array | AdminTypes.Scalars['ID']['input']; +}>; + + +export type ProductVariantsBulkDeleteMutation = { productVariantsBulkDelete?: AdminTypes.Maybe<{ product?: AdminTypes.Maybe>, userErrors: Array> }> }; interface GeneratedQueryTypes { - "#graphql\n query FileGet($query: String!) {\n files(first: 10, sortKey: UPDATED_AT, reverse: true, query: $query) {\n nodes {\n preview {\n image {\n url\n width\n height\n }\n }\n }\n }\n }\n": { - return: FileGetQuery; - variables: FileGetQueryVariables; - }; + "#graphql\n query FileGet($query: String!) {\n files(first: 10, sortKey: UPDATED_AT, reverse: true, query: $query) {\n nodes {\n preview {\n image {\n url\n width\n height\n }\n }\n }\n }\n }\n": {return: FileGetQuery, variables: FileGetQueryVariables}, } interface GeneratedMutationTypes { - "#graphql\n mutation productVariantCreate($input: ProductVariantInput!) {\n productVariantCreate(input: $input) {\n product {\n id\n title\n }\n productVariant {\n createdAt\n displayName\n id\n inventoryPolicy\n inventoryQuantity\n price\n product {\n id\n }\n title\n }\n userErrors {\n field\n message\n }\n }\n }\n": { - return: ProductVariantCreateMutation; - variables: ProductVariantCreateMutationVariables; - }; - "#graphql\n mutation fileCreate($files: [FileCreateInput!]!) {\n fileCreate(files: $files) {\n files {\n fileStatus\n alt\n }\n userErrors {\n field\n message\n }\n }\n }\n": { - return: FileCreateMutation; - variables: FileCreateMutationVariables; - }; - "#graphql\n mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {\n stagedUploadsCreate(input: $input) {\n stagedTargets {\n resourceUrl\n url\n parameters {\n name\n value\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n": { - return: StagedUploadsCreateMutation; - variables: StagedUploadsCreateMutationVariables; - }; + "#graphql\n mutation productVariantCreate($input: ProductVariantInput!) {\n productVariantCreate(input: $input) {\n product {\n id\n title\n }\n productVariant {\n createdAt\n displayName\n id\n inventoryPolicy\n inventoryQuantity\n price\n product {\n id\n }\n title\n }\n userErrors {\n field\n message\n }\n }\n }\n": {return: ProductVariantCreateMutation, variables: ProductVariantCreateMutationVariables}, + "#graphql\n mutation fileCreate($files: [FileCreateInput!]!) {\n fileCreate(files: $files) {\n files {\n fileStatus\n alt\n }\n userErrors {\n field\n message\n }\n }\n }\n": {return: FileCreateMutation, variables: FileCreateMutationVariables}, + "#graphql\n mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {\n stagedUploadsCreate(input: $input) {\n stagedTargets {\n resourceUrl\n url\n parameters {\n name\n value\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n": {return: StagedUploadsCreateMutation, variables: StagedUploadsCreateMutationVariables}, + "#graphql\n mutation productVariantsBulkDelete($productId: ID!, $variantsIds: [ID!]!) {\n productVariantsBulkDelete(productId: $productId, variantsIds: $variantsIds) {\n product {\n id\n title\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n": {return: ProductVariantsBulkDeleteMutation, variables: ProductVariantsBulkDeleteMutationVariables}, } -declare module "@shopify/admin-api-client" { +declare module '@shopify/admin-api-client' { type InputMaybe = AdminTypes.InputMaybe; interface AdminQueries extends GeneratedQueryTypes {} interface AdminMutations extends GeneratedMutationTypes {} From 0c5c646028c0c47d21288a5dcd5e12c48e195b4d Mon Sep 17 00:00:00 2001 From: Jamal Soueidan Date: Tue, 12 Mar 2024 20:47:11 +0100 Subject: [PATCH 03/10] Add index to variantId in ProductSchema --- src/functions/schedule/schemas/product.schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/functions/schedule/schemas/product.schema.ts b/src/functions/schedule/schemas/product.schema.ts index 631c40b8..dd51322e 100644 --- a/src/functions/schedule/schemas/product.schema.ts +++ b/src/functions/schedule/schemas/product.schema.ts @@ -14,6 +14,7 @@ export const ProductSchema = new mongoose.Schema( }, variantId: { type: Number, + index: true, }, selectedOptions: { name: String, From db63681e6432feb4ac8d8074e15643f98e10f4da Mon Sep 17 00:00:00 2001 From: Jamal Soueidan Date: Tue, 12 Mar 2024 21:15:54 +0100 Subject: [PATCH 04/10] Refactor imports and improve error handling in webhookProductProcess function --- src/functions/webhook-product.function.ts | 6 ++---- src/functions/webhook/product/product.ts | 11 +++++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/functions/webhook-product.function.ts b/src/functions/webhook-product.function.ts index a6041eef..eb49ed53 100644 --- a/src/functions/webhook-product.function.ts +++ b/src/functions/webhook-product.function.ts @@ -8,10 +8,8 @@ import { InvocationContext, output, } from "@azure/functions"; -import { - productUpdateSchema, - webhookProductProcess, -} from "./webhook/product/product"; +import { webhookProductProcess } from "./webhook/product/product"; +import { productUpdateSchema } from "./webhook/product/types"; export const productQueueName = "webhook-product"; export const productQueueOutput = output.storageQueue({ diff --git a/src/functions/webhook/product/product.ts b/src/functions/webhook/product/product.ts index d521680c..587c7ac3 100644 --- a/src/functions/webhook/product/product.ts +++ b/src/functions/webhook/product/product.ts @@ -3,6 +3,7 @@ import { InvocationContext } from "@azure/functions"; import { telemetryClient } from "~/library/application-insight"; import { connect } from "~/library/mongoose"; import { shopifyAdmin } from "~/library/shopify"; +import { ProductVariantCreateMutation } from "~/types/admin.generated"; import { ProductUpdateSchema } from "./types"; import { ProductWebHookGetUnusedVariantIds } from "./unused"; @@ -17,7 +18,7 @@ export async function webhookProductProcess( product, }); - const response = await shopifyAdmin.query({ + const { body } = await shopifyAdmin.query({ data: { query: MUTATION_DESTROY_VARIANTS, variables: { @@ -29,7 +30,13 @@ export async function webhookProductProcess( }, }); - console.log(response); + if (!body.productVariantCreate?.product) { + context.error( + "webhook product error", + body.productVariantCreate?.userErrors + ); + } + context.log("webhook product success"); } catch (exception: unknown) { console.log(exception); From 49736a3f933bdcd91038e923d8d324349562c6bd Mon Sep 17 00:00:00 2001 From: Jamal Soueidan Date: Tue, 12 Mar 2024 22:53:02 +0100 Subject: [PATCH 05/10] Refactor product service imports to use new directory structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Move product-related service functions into separate directories • Update import paths in various files to reflect new locations • Remove product.ts and product.spec.ts as their contents are now in individual files • Add new tests for destroy, get, list-ids, list, remove-location-from-all, and upsert functions within the product service • Adjust user service product-related imports to match new product service structure --- .../controllers/product/destroy.spec.ts | 2 +- .../customer/controllers/product/destroy.ts | 2 +- .../customer/controllers/product/get.spec.ts | 2 +- .../customer/controllers/product/get.ts | 2 +- .../customer/controllers/product/upsert.ts | 2 +- .../controllers/products/list-ids.spec.ts | 2 +- .../customer/controllers/products/list-ids.ts | 2 +- .../controllers/products/list.spec.ts | 2 +- .../customer/controllers/products/list.ts | 2 +- src/functions/customer/services/location.ts | 2 +- .../customer/services/product.spec.ts | 412 ------------------ src/functions/customer/services/product.ts | 187 -------- .../customer/services/product/destroy.spec.ts | 49 +++ .../customer/services/product/destroy.ts | 27 ++ .../customer/services/product/get.spec.ts | 49 +++ .../customer/services/product/get.ts | 50 +++ .../services/product/list-ids.spec.ts | 109 +++++ .../customer/services/product/list-ids.ts | 22 + .../customer/services/product/list.spec.ts | 91 ++++ .../customer/services/product/list.ts | 28 ++ .../product/remove-location-from-all.spec.ts | 156 +++++++ .../product/remove-location-from-all.ts | 19 + .../customer/services/product/upsert.spec.ts | 86 ++++ .../customer/services/product/upsert.ts | 53 +++ .../user/controllers/products/get.spec.ts | 2 +- .../products/list-by-schedule.spec.ts | 2 +- .../controllers/products/list-by-schedule.ts | 2 +- .../user/services/products/get.spec.ts | 2 +- 28 files changed, 753 insertions(+), 613 deletions(-) delete mode 100644 src/functions/customer/services/product.spec.ts delete mode 100644 src/functions/customer/services/product.ts create mode 100644 src/functions/customer/services/product/destroy.spec.ts create mode 100644 src/functions/customer/services/product/destroy.ts create mode 100644 src/functions/customer/services/product/get.spec.ts create mode 100644 src/functions/customer/services/product/get.ts create mode 100644 src/functions/customer/services/product/list-ids.spec.ts create mode 100644 src/functions/customer/services/product/list-ids.ts create mode 100644 src/functions/customer/services/product/list.spec.ts create mode 100644 src/functions/customer/services/product/list.ts create mode 100644 src/functions/customer/services/product/remove-location-from-all.spec.ts create mode 100644 src/functions/customer/services/product/remove-location-from-all.ts create mode 100644 src/functions/customer/services/product/upsert.spec.ts create mode 100644 src/functions/customer/services/product/upsert.ts diff --git a/src/functions/customer/controllers/product/destroy.spec.ts b/src/functions/customer/controllers/product/destroy.spec.ts index 4305538b..fc1f0fe9 100644 --- a/src/functions/customer/controllers/product/destroy.spec.ts +++ b/src/functions/customer/controllers/product/destroy.spec.ts @@ -9,7 +9,7 @@ import { import { omitObjectIdProps } from "~/library/jest/helpers"; import { getProductObject } from "~/library/jest/helpers/product"; -import { CustomerProductServiceUpsert } from "../../services/product"; +import { CustomerProductServiceUpsert } from "../../services/product/upsert"; import { CustomerScheduleServiceCreate } from "../../services/schedule/create"; import { CustomerProductControllerDestroy, diff --git a/src/functions/customer/controllers/product/destroy.ts b/src/functions/customer/controllers/product/destroy.ts index e6fe4d11..99af4edb 100644 --- a/src/functions/customer/controllers/product/destroy.ts +++ b/src/functions/customer/controllers/product/destroy.ts @@ -4,7 +4,7 @@ import { ScheduleZodSchema, } from "~/functions/schedule/schedule.types"; import { _ } from "~/library/handler"; -import { CustomerProductServiceDestroy } from "../../services/product"; +import { CustomerProductServiceDestroy } from "../../services/product/destroy"; export type CustomerProductControllerDestroyRequest = { query: z.infer; diff --git a/src/functions/customer/controllers/product/get.spec.ts b/src/functions/customer/controllers/product/get.spec.ts index d99d4229..a6fd7bfb 100644 --- a/src/functions/customer/controllers/product/get.spec.ts +++ b/src/functions/customer/controllers/product/get.spec.ts @@ -8,7 +8,7 @@ import { createHttpRequest, } from "~/library/jest/azure"; import { getProductObject } from "~/library/jest/helpers/product"; -import { CustomerProductServiceUpsert } from "../../services/product"; +import { CustomerProductServiceUpsert } from "../../services/product/upsert"; import { CustomerScheduleServiceCreate } from "../../services/schedule/create"; import { CustomerProductControllerGet, diff --git a/src/functions/customer/controllers/product/get.ts b/src/functions/customer/controllers/product/get.ts index 96b4c61e..741f9219 100644 --- a/src/functions/customer/controllers/product/get.ts +++ b/src/functions/customer/controllers/product/get.ts @@ -5,7 +5,7 @@ import { ScheduleZodSchema, } from "~/functions/schedule/schedule.types"; import { _ } from "~/library/handler"; -import { CustomerProductServiceGet } from "../../services/product"; +import { CustomerProductServiceGet } from "../../services/product/get"; export type CustomerProductControllerGetRequest = { query: z.infer; diff --git a/src/functions/customer/controllers/product/upsert.ts b/src/functions/customer/controllers/product/upsert.ts index e43d9859..d6b22994 100644 --- a/src/functions/customer/controllers/product/upsert.ts +++ b/src/functions/customer/controllers/product/upsert.ts @@ -5,7 +5,7 @@ import { } from "~/functions/schedule/schedule.types"; import { _ } from "~/library/handler"; -import { CustomerProductServiceUpsert } from "../../services/product"; +import { CustomerProductServiceUpsert } from "../../services/product/upsert"; export type CustomerProductControllerUpsertRequest = { query: z.infer; diff --git a/src/functions/customer/controllers/products/list-ids.spec.ts b/src/functions/customer/controllers/products/list-ids.spec.ts index 8aef7e3b..05303c7c 100644 --- a/src/functions/customer/controllers/products/list-ids.spec.ts +++ b/src/functions/customer/controllers/products/list-ids.spec.ts @@ -1,6 +1,5 @@ import { HttpRequest, InvocationContext } from "@azure/functions"; -import { CustomerProductServiceUpsert } from "~/functions/customer/services/product"; import { TimeUnit } from "~/functions/schedule"; import { HttpSuccessResponse, @@ -8,6 +7,7 @@ import { createHttpRequest, } from "~/library/jest/azure"; import { getProductObject } from "~/library/jest/helpers/product"; +import { CustomerProductServiceUpsert } from "../../services/product/upsert"; import { CustomerScheduleServiceCreate } from "../../services/schedule/create"; import { CustomerProductsControllerListIds, diff --git a/src/functions/customer/controllers/products/list-ids.ts b/src/functions/customer/controllers/products/list-ids.ts index 70c65c1d..02d29fb3 100644 --- a/src/functions/customer/controllers/products/list-ids.ts +++ b/src/functions/customer/controllers/products/list-ids.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { _ } from "~/library/handler"; import { UserZodSchema } from "~/functions/user"; -import { CustomerProductsServiceListIds } from "../../services/product"; +import { CustomerProductsServiceListIds } from "../../services/product/list-ids"; export type CustomerProductsControllerListIdsRequest = { query: z.infer; diff --git a/src/functions/customer/controllers/products/list.spec.ts b/src/functions/customer/controllers/products/list.spec.ts index 9d0f8eb8..5d4c8dec 100644 --- a/src/functions/customer/controllers/products/list.spec.ts +++ b/src/functions/customer/controllers/products/list.spec.ts @@ -9,7 +9,7 @@ import { } from "~/library/jest/azure"; import { getProductObject } from "~/library/jest/helpers/product"; -import { CustomerProductServiceUpsert } from "../../services/product"; +import { CustomerProductServiceUpsert } from "../../services/product/upsert"; import { CustomerScheduleServiceCreate } from "../../services/schedule/create"; import { CustomerProductsControllerList, diff --git a/src/functions/customer/controllers/products/list.ts b/src/functions/customer/controllers/products/list.ts index 72f05e83..d79d8396 100644 --- a/src/functions/customer/controllers/products/list.ts +++ b/src/functions/customer/controllers/products/list.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { _ } from "~/library/handler"; import { UserZodSchema } from "~/functions/user"; -import { CustomerProductsServiceList } from "../../services/product"; +import { CustomerProductsServiceList } from "../../services/product/list"; export type CustomerProductsControllerListRequest = { query: z.infer; diff --git a/src/functions/customer/services/location.ts b/src/functions/customer/services/location.ts index 1504563f..19642383 100644 --- a/src/functions/customer/services/location.ts +++ b/src/functions/customer/services/location.ts @@ -22,7 +22,7 @@ import { UserServiceLocationsSetDefault, } from "~/functions/user"; import { NotFoundError } from "~/library/handler"; -import { CustomerProductServiceRemoveLocationFromAll } from "./product"; +import { CustomerProductServiceRemoveLocationFromAll } from "./product/remove-location-from-all"; export const CustomerLocationServiceCreate = async ( body: LocationServiceCreateProps diff --git a/src/functions/customer/services/product.spec.ts b/src/functions/customer/services/product.spec.ts deleted file mode 100644 index f6c8673a..00000000 --- a/src/functions/customer/services/product.spec.ts +++ /dev/null @@ -1,412 +0,0 @@ -import mongoose from "mongoose"; - -import { LocationTypes } from "~/functions/location"; -import { TimeUnit } from "~/functions/schedule"; -import { omitObjectIdProps } from "~/library/jest/helpers"; -import { getProductObject } from "~/library/jest/helpers/product"; -import { - CustomerProductServiceDestroy, - CustomerProductServiceGet, - CustomerProductServiceRemoveLocationFromAll, - CustomerProductServiceUpsert, - CustomerProductsServiceList, - CustomerProductsServiceListIds, -} from "./product"; -import { CustomerScheduleServiceCreate } from "./schedule/create"; -import { CustomerScheduleServiceGet } from "./schedule/get"; - -require("~/library/jest/mongoose/mongodb.jest"); - -describe("CustomerProductsService", () => { - const customerId = 123; - const name = "Test Schedule"; - const productId = 1000; - const newProduct = getProductObject({ - variantId: 1, - duration: 60, - breakTime: 0, - noticePeriod: { - value: 1, - unit: TimeUnit.DAYS, - }, - bookingPeriod: { - value: 1, - unit: TimeUnit.WEEKS, - }, - locations: [], - }); - - it("should get all productIds for all schedules", async () => { - const schedule1 = await CustomerScheduleServiceCreate({ - name: "ab", - customerId: 7, - }); - - const product1 = getProductObject({ - variantId: 1, - duration: 60, - breakTime: 0, - noticePeriod: { - value: 1, - unit: TimeUnit.DAYS, - }, - bookingPeriod: { - value: 1, - unit: TimeUnit.WEEKS, - }, - locations: [], - }); - - await CustomerProductServiceUpsert( - { - customerId: schedule1.customerId, - productId: 999, - }, - { ...product1, scheduleId: schedule1._id } - ); - - const schedule2 = await CustomerScheduleServiceCreate({ - name: "ab", - customerId, - }); - - const product2 = { ...product1, scheduleId: schedule2._id }; - - await CustomerProductServiceUpsert( - { - customerId: schedule2.customerId, - productId: 1001, - }, - product2 - ); - - await CustomerProductServiceUpsert( - { - customerId: schedule2.customerId, - productId: 1000, - }, - product2 - ); - - const schedule3 = await CustomerScheduleServiceCreate({ - name: "test", - customerId, - }); - - const product3 = { - ...product1, - scheduleId: schedule3._id, - }; - - await CustomerProductServiceUpsert( - { - customerId: schedule3.customerId, - productId: 1002, - }, - product3 - ); - - await CustomerProductServiceUpsert( - { - customerId: schedule3.customerId, - productId: 1004, - }, - product3 - ); - - const products = await CustomerProductsServiceListIds({ customerId }); - expect(products).toEqual([1001, 1000, 1002, 1004]); - }); - - it("should get all products for all schedules", async () => { - const schedule1 = await CustomerScheduleServiceCreate({ - name: "ab", - customerId, - }); - - const product1 = getProductObject({ - variantId: 1, - duration: 60, - breakTime: 0, - noticePeriod: { - value: 1, - unit: TimeUnit.DAYS, - }, - bookingPeriod: { - value: 1, - unit: TimeUnit.WEEKS, - }, - locations: [], - }); - - await CustomerProductServiceUpsert( - { - customerId: schedule1.customerId, - productId: 1001, - }, - { ...product1, scheduleId: schedule1._id } - ); - - await CustomerProductServiceUpsert( - { - customerId: schedule1.customerId, - productId: 1000, - }, - { ...product1, scheduleId: schedule1._id } - ); - - const newSchedule2 = await CustomerScheduleServiceCreate({ - name: "test", - customerId, - }); - - const product2 = { ...product1, scheduleId: newSchedule2._id }; - - await CustomerProductServiceUpsert( - { - customerId: newSchedule2.customerId, - productId: 1002, - }, - product2 - ); - - await CustomerProductServiceUpsert( - { - customerId: newSchedule2.customerId, - productId: 1004, - }, - product2 - ); - - const products = await CustomerProductsServiceList({ customerId }); - expect(products).toHaveLength(4); - }); - - it("should add a new product to the schedule", async () => { - const newSchedule = await CustomerScheduleServiceCreate({ - name, - customerId, - }); - - const updateProduct = await CustomerProductServiceUpsert( - { - customerId: newSchedule.customerId, - productId, - }, - { ...newProduct, scheduleId: newSchedule._id } - ); - - expect(updateProduct).toMatchObject({ - ...newProduct, - productId, - scheduleId: newSchedule._id.toString(), - }); - }); - - it("should be able to remove one location from all products", async () => { - const newSchedule1 = await CustomerScheduleServiceCreate({ - name, - customerId, - }); - const locationRemoveId = new mongoose.Types.ObjectId().toString(); - await CustomerProductServiceUpsert( - { - customerId: newSchedule1.customerId, - productId, - }, - { - ...newProduct, - scheduleId: newSchedule1._id, - locations: [ - { - location: locationRemoveId, - locationType: LocationTypes.ORIGIN, - }, - { - location: new mongoose.Types.ObjectId(), - locationType: LocationTypes.ORIGIN, - }, - ], - } - ); - - await CustomerProductServiceUpsert( - { - customerId: newSchedule1.customerId, - productId: 22, - }, - { - ...newProduct, - scheduleId: newSchedule1._id, - locations: [ - { - location: locationRemoveId, - locationType: LocationTypes.ORIGIN, - }, - { - location: new mongoose.Types.ObjectId().toString(), - locationType: LocationTypes.DESTINATION, - }, - ], - } - ); - - const newSchedule2 = await CustomerScheduleServiceCreate({ - name: "test2", - customerId, - }); - - await CustomerProductServiceUpsert( - { - customerId: newSchedule2.customerId, - productId: 232, - }, - { - ...newProduct, - scheduleId: newSchedule2._id, - locations: [ - { - location: locationRemoveId, - locationType: LocationTypes.ORIGIN, - }, - ], - } - ); - - let getSchedule1 = await CustomerScheduleServiceGet({ - customerId, - scheduleId: newSchedule1.id, - }); - - getSchedule1.products.forEach((product) => { - const locationIds = product.locations.map((location) => - location.location.toString() - ); - expect(locationIds).toContain(locationRemoveId); - }); - - let getSchedule2 = await CustomerScheduleServiceGet({ - customerId, - scheduleId: newSchedule2.id, - }); - - getSchedule2.products.forEach((product) => { - const locationIds = product.locations.map((location) => - location.location.toString() - ); - expect(locationIds).toContain(locationRemoveId); - }); - - expect(getSchedule1.products[0].locations).toHaveLength(2); - expect(getSchedule1.products[1].locations).toHaveLength(2); - expect(getSchedule2.products[0].locations).toHaveLength(1); - - await CustomerProductServiceRemoveLocationFromAll({ - locationId: locationRemoveId, - customerId, - }); - - getSchedule1 = await CustomerScheduleServiceGet({ - customerId, - scheduleId: newSchedule1.id, - }); - - getSchedule2 = await CustomerScheduleServiceGet({ - customerId, - scheduleId: newSchedule2.id, - }); - - expect(getSchedule1.products[0].locations).toHaveLength(1); - expect(getSchedule1.products[1].locations).toHaveLength(1); - expect(getSchedule2.products[0].locations).toHaveLength(0); - - getSchedule1.products.forEach((product) => { - const locationIds = product.locations.map((location) => - location.location.toString() - ); - expect(locationIds).not.toContain(locationRemoveId); - }); - }); - - it("should find a product", async () => { - const newSchedule = await CustomerScheduleServiceCreate({ - name, - customerId, - }); - - const updatedSchedule = await CustomerProductServiceUpsert( - { - customerId: newSchedule.customerId, - productId, - }, - { ...newProduct, scheduleId: newSchedule._id } - ); - - const foundProduct = await CustomerProductServiceGet({ - customerId: newSchedule.customerId, - productId, - }); - - expect(foundProduct).toMatchObject({ productId }); - }); - - it("should update an existing product in the schedule", async () => { - const newSchedule = await CustomerScheduleServiceCreate({ - name, - customerId, - }); - - await CustomerProductServiceUpsert( - { - customerId: newSchedule.customerId, - productId, - }, - { ...newProduct, scheduleId: newSchedule._id } - ); - - const productBody = { - ...newProduct, - duration: 90, - scheduleId: newSchedule._id, - }; - - let updateProduct = await CustomerProductServiceUpsert( - { - customerId: newSchedule.customerId, - productId, - }, - productBody - ); - - expect(omitObjectIdProps(updateProduct)).toEqual( - expect.objectContaining( - omitObjectIdProps({ - ...productBody, - productId: updateProduct.productId, - }) - ) - ); - }); - - it("should remove an existing product from the schedule", async () => { - const newSchedule = await CustomerScheduleServiceCreate({ - name, - customerId, - }); - - await CustomerProductServiceUpsert( - { - customerId: newSchedule.customerId, - productId, - }, - { ...newProduct, scheduleId: newSchedule._id } - ); - - const updatedSchedule = await CustomerProductServiceDestroy({ - customerId: newSchedule.customerId, - productId, - }); - - expect(updatedSchedule?.modifiedCount).toBe(1); - }); -}); diff --git a/src/functions/customer/services/product.ts b/src/functions/customer/services/product.ts deleted file mode 100644 index 36302412..00000000 --- a/src/functions/customer/services/product.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { z } from "zod"; -import { - Schedule, - ScheduleModel, - ScheduleProduct, - ScheduleZodSchema, -} from "~/functions/schedule"; -import { NotFoundError } from "~/library/handler"; - -type CustomerProductsServiceListIdsProps = { - customerId: Schedule["customerId"]; -}; - -export const CustomerProductsServiceListIds = async ( - filter: CustomerProductsServiceListIdsProps -) => { - const schedules = await ScheduleModel.find(filter).select( - "products.productId" - ); - - return schedules.flatMap((schedule) => - schedule.products.map((product) => product.productId) - ); -}; - -type CustomerProductsServiceListProps = { - customerId: Schedule["customerId"]; - scheduleId?: Schedule["_id"]; -}; - -export const CustomerProductsServiceList = async ({ - customerId, - scheduleId, -}: CustomerProductsServiceListProps) => { - let query: any = { customerId }; - if (scheduleId !== undefined) { - query._id = scheduleId; - } - - const schedules = await ScheduleModel.find(query) - .select("name products") - .lean(); - - return schedules.flatMap((schedule) => - schedule.products.map((product) => ({ - scheduleId: schedule._id, - scheduleName: schedule.name, - ...product, - })) - ); -}; - -export type CustomerProductServiceDestroyFilter = { - customerId: Schedule["customerId"]; - productId: ScheduleProduct["productId"]; -}; - -export const CustomerProductServiceDestroy = async ( - filter: CustomerProductServiceDestroyFilter -) => { - try { - return ScheduleModel.updateOne( - { - customerId: filter.customerId, - products: { - $elemMatch: { - productId: filter.productId, - }, - }, - }, - { $pull: { products: { productId: filter.productId } } }, - { new: true } - ).lean(); - } catch (error) { - console.error("Error destroying product:", error); - } -}; - -export type CustomerProductServiceUpsert = { - customerId: Schedule["customerId"]; - productId: ScheduleProduct["productId"]; -}; - -export type CustomerProductServiceUpsertBody = Omit< - ScheduleProduct, - "productId" -> & { - scheduleId: z.infer; -}; - -export const CustomerProductServiceUpsert = async ( - filter: CustomerProductServiceUpsert, - product: CustomerProductServiceUpsertBody -) => { - await CustomerProductServiceDestroy(filter); - const schedule = await ScheduleModel.findOneAndUpdate( - { - _id: product.scheduleId, - customerId: filter.customerId, - }, - { $push: { products: { ...product, productId: filter.productId } } }, - { new: true, upsert: true } - ) - .orFail( - new NotFoundError([ - { - code: "custom", - message: "PRODUCT_NOT_FOUND", - path: ["productId"], - }, - ]) - ) - .lean(); - - return { - ...product, - productId: filter.productId, - scheduleId: schedule._id.toString(), - scheduleName: schedule.name, - }; -}; - -export type CustomerProductServiceGetFilter = { - customerId: Schedule["customerId"]; - productId: ScheduleProduct["productId"]; -}; - -export const CustomerProductServiceGet = async ( - filter: CustomerProductServiceGetFilter -) => { - const schedule = await ScheduleModel.findOne({ - customerId: filter.customerId, - products: { - $elemMatch: { - productId: filter.productId, - }, - }, - }) - .orFail( - new NotFoundError([ - { - code: "custom", - message: "PRODUCT_NOT_FOUND", - path: ["productId"], - }, - ]) - ) - .lean(); - - const product = schedule.products.find( - (p) => p.productId === filter.productId - ); - - if (!product) { - throw new NotFoundError([ - { - code: "custom", - message: "PRODUCT_NOT_FOUND", - path: ["productId"], - }, - ]); - } - - return { - ...product, - scheduleId: schedule._id, - scheduleName: schedule.name, - }; -}; - -export const CustomerProductServiceRemoveLocationFromAll = async (filter: { - locationId: string; - customerId: number; -}) => { - const schedules = await ScheduleModel.find({ customerId: filter.customerId }); - - for (let schedule of schedules) { - for (let product of schedule.products) { - product.locations = product.locations.filter( - (location) => - location.location.toString() !== filter.locationId.toString() - ); - } - - await schedule.save(); - } -}; diff --git a/src/functions/customer/services/product/destroy.spec.ts b/src/functions/customer/services/product/destroy.spec.ts new file mode 100644 index 00000000..f470b437 --- /dev/null +++ b/src/functions/customer/services/product/destroy.spec.ts @@ -0,0 +1,49 @@ +import { TimeUnit } from "~/functions/schedule"; +import { getProductObject } from "~/library/jest/helpers/product"; +import { CustomerScheduleServiceCreate } from "../schedule/create"; +import { CustomerProductServiceDestroy } from "./destroy"; +import { CustomerProductServiceUpsert } from "./upsert"; + +require("~/library/jest/mongoose/mongodb.jest"); + +describe("CustomerProductServiceDestroy", () => { + const customerId = 123; + const name = "Test Schedule"; + const productId = 1000; + const newProduct = getProductObject({ + variantId: 1, + duration: 60, + breakTime: 0, + noticePeriod: { + value: 1, + unit: TimeUnit.DAYS, + }, + bookingPeriod: { + value: 1, + unit: TimeUnit.WEEKS, + }, + locations: [], + }); + + it("should remove an existing product from the schedule", async () => { + const newSchedule = await CustomerScheduleServiceCreate({ + name, + customerId, + }); + + await CustomerProductServiceUpsert( + { + customerId: newSchedule.customerId, + productId, + }, + { ...newProduct, scheduleId: newSchedule._id } + ); + + const updatedSchedule = await CustomerProductServiceDestroy({ + customerId: newSchedule.customerId, + productId, + }); + + expect(updatedSchedule?.modifiedCount).toBe(1); + }); +}); diff --git a/src/functions/customer/services/product/destroy.ts b/src/functions/customer/services/product/destroy.ts new file mode 100644 index 00000000..0f484375 --- /dev/null +++ b/src/functions/customer/services/product/destroy.ts @@ -0,0 +1,27 @@ +import { Schedule, ScheduleModel, ScheduleProduct } from "~/functions/schedule"; + +export type CustomerProductServiceDestroyFilter = { + customerId: Schedule["customerId"]; + productId: ScheduleProduct["productId"]; +}; + +export const CustomerProductServiceDestroy = async ( + filter: CustomerProductServiceDestroyFilter +) => { + try { + return ScheduleModel.updateOne( + { + customerId: filter.customerId, + products: { + $elemMatch: { + productId: filter.productId, + }, + }, + }, + { $pull: { products: { productId: filter.productId } } }, + { new: true } + ).lean(); + } catch (error) { + console.error("Error destroying product:", error); + } +}; diff --git a/src/functions/customer/services/product/get.spec.ts b/src/functions/customer/services/product/get.spec.ts new file mode 100644 index 00000000..e594380d --- /dev/null +++ b/src/functions/customer/services/product/get.spec.ts @@ -0,0 +1,49 @@ +import { TimeUnit } from "~/functions/schedule"; +import { getProductObject } from "~/library/jest/helpers/product"; +import { CustomerScheduleServiceCreate } from "../schedule/create"; +import { CustomerProductServiceGet } from "./get"; +import { CustomerProductServiceUpsert } from "./upsert"; + +require("~/library/jest/mongoose/mongodb.jest"); + +describe("CustomerProductsService", () => { + const customerId = 123; + const name = "Test Schedule"; + const productId = 1000; + const newProduct = getProductObject({ + variantId: 1, + duration: 60, + breakTime: 0, + noticePeriod: { + value: 1, + unit: TimeUnit.DAYS, + }, + bookingPeriod: { + value: 1, + unit: TimeUnit.WEEKS, + }, + locations: [], + }); + + it("should find a product", async () => { + const newSchedule = await CustomerScheduleServiceCreate({ + name, + customerId, + }); + + const updatedSchedule = await CustomerProductServiceUpsert( + { + customerId: newSchedule.customerId, + productId, + }, + { ...newProduct, scheduleId: newSchedule._id } + ); + + const foundProduct = await CustomerProductServiceGet({ + customerId: newSchedule.customerId, + productId, + }); + + expect(foundProduct).toMatchObject({ productId }); + }); +}); diff --git a/src/functions/customer/services/product/get.ts b/src/functions/customer/services/product/get.ts new file mode 100644 index 00000000..6b09d48c --- /dev/null +++ b/src/functions/customer/services/product/get.ts @@ -0,0 +1,50 @@ +import { Schedule, ScheduleModel, ScheduleProduct } from "~/functions/schedule"; +import { NotFoundError } from "~/library/handler"; + +export type CustomerProductServiceGetFilter = { + customerId: Schedule["customerId"]; + productId: ScheduleProduct["productId"]; +}; + +export const CustomerProductServiceGet = async ( + filter: CustomerProductServiceGetFilter +) => { + const schedule = await ScheduleModel.findOne({ + customerId: filter.customerId, + products: { + $elemMatch: { + productId: filter.productId, + }, + }, + }) + .orFail( + new NotFoundError([ + { + code: "custom", + message: "PRODUCT_NOT_FOUND", + path: ["productId"], + }, + ]) + ) + .lean(); + + const product = schedule.products.find( + (p) => p.productId === filter.productId + ); + + if (!product) { + throw new NotFoundError([ + { + code: "custom", + message: "PRODUCT_NOT_FOUND", + path: ["productId"], + }, + ]); + } + + return { + ...product, + scheduleId: schedule._id, + scheduleName: schedule.name, + }; +}; diff --git a/src/functions/customer/services/product/list-ids.spec.ts b/src/functions/customer/services/product/list-ids.spec.ts new file mode 100644 index 00000000..4b8fe08b --- /dev/null +++ b/src/functions/customer/services/product/list-ids.spec.ts @@ -0,0 +1,109 @@ +import { TimeUnit } from "~/functions/schedule"; +import { getProductObject } from "~/library/jest/helpers/product"; +import { CustomerScheduleServiceCreate } from "../schedule/create"; +import { CustomerProductsServiceListIds } from "./list-ids"; +import { CustomerProductServiceUpsert } from "./upsert"; + +require("~/library/jest/mongoose/mongodb.jest"); + +describe("CustomerProductsServiceListIds", () => { + const customerId = 123; + const name = "Test Schedule"; + const productId = 1000; + const newProduct = getProductObject({ + variantId: 1, + duration: 60, + breakTime: 0, + noticePeriod: { + value: 1, + unit: TimeUnit.DAYS, + }, + bookingPeriod: { + value: 1, + unit: TimeUnit.WEEKS, + }, + locations: [], + }); + + it("should get all productIds for all schedules", async () => { + const schedule1 = await CustomerScheduleServiceCreate({ + name: "ab", + customerId: 7, + }); + + const product1 = getProductObject({ + variantId: 1, + duration: 60, + breakTime: 0, + noticePeriod: { + value: 1, + unit: TimeUnit.DAYS, + }, + bookingPeriod: { + value: 1, + unit: TimeUnit.WEEKS, + }, + locations: [], + }); + + await CustomerProductServiceUpsert( + { + customerId: schedule1.customerId, + productId: 999, + }, + { ...product1, scheduleId: schedule1._id } + ); + + const schedule2 = await CustomerScheduleServiceCreate({ + name: "ab", + customerId, + }); + + const product2 = { ...product1, scheduleId: schedule2._id }; + + await CustomerProductServiceUpsert( + { + customerId: schedule2.customerId, + productId: 1001, + }, + product2 + ); + + await CustomerProductServiceUpsert( + { + customerId: schedule2.customerId, + productId: 1000, + }, + product2 + ); + + const schedule3 = await CustomerScheduleServiceCreate({ + name: "test", + customerId, + }); + + const product3 = { + ...product1, + scheduleId: schedule3._id, + }; + + await CustomerProductServiceUpsert( + { + customerId: schedule3.customerId, + productId: 1002, + }, + product3 + ); + + await CustomerProductServiceUpsert( + { + customerId: schedule3.customerId, + productId: 1004, + }, + product3 + ); + + const products = await CustomerProductsServiceListIds({ customerId }); + expect(products).toEqual([1001, 1000, 1002, 1004]); + }); +}); diff --git a/src/functions/customer/services/product/list-ids.ts b/src/functions/customer/services/product/list-ids.ts new file mode 100644 index 00000000..92f12c51 --- /dev/null +++ b/src/functions/customer/services/product/list-ids.ts @@ -0,0 +1,22 @@ +import { Schedule, ScheduleModel } from "~/functions/schedule"; + +type CustomerProductsServiceListIdsProps = { + customerId: Schedule["customerId"]; +}; + +export const CustomerProductsServiceListIds = async ( + filter: CustomerProductsServiceListIdsProps +) => { + const schedules = await ScheduleModel.find(filter).select( + "products.productId" + ); + + return schedules.flatMap((schedule) => + schedule.products.map((product) => product.productId) + ); +}; + +type CustomerProductsServiceListProps = { + customerId: Schedule["customerId"]; + scheduleId?: Schedule["_id"]; +}; diff --git a/src/functions/customer/services/product/list.spec.ts b/src/functions/customer/services/product/list.spec.ts new file mode 100644 index 00000000..83db9c07 --- /dev/null +++ b/src/functions/customer/services/product/list.spec.ts @@ -0,0 +1,91 @@ +import { TimeUnit } from "~/functions/schedule"; +import { getProductObject } from "~/library/jest/helpers/product"; +import { CustomerScheduleServiceCreate } from "../schedule/create"; +import { CustomerProductsServiceList } from "./list"; +import { CustomerProductServiceUpsert } from "./upsert"; + +require("~/library/jest/mongoose/mongodb.jest"); + +describe("CustomerProductsServiceList", () => { + const customerId = 123; + const name = "Test Schedule"; + const productId = 1000; + const newProduct = getProductObject({ + variantId: 1, + duration: 60, + breakTime: 0, + noticePeriod: { + value: 1, + unit: TimeUnit.DAYS, + }, + bookingPeriod: { + value: 1, + unit: TimeUnit.WEEKS, + }, + locations: [], + }); + + it("should get all products for all schedules", async () => { + const schedule1 = await CustomerScheduleServiceCreate({ + name: "ab", + customerId, + }); + + const product1 = getProductObject({ + variantId: 1, + duration: 60, + breakTime: 0, + noticePeriod: { + value: 1, + unit: TimeUnit.DAYS, + }, + bookingPeriod: { + value: 1, + unit: TimeUnit.WEEKS, + }, + locations: [], + }); + + await CustomerProductServiceUpsert( + { + customerId: schedule1.customerId, + productId: 1001, + }, + { ...product1, scheduleId: schedule1._id } + ); + + await CustomerProductServiceUpsert( + { + customerId: schedule1.customerId, + productId: 1000, + }, + { ...product1, scheduleId: schedule1._id } + ); + + const newSchedule2 = await CustomerScheduleServiceCreate({ + name: "test", + customerId, + }); + + const product2 = { ...product1, scheduleId: newSchedule2._id }; + + await CustomerProductServiceUpsert( + { + customerId: newSchedule2.customerId, + productId: 1002, + }, + product2 + ); + + await CustomerProductServiceUpsert( + { + customerId: newSchedule2.customerId, + productId: 1004, + }, + product2 + ); + + const products = await CustomerProductsServiceList({ customerId }); + expect(products).toHaveLength(4); + }); +}); diff --git a/src/functions/customer/services/product/list.ts b/src/functions/customer/services/product/list.ts new file mode 100644 index 00000000..e7ce4052 --- /dev/null +++ b/src/functions/customer/services/product/list.ts @@ -0,0 +1,28 @@ +import { Schedule, ScheduleModel } from "~/functions/schedule"; + +type CustomerProductsServiceListProps = { + customerId: Schedule["customerId"]; + scheduleId?: Schedule["_id"]; +}; + +export const CustomerProductsServiceList = async ({ + customerId, + scheduleId, +}: CustomerProductsServiceListProps) => { + let query: any = { customerId }; + if (scheduleId !== undefined) { + query._id = scheduleId; + } + + const schedules = await ScheduleModel.find(query) + .select("name products") + .lean(); + + return schedules.flatMap((schedule) => + schedule.products.map((product) => ({ + scheduleId: schedule._id, + scheduleName: schedule.name, + ...product, + })) + ); +}; diff --git a/src/functions/customer/services/product/remove-location-from-all.spec.ts b/src/functions/customer/services/product/remove-location-from-all.spec.ts new file mode 100644 index 00000000..9f913633 --- /dev/null +++ b/src/functions/customer/services/product/remove-location-from-all.spec.ts @@ -0,0 +1,156 @@ +import mongoose from "mongoose"; + +import { LocationTypes } from "~/functions/location"; +import { TimeUnit } from "~/functions/schedule"; +import { getProductObject } from "~/library/jest/helpers/product"; +import { CustomerScheduleServiceCreate } from "../schedule/create"; +import { CustomerScheduleServiceGet } from "../schedule/get"; +import { CustomerProductServiceRemoveLocationFromAll } from "./remove-location-from-all"; +import { CustomerProductServiceUpsert } from "./upsert"; + +require("~/library/jest/mongoose/mongodb.jest"); + +describe("CustomerProductServiceRemoveLocationFromAll", () => { + const customerId = 123; + const name = "Test Schedule"; + const productId = 1000; + const newProduct = getProductObject({ + variantId: 1, + duration: 60, + breakTime: 0, + noticePeriod: { + value: 1, + unit: TimeUnit.DAYS, + }, + bookingPeriod: { + value: 1, + unit: TimeUnit.WEEKS, + }, + locations: [], + }); + + it("should be able to remove one location from all products", async () => { + const newSchedule1 = await CustomerScheduleServiceCreate({ + name, + customerId, + }); + const locationRemoveId = new mongoose.Types.ObjectId().toString(); + await CustomerProductServiceUpsert( + { + customerId: newSchedule1.customerId, + productId, + }, + { + ...newProduct, + scheduleId: newSchedule1._id, + locations: [ + { + location: locationRemoveId, + locationType: LocationTypes.ORIGIN, + }, + { + location: new mongoose.Types.ObjectId(), + locationType: LocationTypes.ORIGIN, + }, + ], + } + ); + + await CustomerProductServiceUpsert( + { + customerId: newSchedule1.customerId, + productId: 22, + }, + { + ...newProduct, + scheduleId: newSchedule1._id, + locations: [ + { + location: locationRemoveId, + locationType: LocationTypes.ORIGIN, + }, + { + location: new mongoose.Types.ObjectId().toString(), + locationType: LocationTypes.DESTINATION, + }, + ], + } + ); + + const newSchedule2 = await CustomerScheduleServiceCreate({ + name: "test2", + customerId, + }); + + await CustomerProductServiceUpsert( + { + customerId: newSchedule2.customerId, + productId: 232, + }, + { + ...newProduct, + scheduleId: newSchedule2._id, + locations: [ + { + location: locationRemoveId, + locationType: LocationTypes.ORIGIN, + }, + ], + } + ); + + let getSchedule1 = await CustomerScheduleServiceGet({ + customerId, + scheduleId: newSchedule1.id, + }); + + getSchedule1.products.forEach((product) => { + const locationIds = product.locations.map((location) => + location.location.toString() + ); + expect(locationIds).toContain(locationRemoveId); + }); + + let getSchedule2 = await CustomerScheduleServiceGet({ + customerId, + scheduleId: newSchedule2.id, + }); + + getSchedule2.products.forEach((product) => { + const locationIds = product.locations.map((location) => + location.location.toString() + ); + expect(locationIds).toContain(locationRemoveId); + }); + + expect(getSchedule1.products[0].locations).toHaveLength(2); + expect(getSchedule1.products[1].locations).toHaveLength(2); + expect(getSchedule2.products[0].locations).toHaveLength(1); + + await CustomerProductServiceRemoveLocationFromAll({ + locationId: locationRemoveId, + customerId, + }); + + getSchedule1 = await CustomerScheduleServiceGet({ + customerId, + scheduleId: newSchedule1.id, + }); + + getSchedule2 = await CustomerScheduleServiceGet({ + customerId, + scheduleId: newSchedule2.id, + }); + + expect(getSchedule1.products[0].locations).toHaveLength(1); + expect(getSchedule1.products[1].locations).toHaveLength(1); + expect(getSchedule2.products[0].locations).toHaveLength(0); + + getSchedule1.products.forEach((product) => { + const locationIds = product.locations.map((location) => + location.location.toString() + ); + expect(locationIds).not.toContain(locationRemoveId); + }); + }); +}); diff --git a/src/functions/customer/services/product/remove-location-from-all.ts b/src/functions/customer/services/product/remove-location-from-all.ts new file mode 100644 index 00000000..b3247749 --- /dev/null +++ b/src/functions/customer/services/product/remove-location-from-all.ts @@ -0,0 +1,19 @@ +import { ScheduleModel } from "~/functions/schedule"; + +export const CustomerProductServiceRemoveLocationFromAll = async (filter: { + locationId: string; + customerId: number; +}) => { + const schedules = await ScheduleModel.find({ customerId: filter.customerId }); + + for (let schedule of schedules) { + for (let product of schedule.products) { + product.locations = product.locations.filter( + (location) => + location.location.toString() !== filter.locationId.toString() + ); + } + + await schedule.save(); + } +}; diff --git a/src/functions/customer/services/product/upsert.spec.ts b/src/functions/customer/services/product/upsert.spec.ts new file mode 100644 index 00000000..95bc8ee5 --- /dev/null +++ b/src/functions/customer/services/product/upsert.spec.ts @@ -0,0 +1,86 @@ +import { TimeUnit } from "~/functions/schedule"; +import { omitObjectIdProps } from "~/library/jest/helpers"; +import { getProductObject } from "~/library/jest/helpers/product"; +import { CustomerScheduleServiceCreate } from "../schedule/create"; +import { CustomerProductServiceUpsert } from "./upsert"; + +require("~/library/jest/mongoose/mongodb.jest"); + +describe("CustomerProductServiceUpsert", () => { + const customerId = 123; + const name = "Test Schedule"; + const productId = 1000; + const newProduct = getProductObject({ + variantId: 1, + duration: 60, + breakTime: 0, + noticePeriod: { + value: 1, + unit: TimeUnit.DAYS, + }, + bookingPeriod: { + value: 1, + unit: TimeUnit.WEEKS, + }, + locations: [], + }); + + it("should add a new product to the schedule", async () => { + const newSchedule = await CustomerScheduleServiceCreate({ + name, + customerId, + }); + + const updateProduct = await CustomerProductServiceUpsert( + { + customerId: newSchedule.customerId, + productId, + }, + { ...newProduct, scheduleId: newSchedule._id } + ); + + expect(updateProduct).toMatchObject({ + ...newProduct, + productId, + scheduleId: newSchedule._id.toString(), + }); + }); + + it("should update an existing product in the schedule", async () => { + const newSchedule = await CustomerScheduleServiceCreate({ + name, + customerId, + }); + + await CustomerProductServiceUpsert( + { + customerId: newSchedule.customerId, + productId, + }, + { ...newProduct, scheduleId: newSchedule._id } + ); + + const productBody = { + ...newProduct, + duration: 90, + scheduleId: newSchedule._id, + }; + + let updateProduct = await CustomerProductServiceUpsert( + { + customerId: newSchedule.customerId, + productId, + }, + productBody + ); + + expect(omitObjectIdProps(updateProduct)).toEqual( + expect.objectContaining( + omitObjectIdProps({ + ...productBody, + productId: updateProduct.productId, + }) + ) + ); + }); +}); diff --git a/src/functions/customer/services/product/upsert.ts b/src/functions/customer/services/product/upsert.ts new file mode 100644 index 00000000..66b1043b --- /dev/null +++ b/src/functions/customer/services/product/upsert.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; +import { + Schedule, + ScheduleModel, + ScheduleProduct, + ScheduleZodSchema, +} from "~/functions/schedule"; +import { NotFoundError } from "~/library/handler"; +import { CustomerProductServiceDestroy } from "./destroy"; + +export type CustomerProductServiceUpsert = { + customerId: Schedule["customerId"]; + productId: ScheduleProduct["productId"]; +}; + +export type CustomerProductServiceUpsertBody = Omit< + ScheduleProduct, + "productId" +> & { + scheduleId: z.infer; +}; + +export const CustomerProductServiceUpsert = async ( + filter: CustomerProductServiceUpsert, + product: CustomerProductServiceUpsertBody +) => { + await CustomerProductServiceDestroy(filter); + const schedule = await ScheduleModel.findOneAndUpdate( + { + _id: product.scheduleId, + customerId: filter.customerId, + }, + { $push: { products: { ...product, productId: filter.productId } } }, + { new: true, upsert: true } + ) + .orFail( + new NotFoundError([ + { + code: "custom", + message: "PRODUCT_NOT_FOUND", + path: ["productId"], + }, + ]) + ) + .lean(); + + return { + ...product, + productId: filter.productId, + scheduleId: schedule._id.toString(), + scheduleName: schedule.name, + }; +}; diff --git a/src/functions/user/controllers/products/get.spec.ts b/src/functions/user/controllers/products/get.spec.ts index 2345569a..4aabfe89 100644 --- a/src/functions/user/controllers/products/get.spec.ts +++ b/src/functions/user/controllers/products/get.spec.ts @@ -1,9 +1,9 @@ import { HttpRequest, InvocationContext } from "@azure/functions"; -import { CustomerProductServiceUpsert } from "~/functions/customer/services/product"; import { CustomerScheduleServiceCreate } from "~/functions/customer/services/schedule/create"; import { TimeUnit } from "~/functions/schedule"; +import { CustomerProductServiceUpsert } from "~/functions/customer/services/product/upsert"; import { HttpSuccessResponse, createContext, diff --git a/src/functions/user/controllers/products/list-by-schedule.spec.ts b/src/functions/user/controllers/products/list-by-schedule.spec.ts index 6b40d330..ea8ebd98 100644 --- a/src/functions/user/controllers/products/list-by-schedule.spec.ts +++ b/src/functions/user/controllers/products/list-by-schedule.spec.ts @@ -8,7 +8,7 @@ import { createHttpRequest, } from "~/library/jest/azure"; -import { CustomerProductServiceUpsert } from "~/functions/customer/services/product"; +import { CustomerProductServiceUpsert } from "~/functions/customer/services/product/upsert"; import { CustomerScheduleServiceCreate } from "~/functions/customer/services/schedule/create"; import { createUser } from "~/library/jest/helpers"; import { getProductObject } from "~/library/jest/helpers/product"; diff --git a/src/functions/user/controllers/products/list-by-schedule.ts b/src/functions/user/controllers/products/list-by-schedule.ts index 453130c9..0c5ad414 100644 --- a/src/functions/user/controllers/products/list-by-schedule.ts +++ b/src/functions/user/controllers/products/list-by-schedule.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { _ } from "~/library/handler"; -import { CustomerProductsServiceList } from "~/functions/customer/services/product"; +import { CustomerProductsServiceList } from "~/functions/customer/services/product/list"; import { UserServiceGetCustomerId } from "~/functions/user"; export type UserProductsControllerListByScheduleRequest = { diff --git a/src/functions/user/services/products/get.spec.ts b/src/functions/user/services/products/get.spec.ts index 68907bda..92c3c694 100644 --- a/src/functions/user/services/products/get.spec.ts +++ b/src/functions/user/services/products/get.spec.ts @@ -1,4 +1,4 @@ -import { CustomerProductServiceUpsert } from "~/functions/customer/services/product"; +import { CustomerProductServiceUpsert } from "~/functions/customer/services/product/upsert"; import { CustomerScheduleServiceCreate } from "~/functions/customer/services/schedule/create"; import { TimeUnit } from "~/functions/schedule"; import { createUser } from "~/library/jest/helpers"; From 9d088cc494cb85284abba560c3d81002791c04b5 Mon Sep 17 00:00:00 2001 From: Jamal Soueidan Date: Wed, 13 Mar 2024 12:45:07 +0100 Subject: [PATCH 06/10] Add create-variant functionality for customer products and refactor imports --- src/functions/customer-product.function.ts | 16 ++++-- .../controllers/product/create-variant.ts | 42 ++++++++++++++ .../customer/controllers/product/index.ts | 3 - .../services/product/create-variant.ts | 56 +++++++++++++++++++ src/functions/webhook/product/product.ts | 10 ++-- src/types/admin.generated.d.ts | 19 +++---- 6 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 src/functions/customer/controllers/product/create-variant.ts delete mode 100644 src/functions/customer/controllers/product/index.ts create mode 100644 src/functions/customer/services/product/create-variant.ts diff --git a/src/functions/customer-product.function.ts b/src/functions/customer-product.function.ts index a5f114de..4063e894 100644 --- a/src/functions/customer-product.function.ts +++ b/src/functions/customer-product.function.ts @@ -3,11 +3,10 @@ import { CustomerProductsControllerList } from "./customer/controllers/products/ import { app } from "@azure/functions"; -import { - CustomerProductControllerDestroy, - CustomerProductControllerGet, - CustomerProductControllerUpsert, -} from "./customer/controllers/product"; +import { CustomerProductControllerCreateVariant } from "./customer/controllers/product/create-variant"; +import { CustomerProductControllerDestroy } from "./customer/controllers/product/destroy"; +import { CustomerProductControllerGet } from "./customer/controllers/product/get"; +import { CustomerProductControllerUpsert } from "./customer/controllers/product/upsert"; import { CustomerProductsControllerListIds } from "./customer/controllers/products"; app.http("customerProductsListIds", { @@ -24,6 +23,13 @@ app.http("customerProductsList", { handler: CustomerProductsControllerList, }); +app.http("customerProductCreateVariant", { + methods: ["POST"], + authLevel: "anonymous", + route: "customer/{customerId?}/product/{productId?}/create-variant", + handler: CustomerProductControllerCreateVariant, +}); + app.http("customerProductUpsert", { methods: ["PUT"], authLevel: "anonymous", diff --git a/src/functions/customer/controllers/product/create-variant.ts b/src/functions/customer/controllers/product/create-variant.ts new file mode 100644 index 00000000..99cbe5ca --- /dev/null +++ b/src/functions/customer/controllers/product/create-variant.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; +import { + ScheduleProductZodSchema, + ScheduleZodSchema, +} from "~/functions/schedule/schedule.types"; + +import { _ } from "~/library/handler"; +import { NumberOrStringType } from "~/library/zod"; +import { CustomerProductServiceCreateVariant } from "../../services/product/create-variant"; + +export type CustomerProductControllerCreateVariantRequest = { + query: z.infer; + body: z.infer; +}; + +const CustomerProductControllerCreateVariantQuerySchema = z.object({ + customerId: ScheduleZodSchema.shape.customerId, + productId: ScheduleProductZodSchema.shape.productId, +}); + +const CustomerProductControllerCreateVariantBodySchema = z.object({ + price: NumberOrStringType, + compareAtPrice: NumberOrStringType, +}); + +export type CustomerProductControllerCreateVariantResponse = Awaited< + ReturnType +>; + +export const CustomerProductControllerCreateVariant = _( + ({ query, body }: CustomerProductControllerCreateVariantRequest) => { + const validateQuery = + CustomerProductControllerCreateVariantQuerySchema.parse(query); + const validateBody = + CustomerProductControllerCreateVariantBodySchema.parse(body); + + return CustomerProductServiceCreateVariant({ + ...validateQuery, + ...validateBody, + }); + } +); diff --git a/src/functions/customer/controllers/product/index.ts b/src/functions/customer/controllers/product/index.ts deleted file mode 100644 index 1292ba29..00000000 --- a/src/functions/customer/controllers/product/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./destroy"; -export * from "./get"; -export * from "./upsert"; diff --git a/src/functions/customer/services/product/create-variant.ts b/src/functions/customer/services/product/create-variant.ts new file mode 100644 index 00000000..68404b73 --- /dev/null +++ b/src/functions/customer/services/product/create-variant.ts @@ -0,0 +1,56 @@ +import { shopifyAdmin } from "~/library/shopify"; +import { ProductVariantCreateMutation } from "~/types/admin.generated"; + +export type CustomerProductServiceCreateVariantProps = { + productId: number; + price: number; + compareAtPrice: number; +}; + +export const CustomerProductServiceCreateVariant = async ( + props: CustomerProductServiceCreateVariantProps +) => { + const { body } = await shopifyAdmin.query<{ + data: ProductVariantCreateMutation; + }>({ + data: { + query: CREATE_VARIANT, + variables: { + input: { + price: props.price, + compareAtPrice: props.compareAtPrice, + productId: `gid://shopify/Product/${props.productId}`, + inventoryItem: { + tracked: false, + }, + options: `Artist ${props.price}.${props.compareAtPrice}`, + }, + }, + }, + }); + + if (body.data.productVariantCreate?.userErrors) { + throw new Error(body.data.productVariantCreate.userErrors[0].message); + } + + return body.data.productVariantCreate?.productVariant; +}; + +const CREATE_VARIANT = `#graphql + mutation productVariantCreate($input: ProductVariantInput!) { + productVariantCreate(input: $input) { + productVariant { + id + title + selectedOptions { + name + value + } + } + userErrors { + field + message + } + } + } +` as const; diff --git a/src/functions/webhook/product/product.ts b/src/functions/webhook/product/product.ts index 587c7ac3..0cb4c365 100644 --- a/src/functions/webhook/product/product.ts +++ b/src/functions/webhook/product/product.ts @@ -3,7 +3,7 @@ import { InvocationContext } from "@azure/functions"; import { telemetryClient } from "~/library/application-insight"; import { connect } from "~/library/mongoose"; import { shopifyAdmin } from "~/library/shopify"; -import { ProductVariantCreateMutation } from "~/types/admin.generated"; +import { ProductVariantsBulkDeleteMutation } from "~/types/admin.generated"; import { ProductUpdateSchema } from "./types"; import { ProductWebHookGetUnusedVariantIds } from "./unused"; @@ -18,7 +18,9 @@ export async function webhookProductProcess( product, }); - const { body } = await shopifyAdmin.query({ + const { body } = await shopifyAdmin.query<{ + data: ProductVariantsBulkDeleteMutation; + }>({ data: { query: MUTATION_DESTROY_VARIANTS, variables: { @@ -30,10 +32,10 @@ export async function webhookProductProcess( }, }); - if (!body.productVariantCreate?.product) { + if (!body.data.productVariantsBulkDelete?.product) { context.error( "webhook product error", - body.productVariantCreate?.userErrors + body.data.productVariantsBulkDelete?.userErrors ); } diff --git a/src/types/admin.generated.d.ts b/src/types/admin.generated.d.ts index 2ca84b6d..d848079b 100644 --- a/src/types/admin.generated.d.ts +++ b/src/types/admin.generated.d.ts @@ -3,16 +3,6 @@ /* eslint-disable */ import * as AdminTypes from './admin.types'; -export type ProductVariantCreateMutationVariables = AdminTypes.Exact<{ - input: AdminTypes.ProductVariantInput; -}>; - - -export type ProductVariantCreateMutation = { productVariantCreate?: AdminTypes.Maybe<{ product?: AdminTypes.Maybe>, productVariant?: AdminTypes.Maybe<( - Pick - & { product: Pick } - )>, userErrors: Array> }> }; - export type FileCreateMutationVariables = AdminTypes.Exact<{ files: Array | AdminTypes.FileCreateInput; }>; @@ -37,6 +27,13 @@ export type StagedUploadsCreateMutation = { stagedUploadsCreate?: AdminTypes.May & { parameters: Array> } )>>, userErrors: Array> }> }; +export type ProductVariantCreateMutationVariables = AdminTypes.Exact<{ + input: AdminTypes.ProductVariantInput; +}>; + + +export type ProductVariantCreateMutation = { productVariantCreate?: AdminTypes.Maybe<{ productVariant?: AdminTypes.Maybe>, userErrors: Array> }> }; + export type ProductVariantsBulkDeleteMutationVariables = AdminTypes.Exact<{ productId: AdminTypes.Scalars['ID']['input']; variantsIds: Array | AdminTypes.Scalars['ID']['input']; @@ -50,9 +47,9 @@ interface GeneratedQueryTypes { } interface GeneratedMutationTypes { - "#graphql\n mutation productVariantCreate($input: ProductVariantInput!) {\n productVariantCreate(input: $input) {\n product {\n id\n title\n }\n productVariant {\n createdAt\n displayName\n id\n inventoryPolicy\n inventoryQuantity\n price\n product {\n id\n }\n title\n }\n userErrors {\n field\n message\n }\n }\n }\n": {return: ProductVariantCreateMutation, variables: ProductVariantCreateMutationVariables}, "#graphql\n mutation fileCreate($files: [FileCreateInput!]!) {\n fileCreate(files: $files) {\n files {\n fileStatus\n alt\n }\n userErrors {\n field\n message\n }\n }\n }\n": {return: FileCreateMutation, variables: FileCreateMutationVariables}, "#graphql\n mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {\n stagedUploadsCreate(input: $input) {\n stagedTargets {\n resourceUrl\n url\n parameters {\n name\n value\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n": {return: StagedUploadsCreateMutation, variables: StagedUploadsCreateMutationVariables}, + "#graphql\n mutation productVariantCreate($input: ProductVariantInput!) {\n productVariantCreate(input: $input) {\n productVariant {\n id\n title\n }\n userErrors {\n field\n message\n }\n }\n }\n": {return: ProductVariantCreateMutation, variables: ProductVariantCreateMutationVariables}, "#graphql\n mutation productVariantsBulkDelete($productId: ID!, $variantsIds: [ID!]!) {\n productVariantsBulkDelete(productId: $productId, variantsIds: $variantsIds) {\n product {\n id\n title\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n": {return: ProductVariantsBulkDeleteMutation, variables: ProductVariantsBulkDeleteMutationVariables}, } declare module '@shopify/admin-api-client' { From 46f9ff6813ba83fc5df358a09c40fa52feabde44 Mon Sep 17 00:00:00 2001 From: Jamal Soueidan Date: Wed, 13 Mar 2024 13:17:30 +0100 Subject: [PATCH 07/10] Update package.json scripts and dependencies, refactor Shopify client usage - Modify `graphql:codegen` script to include `--watch` flag - Replace `@shopify/shopify-api` with `@shopify/admin-api-client` and update to newer versions - Update `applicationinsights` to version `2.9.5` - Refactor customer-upload orchestrator to use new types from `admin.generated` and `admin.types` - Simplify GraphQL queries and mutations in `customer-upload.orchestrator.ts` and `create-variant.ts` - Update Shopify client initialization to use `createAdminApiClient` from `@shopify/admin-api-client` - Remove unused types from `admin.generated.d.ts` --- package.json | 7 +- src/functions/customer-upload.orchestrator.ts | 94 +++++-------------- .../services/product/create-variant.ts | 34 ++++--- src/functions/webhook/product/product.ts | 22 ++--- src/library/shopify/index.ts | 24 ++--- src/types/admin.generated.d.ts | 7 +- 6 files changed, 60 insertions(+), 128 deletions(-) diff --git a/package.json b/package.json index 4af6ce4a..130cb2db 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,15 @@ "generate-docs": "npx @redocly/cli build-docs docs/openapi.yaml -o docs/index.html --title 'Booking Api Documentation'", "generate-ts": "npm run bundle && npx orval --config ./orval.config.js", "postprocess": "node postprocess.js", - "graphql:codegen": "npx graphql-codegen && npm run postprocess" + "graphql:codegen": "npx graphql-codegen -- --watch && npm run postprocess" }, "dependencies": { "@azure/functions": "^4.0.0", "@azure/storage-queue": "^12.16.0", - "@shopify/shopify-api": "^8.0.2", + "@shopify/admin-api-client": "^0.2.8", + "@shopify/shopify-api": "^9.5.0", "@types/jsonwebtoken": "^9.0.3", - "applicationinsights": "^2.9.1", + "applicationinsights": "^2.9.5", "axios": "^1.5.1", "bcryptjs": "^2.4.3", "date-fns": "^2.30.0", diff --git a/src/functions/customer-upload.orchestrator.ts b/src/functions/customer-upload.orchestrator.ts index 6d5e6bbf..20986f71 100644 --- a/src/functions/customer-upload.orchestrator.ts +++ b/src/functions/customer-upload.orchestrator.ts @@ -11,6 +11,8 @@ import { z } from "zod"; import { connect } from "~/library/mongoose"; import { shopifyAdmin } from "~/library/shopify"; import { NumberOrStringType } from "~/library/zod"; +import { FileGetQuery } from "~/types/admin.generated"; +import { FileContentType } from "~/types/admin.types"; import { CustomerUploadControllerResourceURL } from "./customer/controllers/upload/resource-url"; import { UserModel } from "./user"; @@ -41,7 +43,7 @@ df.app.orchestration("upload", function* (context: OrchestrationContext) { const maxRetries = 5; let attemptCount = 0; - let fileUploaded: PreviewImage | undefined; + let fileUploaded: FileGetQuery["files"]["nodes"][number] | undefined; while (!fileUploaded && attemptCount < maxRetries) { // Wait for 5 seconds before each new attempt @@ -53,7 +55,7 @@ df.app.orchestration("upload", function* (context: OrchestrationContext) { // Check if data is available from Shopify const response: Awaited> = yield context.df.callActivity("fileGet", body); - if (response.files.nodes.length > 0) { + if (response && response.files.nodes.length > 0) { fileUploaded = response.files.nodes[0]; } @@ -63,7 +65,7 @@ df.app.orchestration("upload", function* (context: OrchestrationContext) { if (fileUploaded) { return yield context.df.callActivity("updateCustomer", { customerId: body.customerId, - image: fileUploaded.preview.image, + image: fileUploaded.preview?.image, }); } @@ -74,9 +76,13 @@ df.app.orchestration("upload", function* (context: OrchestrationContext) { }; }); +type Node = FileGetQuery["files"]["nodes"][number]; +type PreviewType = NonNullable; +type ImageType = NonNullable; + type updateCustomer = { customerId: number; - image: PreviewImage["preview"]["image"]; + image: ImageType; }; df.app.activity("updateCustomer", { @@ -99,20 +105,17 @@ df.app.activity("updateCustomer", { }); async function fileCreate(input: Body) { - const response = await shopifyAdmin.query({ - data: { - query: FILE_CREATE, - variables: { - files: { - alt: getFilenameFromUrl(input.resourceUrl), - contentType: "IMAGE", - originalSource: input.resourceUrl, - }, + const { data } = await shopifyAdmin.request(FILE_CREATE, { + variables: { + files: { + alt: getFilenameFromUrl(input.resourceUrl), + contentType: FileContentType.Image, + originalSource: input.resourceUrl, }, }, }); - return response.body.data; + return data; } df.app.activity("fileCreate", { @@ -120,16 +123,13 @@ df.app.activity("fileCreate", { }); async function fileGet(input: Body) { - const fileGet = await shopifyAdmin.query({ - data: { - query: FILE_GET, - variables: { - query: getFilenameFromUrl(input.resourceUrl), - }, + const { data } = await shopifyAdmin.request(FILE_GET, { + variables: { + query: getFilenameFromUrl(input.resourceUrl) || "", }, }); - return fileGet.body.data; + return data; } df.app.activity("fileGet", { @@ -180,29 +180,6 @@ const FILE_CREATE = `#graphql } ` as const; -type FileCreateQuery = { - data: { - fileCreate: { - files: Array<{ - fileStatus: string; - alt: string; - }>; - userErrors: Array; - }; - }; - extensions: { - cost: { - requestedQueryCost: number; - actualQueryCost: number; - throttleStatus: { - maximumAvailable: number; - currentlyAvailable: number; - restoreRate: number; - }; - }; - }; -}; - const FILE_GET = `#graphql query FileGet($query: String!) { files(first: 10, sortKey: UPDATED_AT, reverse: true, query: $query) { @@ -218,32 +195,3 @@ const FILE_GET = `#graphql } } ` as const; - -type PreviewImage = { - preview: { - image: { - url: string; - width: number; - height: number; - }; - }; -}; - -type FileGetQuery = { - data: { - files: { - nodes: Array; - }; - }; - extensions: { - cost: { - requestedQueryCost: number; - actualQueryCost: number; - throttleStatus: { - maximumAvailable: number; - currentlyAvailable: number; - restoreRate: number; - }; - }; - }; -}; diff --git a/src/functions/customer/services/product/create-variant.ts b/src/functions/customer/services/product/create-variant.ts index 68404b73..23a96283 100644 --- a/src/functions/customer/services/product/create-variant.ts +++ b/src/functions/customer/services/product/create-variant.ts @@ -1,5 +1,4 @@ import { shopifyAdmin } from "~/library/shopify"; -import { ProductVariantCreateMutation } from "~/types/admin.generated"; export type CustomerProductServiceCreateVariantProps = { productId: number; @@ -10,30 +9,29 @@ export type CustomerProductServiceCreateVariantProps = { export const CustomerProductServiceCreateVariant = async ( props: CustomerProductServiceCreateVariantProps ) => { - const { body } = await shopifyAdmin.query<{ - data: ProductVariantCreateMutation; - }>({ - data: { - query: CREATE_VARIANT, - variables: { - input: { - price: props.price, - compareAtPrice: props.compareAtPrice, - productId: `gid://shopify/Product/${props.productId}`, - inventoryItem: { - tracked: false, - }, - options: `Artist ${props.price}.${props.compareAtPrice}`, + const { data } = await shopifyAdmin.request(CREATE_VARIANT, { + variables: { + input: { + price: props.price, + compareAtPrice: props.compareAtPrice, + productId: `gid://shopify/Product/${props.productId}`, + inventoryItem: { + tracked: false, }, + options: [`Artist ${props.price}.${props.compareAtPrice}`], }, }, }); - if (body.data.productVariantCreate?.userErrors) { - throw new Error(body.data.productVariantCreate.userErrors[0].message); + if ( + data && + data.productVariantCreate && + data.productVariantCreate?.userErrors.length > 0 + ) { + throw new Error(data.productVariantCreate.userErrors[0].message); } - return body.data.productVariantCreate?.productVariant; + return data?.productVariantCreate?.productVariant; }; const CREATE_VARIANT = `#graphql diff --git a/src/functions/webhook/product/product.ts b/src/functions/webhook/product/product.ts index 0cb4c365..fa55fe92 100644 --- a/src/functions/webhook/product/product.ts +++ b/src/functions/webhook/product/product.ts @@ -3,7 +3,6 @@ import { InvocationContext } from "@azure/functions"; import { telemetryClient } from "~/library/application-insight"; import { connect } from "~/library/mongoose"; import { shopifyAdmin } from "~/library/shopify"; -import { ProductVariantsBulkDeleteMutation } from "~/types/admin.generated"; import { ProductUpdateSchema } from "./types"; import { ProductWebHookGetUnusedVariantIds } from "./unused"; @@ -18,24 +17,19 @@ export async function webhookProductProcess( product, }); - const { body } = await shopifyAdmin.query<{ - data: ProductVariantsBulkDeleteMutation; - }>({ - data: { - query: MUTATION_DESTROY_VARIANTS, - variables: { - productId: product.admin_graphql_api_id, - variantsIds: unusedVariantIds.map( - (l) => `gid://shopify/ProductVariant/${l}` - ), - }, + const { data } = await shopifyAdmin.request(MUTATION_DESTROY_VARIANTS, { + variables: { + productId: product.admin_graphql_api_id, + variantsIds: unusedVariantIds.map( + (l) => `gid://shopify/ProductVariant/${l}` + ), }, }); - if (!body.data.productVariantsBulkDelete?.product) { + if (!data?.productVariantsBulkDelete?.product) { context.error( "webhook product error", - body.data.productVariantsBulkDelete?.userErrors + data?.productVariantsBulkDelete?.userErrors ); } diff --git a/src/library/shopify/index.ts b/src/library/shopify/index.ts index 528ad3fb..b8520a30 100644 --- a/src/library/shopify/index.ts +++ b/src/library/shopify/index.ts @@ -1,22 +1,10 @@ -import { LATEST_API_VERSION, shopifyApi } from "@shopify/shopify-api"; -import "@shopify/shopify-api/adapters/node"; +import { createAdminApiClient } from "@shopify/admin-api-client"; /** - * Create Spi's Storefront client. + * Create Shopify Admin client. */ -const shopify = shopifyApi({ - apiKey: process.env["ShopifyApiKey"] || "", - apiSecretKey: process.env["ShopifyApiSecretKey"] || "", - adminApiAccessToken: process.env["ShopifyApiAccessToken"] || "", - apiVersion: LATEST_API_VERSION, - isCustomStoreApp: true, - scopes: [], - isEmbeddedApp: false, - hostName: process.env["ShopifyStoreDomain"] || "", -}); - -export const shopifyAdmin = new shopify.clients.Graphql({ - session: shopify.session.customAppSession( - process.env["ShopifyStoreDomain"] || "" - ), +export const shopifyAdmin = createAdminApiClient({ + storeDomain: process.env["ShopifyStoreDomain"] || "", + accessToken: process.env["ShopifyApiAccessToken"] || "", + apiVersion: "2023-10", }); diff --git a/src/types/admin.generated.d.ts b/src/types/admin.generated.d.ts index d848079b..2f4fc613 100644 --- a/src/types/admin.generated.d.ts +++ b/src/types/admin.generated.d.ts @@ -32,7 +32,10 @@ export type ProductVariantCreateMutationVariables = AdminTypes.Exact<{ }>; -export type ProductVariantCreateMutation = { productVariantCreate?: AdminTypes.Maybe<{ productVariant?: AdminTypes.Maybe>, userErrors: Array> }> }; +export type ProductVariantCreateMutation = { productVariantCreate?: AdminTypes.Maybe<{ productVariant?: AdminTypes.Maybe<( + Pick + & { selectedOptions: Array> } + )>, userErrors: Array> }> }; export type ProductVariantsBulkDeleteMutationVariables = AdminTypes.Exact<{ productId: AdminTypes.Scalars['ID']['input']; @@ -49,7 +52,7 @@ interface GeneratedQueryTypes { interface GeneratedMutationTypes { "#graphql\n mutation fileCreate($files: [FileCreateInput!]!) {\n fileCreate(files: $files) {\n files {\n fileStatus\n alt\n }\n userErrors {\n field\n message\n }\n }\n }\n": {return: FileCreateMutation, variables: FileCreateMutationVariables}, "#graphql\n mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {\n stagedUploadsCreate(input: $input) {\n stagedTargets {\n resourceUrl\n url\n parameters {\n name\n value\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n": {return: StagedUploadsCreateMutation, variables: StagedUploadsCreateMutationVariables}, - "#graphql\n mutation productVariantCreate($input: ProductVariantInput!) {\n productVariantCreate(input: $input) {\n productVariant {\n id\n title\n }\n userErrors {\n field\n message\n }\n }\n }\n": {return: ProductVariantCreateMutation, variables: ProductVariantCreateMutationVariables}, + "#graphql\n mutation productVariantCreate($input: ProductVariantInput!) {\n productVariantCreate(input: $input) {\n productVariant {\n id\n title\n selectedOptions {\n name\n value\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n": {return: ProductVariantCreateMutation, variables: ProductVariantCreateMutationVariables}, "#graphql\n mutation productVariantsBulkDelete($productId: ID!, $variantsIds: [ID!]!) {\n productVariantsBulkDelete(productId: $productId, variantsIds: $variantsIds) {\n product {\n id\n title\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n": {return: ProductVariantsBulkDeleteMutation, variables: ProductVariantsBulkDeleteMutationVariables}, } declare module '@shopify/admin-api-client' { From fde4b0bb893e848d2df23b002117dd5b8e7f974d Mon Sep 17 00:00:00 2001 From: Jamal Soueidan Date: Wed, 13 Mar 2024 13:17:55 +0100 Subject: [PATCH 08/10] package-lock --- package-lock.json | 143 ++++++++++++++++++++++++---------------------- 1 file changed, 76 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index 57427eeb..5ddad918 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,10 @@ "dependencies": { "@azure/functions": "^4.0.0", "@azure/storage-queue": "^12.16.0", - "@shopify/shopify-api": "^8.0.2", + "@shopify/admin-api-client": "^0.2.8", + "@shopify/shopify-api": "^9.5.0", "@types/jsonwebtoken": "^9.0.3", - "applicationinsights": "^2.9.1", + "applicationinsights": "^2.9.5", "axios": "^1.5.1", "bcryptjs": "^2.4.3", "date-fns": "^2.30.0", @@ -2468,15 +2469,6 @@ "node": ">= 14" } }, - "node_modules/@graphql-tools/prisma-loader/node_modules/jose": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.3.tgz", - "integrity": "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/@graphql-tools/prisma-loader/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3219,17 +3211,17 @@ } }, "node_modules/@opentelemetry/core": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.18.1.tgz", - "integrity": "sha512-kvnUqezHMhsQvdsnhnqTNfAJs3ox/isB0SVrM1dhVFw7SsB7TstuVa6fgWnN2GdPyilIFLUvvbTZoVRmx6eiRg==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.22.0.tgz", + "integrity": "sha512-0VoAlT6x+Xzik1v9goJ3pZ2ppi6+xd3aUfg4brfrLkDBHRIVjMP0eBHrKrhB+NKcDyMAg8fAbGL3Npg/F6AwWA==", "dependencies": { - "@opentelemetry/semantic-conventions": "1.18.1" + "@opentelemetry/semantic-conventions": "1.22.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.8.0" + "@opentelemetry/api": ">=1.0.0 <1.9.0" } }, "node_modules/@opentelemetry/instrumentation": { @@ -3281,40 +3273,40 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/@opentelemetry/resources": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.18.1.tgz", - "integrity": "sha512-JjbcQLYMttXcIabflLRuaw5oof5gToYV9fuXbcsoOeQ0BlbwUn6DAZi++PNsSz2jjPeASfDls10iaO/8BRIPRA==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.22.0.tgz", + "integrity": "sha512-+vNeIFPH2hfcNL0AJk/ykJXoUCtR1YaDUZM+p3wZNU4Hq98gzq+7b43xbkXjadD9VhWIUQqEwXyY64q6msPj6A==", "dependencies": { - "@opentelemetry/core": "1.18.1", - "@opentelemetry/semantic-conventions": "1.18.1" + "@opentelemetry/core": "1.22.0", + "@opentelemetry/semantic-conventions": "1.22.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.8.0" + "@opentelemetry/api": ">=1.0.0 <1.9.0" } }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.18.1.tgz", - "integrity": "sha512-tRHfDxN5dO+nop78EWJpzZwHsN1ewrZRVVwo03VJa3JQZxToRDH29/+MB24+yoa+IArerdr7INFJiX/iN4gjqg==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.22.0.tgz", + "integrity": "sha512-pfTuSIpCKONC6vkTpv6VmACxD+P1woZf4q0K46nSUvXFvOFqjBYKFaAMkKD3M1mlKUUh0Oajwj35qNjMl80m1Q==", "dependencies": { - "@opentelemetry/core": "1.18.1", - "@opentelemetry/resources": "1.18.1", - "@opentelemetry/semantic-conventions": "1.18.1" + "@opentelemetry/core": "1.22.0", + "@opentelemetry/resources": "1.22.0", + "@opentelemetry/semantic-conventions": "1.22.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.8.0" + "@opentelemetry/api": ">=1.0.0 <1.9.0" } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.18.1.tgz", - "integrity": "sha512-+NLGHr6VZwcgE/2lw8zDIufOCGnzsA5CbQIMleXZTrgkBd0TanCX+MiDYJ1TOS4KL/Tqk0nFRxawnaYr6pkZkA==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.22.0.tgz", + "integrity": "sha512-CAOgFOKLybd02uj/GhCdEeeBjOS0yeoDeo/CA7ASBSmenpZHAKGB3iDm/rv3BQLcabb/OprDEsSQ1y0P8A7Siw==", "engines": { "node": ">=14" } @@ -3803,6 +3795,14 @@ "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", "dev": true }, + "node_modules/@shopify/admin-api-client": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@shopify/admin-api-client/-/admin-api-client-0.2.8.tgz", + "integrity": "sha512-UyGhssHLw8Cwl7ai9SFtiZj0A78Ldt0qKotKg7r95mNTfKLao01oVjJFOZCZfFK+1x7ePUdh+UD+pbsb17KshA==", + "dependencies": { + "@shopify/graphql-client": "^0.10.3" + } + }, "node_modules/@shopify/api-codegen-preset": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/@shopify/api-codegen-preset/-/api-codegen-preset-0.0.5.tgz", @@ -3817,6 +3817,11 @@ "graphql": "^16.8.1" } }, + "node_modules/@shopify/graphql-client": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@shopify/graphql-client/-/graphql-client-0.10.3.tgz", + "integrity": "sha512-w9noa5wbuyQegOdmqR5Yfj0GQJKmwuDiqIzDETgZhTMcmV45CNxVRxCWx6BQt3MpLzgk0A8G9iaoppfHXzkhfA==" + }, "node_modules/@shopify/graphql-codegen": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@shopify/graphql-codegen/-/graphql-codegen-0.0.1.tgz", @@ -3856,24 +3861,21 @@ } }, "node_modules/@shopify/shopify-api": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@shopify/shopify-api/-/shopify-api-8.0.2.tgz", - "integrity": "sha512-hvVLoEsYglE4GRqFhr9D6oMr2bV6tEdsD9PxuNZ6bYDptoD+kQFKsaP83jE1qtHhB3ve0DeevaVVYjS/2TU7MA==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@shopify/shopify-api/-/shopify-api-9.5.0.tgz", + "integrity": "sha512-DqfkeHJn3uch3ujSzku9R2Q3rsCtW7QHbyPHIahORQsQVKj+YAhkDjw1V+pMQRTbveL2HHW6D2r8GQvEPOU2Gw==", "dependencies": { + "@shopify/admin-api-client": "^0.2.8", "@shopify/network": "^3.2.1", - "compare-versions": "^5.0.3", - "isbot": "^3.6.10", - "jose": "^4.9.1", + "@shopify/storefront-api-client": "^0.3.3", + "compare-versions": "^6.1.0", + "isbot": "^4.4.0", + "jose": "^5.2.2", "node-fetch": "^2.6.1", "tslib": "^2.0.3", "uuid": "^9.0.0" } }, - "node_modules/@shopify/shopify-api/node_modules/compare-versions": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", - "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==" - }, "node_modules/@shopify/shopify-api/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -3886,6 +3888,14 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@shopify/storefront-api-client": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@shopify/storefront-api-client/-/storefront-api-client-0.3.3.tgz", + "integrity": "sha512-yoZul8r2mhb/XCDh55UxKNuo/PDB+bldTOt+rRz9uCKC9WkVQfGfMLwJhKb92i3CT/cxWNREPqEN5z1H+B9+IQ==", + "dependencies": { + "@shopify/graphql-client": "^0.10.3" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -4796,23 +4806,23 @@ } }, "node_modules/applicationinsights": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.9.1.tgz", - "integrity": "sha512-hrpe/OvHFZlq+SQERD1fxaYICyunxzEBh9SolJebzYnIXkyA9zxIR87dZAh+F3+weltbqdIP8W038cvtpMNhQg==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.9.5.tgz", + "integrity": "sha512-APQ8IWyYDHFvKbitFKpsmZXxkzQh0yYTFacQqoVW7HwlPo3eeLprwnq5RFNmmG6iqLmvQ+xRJSDLEQCgqPh+bw==", "dependencies": { "@azure/core-auth": "^1.5.0", "@azure/core-rest-pipeline": "1.10.1", "@azure/core-util": "1.2.0", "@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.5", - "@microsoft/applicationinsights-web-snippet": "^1.0.1", - "@opentelemetry/api": "^1.4.1", - "@opentelemetry/core": "^1.15.2", - "@opentelemetry/sdk-trace-base": "^1.15.2", - "@opentelemetry/semantic-conventions": "^1.15.2", + "@microsoft/applicationinsights-web-snippet": "1.0.1", + "@opentelemetry/api": "^1.7.0", + "@opentelemetry/core": "^1.19.0", + "@opentelemetry/sdk-trace-base": "^1.19.0", + "@opentelemetry/semantic-conventions": "^1.19.0", "cls-hooked": "^4.2.2", "continuation-local-storage": "^3.2.1", "diagnostic-channel": "1.1.1", - "diagnostic-channel-publishers": "1.0.7" + "diagnostic-channel-publishers": "1.0.8" }, "engines": { "node": ">=8.0.0" @@ -5770,8 +5780,7 @@ "node_modules/compare-versions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz", - "integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==", - "dev": true + "integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==" }, "node_modules/concat-map": { "version": "0.0.1", @@ -6095,9 +6104,9 @@ } }, "node_modules/diagnostic-channel-publishers": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.7.tgz", - "integrity": "sha512-SEECbY5AiVt6DfLkhkaHNeshg1CogdLLANA8xlG/TKvS+XUgvIKl7VspJGYiEdL5OUyzMVnr7o0AwB7f+/Mjtg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.8.tgz", + "integrity": "sha512-HmSm9hXxSPxA9BaLGY98QU1zsdjeCk113KjAYGPCen1ZP6mhVaTPzHd6UYv5r21DnWANi+f+NyPOHruGT9jpqQ==", "peerDependencies": { "diagnostic-channel": "*" } @@ -6114,9 +6123,9 @@ } }, "node_modules/diagnostic-channel/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -8446,11 +8455,11 @@ "dev": true }, "node_modules/isbot": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/isbot/-/isbot-3.7.0.tgz", - "integrity": "sha512-9BcjlI89966BqWJmYdTnRub85sit931MyCthSIPtgoOsTjoW7A2MVa09HzPpYE2+G4vyAxfDvR0AbUGV0FInQg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-4.4.0.tgz", + "integrity": "sha512-8ZvOWUA68kyJO4hHJdWjyreq7TYNWTS9y15IzeqVdKxR9pPr3P/3r9AHcoIv9M0Rllkao5qWz2v1lmcyKIVCzQ==", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/isexe": { @@ -9206,9 +9215,9 @@ } }, "node_modules/jose": { - "version": "4.15.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", - "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.3.tgz", + "integrity": "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA==", "funding": { "url": "https://github.com/sponsors/panva" } From 457003f00dcab43f062d86f50967232171716993 Mon Sep 17 00:00:00 2001 From: Jamal Soueidan Date: Wed, 13 Mar 2024 14:08:27 +0100 Subject: [PATCH 09/10] Add create-variant endpoint to customer product API with request/response schemas --- openapi/openapi.yaml | 8 ++++ .../customer/product/create-variant/body.yaml | 10 +++++ .../product/create-variant/index.yaml | 41 +++++++++++++++++++ .../product/create-variant/response.yaml | 10 +++++ .../product/create-variant/variant.yaml | 10 +++++ 5 files changed, 79 insertions(+) create mode 100644 openapi/paths/customer/product/create-variant/body.yaml create mode 100644 openapi/paths/customer/product/create-variant/index.yaml create mode 100644 openapi/paths/customer/product/create-variant/response.yaml create mode 100644 openapi/paths/customer/product/create-variant/variant.yaml diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index ff8cb552..dddea5b6 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -62,6 +62,12 @@ components: $ref: paths/customer/product/destroy/response.yaml CustomerProductGetResponse: $ref: paths/customer/product/get/response.yaml + CustomerProductCreateVariantResponse: + $ref: paths/customer/product/create-variant/response.yaml + CustomerProductCreateVariantBody: + $ref: paths/customer/product/create-variant/body.yaml + CustomerProductCreateVariant: + $ref: paths/customer/product/create-variant/variant.yaml # Schedule CustomerScheduleDestroy: @@ -351,6 +357,8 @@ paths: $ref: "./paths/customer/product/list-ids/index.yaml" /customer/{customerId}/product/{productId}: $ref: "paths/customer/product/product.yaml" + /customer/{customerId}/product/{productId}/create-variant: + $ref: "paths/customer/product/create-variant/index.yaml" # Orders /customer/{customerId}/bookings/{orderId}/group/{groupId}: diff --git a/openapi/paths/customer/product/create-variant/body.yaml b/openapi/paths/customer/product/create-variant/body.yaml new file mode 100644 index 00000000..1869d600 --- /dev/null +++ b/openapi/paths/customer/product/create-variant/body.yaml @@ -0,0 +1,10 @@ +type: object +properties: + price: + type: string + compareAtPrice: + type: string + +required: + - price + - compareAtPrice diff --git a/openapi/paths/customer/product/create-variant/index.yaml b/openapi/paths/customer/product/create-variant/index.yaml new file mode 100644 index 00000000..2c419ca5 --- /dev/null +++ b/openapi/paths/customer/product/create-variant/index.yaml @@ -0,0 +1,41 @@ +post: + parameters: + - name: customerId + in: path + required: true + schema: + type: string + - name: productId + in: path + required: true + schema: + type: string + tags: + - CustomerProduct + operationId: customerProductCreateVariant + summary: POST create product variant + description: This endpoint create product variant + requestBody: + required: true + content: + application/json: + schema: + $ref: "./body.yaml" + + responses: + "200": + description: Response with product variant payload + content: + application/json: + schema: + $ref: "./response.yaml" + "400": + $ref: "../../../../responses/bad.yaml" + "401": + $ref: "../../../../responses/unauthorized.yaml" + "403": + $ref: "../../../../responses/forbidden.yaml" + "404": + $ref: "../../../../responses/not-found.yaml" + + security: [] diff --git a/openapi/paths/customer/product/create-variant/response.yaml b/openapi/paths/customer/product/create-variant/response.yaml new file mode 100644 index 00000000..bf86ee09 --- /dev/null +++ b/openapi/paths/customer/product/create-variant/response.yaml @@ -0,0 +1,10 @@ +type: object +properties: + success: + type: boolean + example: true + payload: + $ref: ./variant.yaml +required: + - success + - payload diff --git a/openapi/paths/customer/product/create-variant/variant.yaml b/openapi/paths/customer/product/create-variant/variant.yaml new file mode 100644 index 00000000..419be7e0 --- /dev/null +++ b/openapi/paths/customer/product/create-variant/variant.yaml @@ -0,0 +1,10 @@ +type: object +properties: + id: + type: number + title: + type: string + selectedOptions: + type: array + items: + $ref: ../../schedule/_types/product-selected-options.yaml From 123717fc2c3003ae09b770f92e934d94a9287603 Mon Sep 17 00:00:00 2001 From: Jamal Soueidan Date: Wed, 13 Mar 2024 14:08:33 +0100 Subject: [PATCH 10/10] Refactor imports and improve error handling in upload orchestrator and services --- src/functions/customer-upload.orchestrator.ts | 5 +-- .../controllers/upload/resource-url.ts | 40 +++++++++++-------- .../services/product/create-variant.ts | 2 +- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/functions/customer-upload.orchestrator.ts b/src/functions/customer-upload.orchestrator.ts index 20986f71..73f96775 100644 --- a/src/functions/customer-upload.orchestrator.ts +++ b/src/functions/customer-upload.orchestrator.ts @@ -11,8 +11,7 @@ import { z } from "zod"; import { connect } from "~/library/mongoose"; import { shopifyAdmin } from "~/library/shopify"; import { NumberOrStringType } from "~/library/zod"; -import { FileGetQuery } from "~/types/admin.generated"; -import { FileContentType } from "~/types/admin.types"; +import { type FileGetQuery } from "~/types/admin.generated"; import { CustomerUploadControllerResourceURL } from "./customer/controllers/upload/resource-url"; import { UserModel } from "./user"; @@ -109,7 +108,7 @@ async function fileCreate(input: Body) { variables: { files: { alt: getFilenameFromUrl(input.resourceUrl), - contentType: FileContentType.Image, + contentType: "IMAGE" as any, originalSource: input.resourceUrl, }, }, diff --git a/src/functions/customer/controllers/upload/resource-url.ts b/src/functions/customer/controllers/upload/resource-url.ts index 7af3efac..1552cdff 100644 --- a/src/functions/customer/controllers/upload/resource-url.ts +++ b/src/functions/customer/controllers/upload/resource-url.ts @@ -19,26 +19,32 @@ export type CustomerUploadControllerResourceURLResponse = Awaited< export const CustomerUploadControllerResourceURL = _( async ({ query }: CustomerUploadControllerResourceURLRequest) => { - const data = CustomerUploadControllerResourceURLQuerySchema.parse(query); - const response = await shopifyAdmin.query({ - data: { - query: UPLOAD_CREATE, - variables: { - input: [ - { - resource: "IMAGE", - filename: `${ - data.customerId - }_customer_profile_${new Date().getTime()}.jpg`, - mimeType: "image/jpeg", - httpMethod: "POST", - }, - ], - }, + const validateData = + CustomerUploadControllerResourceURLQuerySchema.parse(query); + const { data } = await shopifyAdmin.request(UPLOAD_CREATE, { + variables: { + input: [ + { + resource: "IMAGE" as any, + filename: `${ + validateData.customerId + }_customer_profile_${new Date().getTime()}.jpg`, + mimeType: "image/jpeg", + httpMethod: "POST" as any, + }, + ], }, }); - return response.body.data.stagedUploadsCreate.stagedTargets[0]; + if ( + !data || + !data.stagedUploadsCreate || + !data?.stagedUploadsCreate?.stagedTargets + ) { + throw new Error("something went wrong with uploading image"); + } + + return data?.stagedUploadsCreate?.stagedTargets[0]; } ); diff --git a/src/functions/customer/services/product/create-variant.ts b/src/functions/customer/services/product/create-variant.ts index 23a96283..2eef9c3a 100644 --- a/src/functions/customer/services/product/create-variant.ts +++ b/src/functions/customer/services/product/create-variant.ts @@ -28,7 +28,7 @@ export const CustomerProductServiceCreateVariant = async ( data.productVariantCreate && data.productVariantCreate?.userErrors.length > 0 ) { - throw new Error(data.productVariantCreate.userErrors[0].message); + throw data.productVariantCreate.userErrors[0]; } return data?.productVariantCreate?.productVariant;