diff --git a/src/functions/customer-location.function.ts b/src/functions/customer-location.function.ts index 987d1b55..de97d202 100644 --- a/src/functions/customer-location.function.ts +++ b/src/functions/customer-location.function.ts @@ -38,6 +38,7 @@ app.http("customerLocationUpdate", { authLevel: "anonymous", route: "customer/{customerId}/location/{locationId?}", handler: CustomerLocationControllerUpdate, + extraInputs: [df.input.durableClient()], }); app.http("customerLocationCreate", { diff --git a/src/functions/customer/controllers/location/update.ts b/src/functions/customer/controllers/location/update.ts index 1f1aece3..edf51d30 100644 --- a/src/functions/customer/controllers/location/update.ts +++ b/src/functions/customer/controllers/location/update.ts @@ -1,14 +1,16 @@ import { z } from "zod"; import { LocationZodSchema } from "~/functions/location/location.types"; +import { InvocationContext } from "@azure/functions"; import { _ } from "~/library/handler"; import { StringOrObjectId } from "~/library/zod"; +import { CustomerLocationUpdateOrchestration } from "../../orchestrations/location/update"; import { CustomerLocationServiceUpdate } from "../../services/location/update"; -// should be PATCH and UPSERT export type CustomerLocationControllerUpdateRequest = { query: z.infer; body: z.infer; + context: InvocationContext; }; export const CustomerLocationControllerUpdateBodySchema = @@ -30,12 +32,22 @@ export type CustomerLocationControllerUpdateResponse = Awaited< >; export const CustomerLocationControllerUpdate = _( - ({ query, body }: CustomerLocationControllerUpdateRequest) => { + async ({ query, body, context }: CustomerLocationControllerUpdateRequest) => { const validateData = CustomerLocationControllerUpdateQuerySchema.parse(query); const validateBody = CustomerLocationControllerUpdateBodySchema.parse(body) || {}; - return CustomerLocationServiceUpdate(validateData, validateBody); + const location = await CustomerLocationServiceUpdate( + validateData, + validateBody + ); + + await CustomerLocationUpdateOrchestration( + { locationId: location._id }, + context + ); + + return location; } ); diff --git a/src/functions/customer/orchestrations/location/create/create-location-metafield.spec.ts b/src/functions/customer/orchestrations/location/create/create-location-metafield.spec.ts index 64410e75..f201e883 100644 --- a/src/functions/customer/orchestrations/location/create/create-location-metafield.spec.ts +++ b/src/functions/customer/orchestrations/location/create/create-location-metafield.spec.ts @@ -21,12 +21,12 @@ jest.mock("~/library/shopify", () => ({ const mockRequest = shopifyAdmin().request as jest.Mock; -describe("CustomerUpdateOrchestration", () => { +describe("CustomerLocationCreateOrchestration", () => { beforeAll(async () => { jest.clearAllMocks(); }); - it("updateUserMetaobject", async () => { + it("createLocationMetafield", async () => { const coordinates = { longitude: 10.12961271, latitude: 56.15563438, @@ -101,6 +101,10 @@ describe("CustomerUpdateOrchestration", () => { value: location.startFee.toString(), key: "start_fee", }, + { + key: "geo_location", + value: JSON.stringify(location.geoLocation), + }, ], }, }, @@ -163,6 +167,10 @@ describe("CustomerUpdateOrchestration", () => { value: location.startFee.toString(), key: "start_fee", }, + { + key: "geo_location", + value: JSON.stringify(location.geoLocation), + }, ], }), }); diff --git a/src/functions/customer/orchestrations/location/create/create-location-metafield.ts b/src/functions/customer/orchestrations/location/create/create-location-metafield.ts index 9472aae4..2a3d4989 100644 --- a/src/functions/customer/orchestrations/location/create/create-location-metafield.ts +++ b/src/functions/customer/orchestrations/location/create/create-location-metafield.ts @@ -68,6 +68,10 @@ export const createLocationMetafield = async ({ key: "start_fee", value: location.startFee.toString(), }, + { + key: "geo_location", + value: JSON.stringify(location.geoLocation), + }, ], }, }); diff --git a/src/functions/customer/orchestrations/location/update.ts b/src/functions/customer/orchestrations/location/update.ts new file mode 100644 index 00000000..24aa75a0 --- /dev/null +++ b/src/functions/customer/orchestrations/location/update.ts @@ -0,0 +1,50 @@ +import { InvocationContext } from "@azure/functions"; +import * as df from "durable-functions"; +import { OrchestrationContext } from "durable-functions"; +import { activityType } from "~/library/orchestration"; +import { StringOrObjectIdType } from "~/library/zod"; +import { + updateLocationMetafield, + updateLocationMetafieldName, +} from "./update/update-location-metafield"; + +df.app.activity(updateLocationMetafieldName, { + handler: updateLocationMetafield, +}); + +const orchestrator: df.OrchestrationHandler = function* ( + context: OrchestrationContext +) { + const input = context.df.getInput() as Input; + + const metafield: Awaited> = + yield context.df.callActivity( + updateLocationMetafieldName, + activityType(input) + ); + + return { metafield }; +}; + +df.app.orchestration("CustomerLocationUpdateOrchestration", orchestrator); + +type Input = { + locationId: StringOrObjectIdType; +}; + +export const CustomerLocationUpdateOrchestration = async ( + input: Input, + context: InvocationContext +): Promise => { + const client = df.getClient(context); + const instanceId: string = await client.startNew( + "CustomerLocationUpdateOrchestration", + { + input, + } + ); + + context.log(`Started orchestration with ID = '${instanceId}'.`); + + return instanceId; +}; diff --git a/src/functions/customer/orchestrations/location/update/update-location-metafield.spec.ts b/src/functions/customer/orchestrations/location/update/update-location-metafield.spec.ts new file mode 100644 index 00000000..2c078bd0 --- /dev/null +++ b/src/functions/customer/orchestrations/location/update/update-location-metafield.spec.ts @@ -0,0 +1,176 @@ +import { LocationOriginTypes, LocationTypes } from "~/functions/location"; +import { createLocation } from "~/library/jest/helpers/location"; +import { ensureType } from "~/library/jest/helpers/mock"; +import { shopifyAdmin } from "~/library/shopify"; +import { + UpdateLocationMetaobjectMutation, + UpdateLocationMetaobjectMutationVariables, +} from "~/types/admin.generated"; +import { + UPDATE_LOCATION_METAOBJECT, + updateLocationMetafield, +} from "./update-location-metafield"; + +require("~/library/jest/mongoose/mongodb.jest"); + +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ + request: jest.fn(), + }), +})); + +const mockRequest = shopifyAdmin().request as jest.Mock; + +describe("CustomerLocationUpdateOrchestration", () => { + beforeAll(async () => { + jest.clearAllMocks(); + }); + + it("updateLocationMetafield", async () => { + const coordinates = { + longitude: 10.12961271, + latitude: 56.15563438, + fullAddress: "Sigridsvej 45, 1. th, 8220 Brabrand", + city: "Aarhus", + country: "Denmark", + }; + + const location = await createLocation({ + name: "Falafel", + fullAddress: "Sigridsvej 45, 1. th, 8220 Brabrand", + originType: LocationOriginTypes.COMMERCIAL, + locationType: LocationTypes.ORIGIN, + customerId: 12, + distanceHourlyRate: 1, + fixedRatePerKm: 10, + distanceForFree: 10, + }); + + mockRequest.mockResolvedValueOnce({ + data: ensureType({ + metaobjectUpdate: { + metaobject: { + fields: [ + { + value: location.locationType, + key: "location_type", + }, + { + value: location.name, + key: "name", + }, + { + value: coordinates.fullAddress, + key: "full_address", + }, + { + value: coordinates.city, + key: "city", + }, + { + value: coordinates.country, + key: "country", + }, + { + value: location.originType, + key: "origin_type", + }, + { + value: location.distanceForFree.toString(), + key: "distance_for_free", + }, + { + value: location.distanceHourlyRate.toString(), + key: "distance_hourly_rate", + }, + { + value: location.fixedRatePerKm.toString(), + key: "fixed_rate_per_km", + }, + { + value: location.minDriveDistance.toString(), + key: "min_drive_distance", + }, + { + value: location.maxDriveDistance.toString(), + key: "max_drive_distance", + }, + { + value: location.startFee.toString(), + key: "start_fee", + }, + { + key: "geo_location", + value: JSON.stringify(location.geoLocation), + }, + ], + }, + }, + }), + }); + + await updateLocationMetafield({ locationId: location._id }); + + expect(mockRequest).toHaveBeenCalledTimes(1); + + expect(mockRequest).toHaveBeenNthCalledWith(1, UPDATE_LOCATION_METAOBJECT, { + variables: ensureType({ + id: location.metafieldId || "", + fields: [ + { + value: location.locationType, + key: "location_type", + }, + { + value: location.name, + key: "name", + }, + { + value: location.fullAddress, + key: "full_address", + }, + { + value: location.city, + key: "city", + }, + { + value: location.country, + key: "country", + }, + { + value: location.originType, + key: "origin_type", + }, + { + value: location.distanceForFree.toString(), + key: "distance_for_free", + }, + { + value: location.distanceHourlyRate.toString(), + key: "distance_hourly_rate", + }, + { + value: location.fixedRatePerKm.toString(), + key: "fixed_rate_per_km", + }, + { + value: location.minDriveDistance.toString(), + key: "min_drive_distance", + }, + { + value: location.maxDriveDistance.toString(), + key: "max_drive_distance", + }, + { + value: location.startFee.toString(), + key: "start_fee", + }, + { + key: "geo_location", + value: JSON.stringify(location.geoLocation), + }, + ], + }), + }); + }); +}); diff --git a/src/functions/customer/orchestrations/location/update/update-location-metafield.ts b/src/functions/customer/orchestrations/location/update/update-location-metafield.ts new file mode 100644 index 00000000..29ded3c9 --- /dev/null +++ b/src/functions/customer/orchestrations/location/update/update-location-metafield.ts @@ -0,0 +1,97 @@ +import { LocationModel } from "~/functions/location"; +import { shopifyAdmin } from "~/library/shopify"; +import { StringOrObjectIdType } from "~/library/zod"; + +export const updateLocationMetafieldName = "updateLocationMetafield"; +export const updateLocationMetafield = async ({ + locationId, +}: { + locationId: StringOrObjectIdType; +}) => { + const location = await LocationModel.findById(locationId); + + if (!location || !location.metafieldId) { + throw new Error( + `Failed to find locations to create metafield ${locationId}` + ); + } + + const { data } = await shopifyAdmin().request(UPDATE_LOCATION_METAOBJECT, { + variables: { + id: location.metafieldId, + fields: [ + { + key: "location_type", + value: location.locationType, + }, + { + key: "name", + value: location.name, + }, + { + key: "full_address", + value: location.fullAddress, + }, + { + key: "city", + value: location.city, + }, + { + key: "country", + value: location.country, + }, + { + key: "origin_type", + value: location.originType, + }, + { + key: "distance_for_free", + value: location.distanceForFree.toString(), + }, + { + key: "distance_hourly_rate", + value: location.distanceHourlyRate.toString(), + }, + { + key: "fixed_rate_per_km", + value: location.fixedRatePerKm.toString(), + }, + { + key: "min_drive_distance", + value: location.minDriveDistance.toString(), + }, + { + key: "max_drive_distance", + value: location.maxDriveDistance.toString(), + }, + { + key: "start_fee", + value: location.startFee.toString(), + }, + { + key: "geo_location", + value: JSON.stringify(location.geoLocation), + }, + ], + }, + }); + + if (!data?.metaobjectUpdate?.metaobject) { + throw new Error(`Failed to update metafield for location ${location}`); + } + + return data?.metaobjectUpdate?.metaobject; +}; + +export const UPDATE_LOCATION_METAOBJECT = `#graphql + mutation UpdateLocationMetaobject($id: ID!, $fields: [MetaobjectFieldInput!]!) { + metaobjectUpdate(id: $id, metaobject: {fields: $fields}) { + metaobject { + fields { + value + key + } + } + } + } +` as const; diff --git a/src/functions/customer/services/location/update.spec.ts b/src/functions/customer/services/location/update.spec.ts index 19dd7413..bd66025f 100644 --- a/src/functions/customer/services/location/update.spec.ts +++ b/src/functions/customer/services/location/update.spec.ts @@ -1,9 +1,6 @@ import { faker } from "@faker-js/faker"; import { LocationServiceGetCoordinates } from "~/functions/location/services/get-coordinates"; import { createLocation } from "~/library/jest/helpers/location"; -import { ensureType } from "~/library/jest/helpers/mock"; -import { shopifyAdmin } from "~/library/shopify"; -import { UpdateLocationMetaobjectMutation } from "~/types/admin.generated"; import { CustomerLocationServiceUpdate } from "./update"; require("~/library/jest/mongoose/mongodb.jest"); @@ -12,13 +9,6 @@ jest.mock("~/functions/location/services/get-coordinates", () => ({ LocationServiceGetCoordinates: jest.fn(), })); -jest.mock("~/library/shopify", () => ({ - shopifyAdmin: jest.fn().mockReturnValue({ - request: jest.fn(), - }), -})); -const mockRequest = shopifyAdmin().request as jest.Mock; - type LocationServiceGetCoordinatesMock = jest.Mock< Promise<{ longitude: number; @@ -52,65 +42,6 @@ describe("CustomerLocationServiceUpdate", () => { mockGetCoordinates.mockResolvedValue(coordinates); - mockRequest.mockResolvedValueOnce({ - data: ensureType({ - metaobjectUpdate: { - metaobject: { - fields: [ - { - value: document.locationType, - key: "location_type", - }, - { - value: document.name, - key: "name", - }, - { - value: coordinates.fullAddress, - key: "full_address", - }, - { - value: coordinates.city, - key: "city", - }, - { - value: coordinates.country, - key: "country", - }, - { - value: document.originType, - key: "origin_type", - }, - { - value: document.distanceForFree.toString(), - key: "distance_for_free", - }, - { - value: document.distanceHourlyRate.toString(), - key: "distance_hourly_rate", - }, - { - value: document.fixedRatePerKm.toString(), - key: "fixed_rate_per_km", - }, - { - value: document.minDriveDistance.toString(), - key: "min_drive_distance", - }, - { - value: document.maxDriveDistance.toString(), - key: "max_drive_distance", - }, - { - value: document.startFee.toString(), - key: "start_fee", - }, - ], - }, - }, - }), - }); - const update = await CustomerLocationServiceUpdate( { locationId: document._id, customerId }, { diff --git a/src/functions/customer/services/location/update.ts b/src/functions/customer/services/location/update.ts index cbbda48d..8bb34024 100644 --- a/src/functions/customer/services/location/update.ts +++ b/src/functions/customer/services/location/update.ts @@ -2,7 +2,6 @@ import mongoose from "mongoose"; import { ILocation, LocationModel } from "~/functions/location"; import { LocationServiceGetCoordinates } from "~/functions/location/services/get-coordinates"; import { NotFoundError } from "~/library/handler"; -import { shopifyAdmin } from "~/library/shopify"; import { StringOrObjectIdType } from "~/library/zod"; export type CustomerLocationUpdateFilterProps = { @@ -46,7 +45,7 @@ export const CustomerLocationServiceUpdate = async ( }; } - const updateLocation = await LocationModel.findOneAndUpdate( + return await LocationModel.findOneAndUpdate( { _id: new mongoose.Types.ObjectId(filter.locationId), customerId: filter.customerId, @@ -62,77 +61,4 @@ export const CustomerLocationServiceUpdate = async ( }, ]) ); - - if (updateLocation.metafieldId) { - await shopifyAdmin().request(UPDATE_LOCATION_METAOBJECT, { - variables: { - id: updateLocation.metafieldId, - fields: [ - { - key: "location_type", - value: updateLocation.locationType, - }, - { - key: "name", - value: updateLocation.name, - }, - { - key: "full_address", - value: updateLocation.fullAddress, - }, - { - key: "city", - value: updateLocation.city, - }, - { - key: "country", - value: updateLocation.country, - }, - { - key: "origin_type", - value: updateLocation.originType, - }, - { - key: "distance_for_free", - value: updateLocation.distanceForFree.toString(), - }, - { - key: "distance_hourly_rate", - value: updateLocation.distanceHourlyRate.toString(), - }, - { - key: "fixed_rate_per_km", - value: updateLocation.fixedRatePerKm.toString(), - }, - { - key: "min_drive_distance", - value: updateLocation.minDriveDistance.toString(), - }, - { - key: "max_drive_distance", - value: updateLocation.maxDriveDistance.toString(), - }, - { - key: "start_fee", - value: updateLocation.startFee.toString(), - }, - ], - }, - }); - } - - return updateLocation; }; - -export const UPDATE_LOCATION_METAOBJECT = `#graphql - mutation UpdateLocationMetaobject($id: ID!, $fields: [MetaobjectFieldInput!]!) { - metaobjectUpdate(id: $id, metaobject: {fields: $fields}) { - metaobject { - fields { - value - key - } - } - } - } -` as const; diff --git a/src/types/admin.generated.d.ts b/src/types/admin.generated.d.ts index 35de31e8..08e6ef26 100644 --- a/src/types/admin.generated.d.ts +++ b/src/types/admin.generated.d.ts @@ -77,6 +77,14 @@ export type CreateLocationMetaobjectMutation = { metaobjectCreate?: AdminTypes.M & { fields: Array> } )> }> }; +export type UpdateLocationMetaobjectMutationVariables = AdminTypes.Exact<{ + id: AdminTypes.Scalars['ID']['input']; + fields: Array | AdminTypes.MetaobjectFieldInput; +}>; + + +export type UpdateLocationMetaobjectMutation = { metaobjectUpdate?: AdminTypes.Maybe<{ metaobject?: AdminTypes.Maybe<{ fields: Array> }> }> }; + export type ProductParentUpdateMutationVariables = AdminTypes.Exact<{ id?: AdminTypes.InputMaybe; metafields?: AdminTypes.InputMaybe | AdminTypes.MetafieldInput>; @@ -166,14 +174,6 @@ export type UpdateScheduleMetaobjectMutationVariables = AdminTypes.Exact<{ export type UpdateScheduleMetaobjectMutation = { metaobjectUpdate?: AdminTypes.Maybe<{ metaobject?: AdminTypes.Maybe<{ fields: Array> }> }> }; -export type UpdateLocationMetaobjectMutationVariables = AdminTypes.Exact<{ - id: AdminTypes.Scalars['ID']['input']; - fields: Array | AdminTypes.MetaobjectFieldInput; -}>; - - -export type UpdateLocationMetaobjectMutation = { metaobjectUpdate?: AdminTypes.Maybe<{ metaobject?: AdminTypes.Maybe<{ fields: Array> }> }> }; - export type ProductOptionFragmentFragment = ( Pick & { required?: AdminTypes.Maybe>, parentId?: AdminTypes.Maybe>, variants: { nodes: Array<( @@ -246,6 +246,7 @@ interface GeneratedMutationTypes { "#graphql\n mutation PublishablePublish($collectionId: ID!, $publicationId: ID!) {\n publishablePublish(id: $collectionId, input: {publicationId: $publicationId}) {\n publishable {\n ... on Collection {\n id\n handle\n }\n }\n }\n }\n": {return: PublishablePublishMutation, variables: PublishablePublishMutationVariables}, "#graphql\n mutation UpdateUserMetaobject($id: ID!, $fields: [MetaobjectFieldInput!]!) {\n metaobjectUpdate(id: $id, metaobject: {fields: $fields}) {\n metaobject {\n fields {\n value\n key\n }\n }\n }\n }\n": {return: UpdateUserMetaobjectMutation, variables: UpdateUserMetaobjectMutationVariables}, "#graphql\n mutation CreateLocationMetaobject($handle: String!, $fields: [MetaobjectFieldInput!]) {\n metaobjectCreate(\n metaobject: {type: \"location\", fields: $fields, handle: $handle, capabilities: {publishable: {status: ACTIVE}}}\n ) {\n metaobject {\n id\n type\n fields {\n value\n key\n }\n }\n }\n }\n": {return: CreateLocationMetaobjectMutation, variables: CreateLocationMetaobjectMutationVariables}, + "#graphql\n mutation UpdateLocationMetaobject($id: ID!, $fields: [MetaobjectFieldInput!]!) {\n metaobjectUpdate(id: $id, metaobject: {fields: $fields}) {\n metaobject {\n fields {\n value\n key\n }\n }\n }\n }\n": {return: UpdateLocationMetaobjectMutation, variables: UpdateLocationMetaobjectMutationVariables}, "#graphql\n mutation ProductParentUpdate($id: ID, $metafields: [MetafieldInput!]) {\n productUpdate(input: {id: $id, metafields: $metafields}) {\n product {\n options: metafield(key: \"options\", namespace: \"booking\") {\n id\n }\n }\n }\n }\n": {return: ProductParentUpdateMutation, variables: ProductParentUpdateMutationVariables}, "#graphql\n #graphql\n fragment ProductOptionFragment on Product {\n id\n title\n handle\n tags\n required: metafield(key: \"required\", namespace: \"system\") {\n id\n value\n }\n parentId: metafield(key: \"parentId\", namespace: \"booking\") {\n id\n value\n }\n variants(first: 5) {\n nodes {\n id\n title\n price\n duration: metafield(key: \"duration\", namespace: \"booking\") {\n id\n value\n }\n }\n }\n }\n\n mutation ProductOptionAdd($id: ID!, $metafields: [MetafieldInput!]!, $tags: [String!]!) {\n productUpdate(input: {id: $id, metafields: $metafields, tags: $tags}) {\n product {\n ...ProductOptionFragment\n }\n }\n }\n": {return: ProductOptionAddMutation, variables: ProductOptionAddMutationVariables}, "#graphql\n mutation productOptionDestroy($productId: ID!) {\n productDelete(input: {id: $productId}) {\n deletedProductId\n }\n }\n": {return: ProductOptionDestroyMutation, variables: ProductOptionDestroyMutationVariables}, @@ -255,7 +256,6 @@ interface GeneratedMutationTypes { "#graphql\n mutation UpdateScheduleLocationsField($id: ID!, $fields: [MetaobjectFieldInput!]!) {\n metaobjectUpdate(id: $id, metaobject: {fields: $fields}) {\n metaobject {\n fields {\n value\n key\n }\n }\n }\n }\n": {return: UpdateScheduleLocationsFieldMutation, variables: UpdateScheduleLocationsFieldMutationVariables}, "#graphql\n mutation CreateScheduleMetaobject($handle: String!, $fields: [MetaobjectFieldInput!]) {\n metaobjectCreate(\n metaobject: {type: \"schedule\", fields: $fields, handle: $handle, capabilities: {publishable: {status: ACTIVE}}}\n ) {\n metaobject {\n id\n type\n fields {\n value\n key\n }\n }\n }\n }\n": {return: CreateScheduleMetaobjectMutation, variables: CreateScheduleMetaobjectMutationVariables}, "#graphql\n mutation UpdateScheduleMetaobject($id: ID!, $fields: [MetaobjectFieldInput!]!) {\n metaobjectUpdate(id: $id, metaobject: {fields: $fields}) {\n metaobject {\n fields {\n value\n key\n }\n }\n }\n }\n": {return: UpdateScheduleMetaobjectMutation, variables: UpdateScheduleMetaobjectMutationVariables}, - "#graphql\n mutation UpdateLocationMetaobject($id: ID!, $fields: [MetaobjectFieldInput!]!) {\n metaobjectUpdate(id: $id, metaobject: {fields: $fields}) {\n metaobject {\n fields {\n value\n key\n }\n }\n }\n }\n": {return: UpdateLocationMetaobjectMutation, variables: UpdateLocationMetaobjectMutationVariables}, "#graphql\n #graphql\n fragment ProductOptionFragment on Product {\n id\n title\n handle\n tags\n required: metafield(key: \"required\", namespace: \"system\") {\n id\n value\n }\n parentId: metafield(key: \"parentId\", namespace: \"booking\") {\n id\n value\n }\n variants(first: 5) {\n nodes {\n id\n title\n price\n duration: metafield(key: \"duration\", namespace: \"booking\") {\n id\n value\n }\n }\n }\n }\n\n mutation productOptionDuplicate($productId: ID!, $title: String!) {\n productDuplicate(newTitle: $title, productId: $productId, includeImages: true) {\n newProduct {\n ...ProductOptionFragment\n }\n }\n }\n": {return: ProductOptionDuplicateMutation, variables: ProductOptionDuplicateMutationVariables}, "#graphql\n mutation productOptionUpdate($productId: ID!, $variants: [ProductVariantsBulkInput!] = {}) {\n productVariantsBulkUpdate(\n productId: $productId,\n variants: $variants\n ) {\n product {\n id\n title\n variants(first: 5) {\n nodes {\n id\n title\n price\n duration: metafield(key: \"duration\", namespace: \"booking\") {\n id\n value\n }\n }\n }\n }\n }\n}\n": {return: ProductOptionUpdateMutation, variables: ProductOptionUpdateMutationVariables}, "#graphql\n #graphql\n fragment ProductFragment on Product {\n id\n handle\n tags\n title\n variants(first: 1) {\n nodes {\n id\n compareAtPrice\n price\n }\n }\n default: metafield(key: \"default\", namespace: \"system\") {\n id\n value\n }\n active: metafield(key: \"active\", namespace: \"system\") {\n id\n value\n }\n user: metafield(key: \"user\", namespace: \"booking\") {\n id\n value\n }\n hideFromCombine: metafield(key: \"hide_from_combine\", namespace: \"booking\") {\n id\n value\n }\n hideFromProfile: metafield(key: \"hide_from_profile\", namespace: \"booking\") {\n id\n value\n }\n parentId: metafield(key: \"parentId\", namespace: \"booking\") {\n id\n value\n }\n scheduleId: metafield(key: \"scheduleId\", namespace: \"booking\") {\n id\n value\n }\n locations: metafield(key: \"locations\", namespace: \"booking\") {\n id\n value\n }\n bookingPeriodValue: metafield(key: \"booking_period_value\", namespace: \"booking\") {\n id\n value\n }\n bookingPeriodUnit: metafield(key: \"booking_period_unit\", namespace: \"booking\") {\n id\n value\n }\n noticePeriodValue: metafield(key: \"notice_period_value\", namespace: \"booking\") {\n id\n value\n }\n noticePeriodUnit: metafield(key: \"notice_period_unit\", namespace: \"booking\") {\n id\n value\n }\n duration: metafield(key: \"duration\", namespace: \"booking\") {\n id\n value\n }\n breaktime: metafield(key: \"breaktime\", namespace: \"booking\") {\n id\n value\n }\n }\n\n mutation productDuplicate($productId: ID!, $title: String!) {\n productDuplicate(newTitle: $title, productId: $productId, includeImages: true) {\n newProduct {\n ...ProductFragment\n }\n }\n }\n": {return: ProductDuplicateMutation, variables: ProductDuplicateMutationVariables},