From 400bfee6836a51c6ab5e4804e8b1e9ad48856dcb Mon Sep 17 00:00:00 2001 From: Helen Lin Date: Fri, 19 Jan 2024 10:21:54 -0800 Subject: [PATCH] Error handling across Hydrogen (#1645) Co-authored-by: Fran Dios --- .changeset/happy-pigs-collect.md | 22 +++++++ examples/multipass/app/routes/cart.tsx | 4 +- examples/multipass/app/routes/search.tsx | 2 +- examples/subscriptions/app/routes/cart.tsx | 4 +- packages/cli/src/lib/request-events.ts | 26 +++++--- packages/hydrogen/src/cache/fetch.ts | 9 +-- .../src/cart/CartForm.custom.example.tsx | 4 +- .../hydrogen/src/cart/CartForm.example.tsx | 4 +- .../src/cart/CartForm.fetcher.example.tsx | 4 +- .../src/cart/CartForm.input-tag.example.tsx | 4 +- .../hydrogen/src/cart/cart-test-helper.ts | 25 +++++--- .../src/cart/createCartHandler.test.ts | 22 +++---- .../hydrogen/src/cart/queries/cart-types.ts | 12 +++- .../cartAttributesUpdateDefault.test.ts | 2 +- .../queries/cartAttributesUpdateDefault.ts | 11 ++-- .../cartBuyerIdentityUpdateDefault.test.ts | 2 +- .../queries/cartBuyerIdentityUpdateDefault.ts | 11 ++-- .../cart/queries/cartCreateDefault.test.ts | 2 +- .../src/cart/queries/cartCreateDefault.ts | 11 ++-- .../cartDiscountCodesUpdateDefault.test.ts | 2 +- .../queries/cartDiscountCodesUpdateDefault.ts | 12 ++-- .../src/cart/queries/cartGetDefault.test.ts | 1 - .../src/cart/queries/cartGetDefault.ts | 25 ++++---- .../cart/queries/cartLinesAddDefault.test.ts | 2 +- .../src/cart/queries/cartLinesAddDefault.ts | 12 ++-- .../queries/cartLinesRemoveDefault.test.ts | 2 +- .../cart/queries/cartLinesRemoveDefault.ts | 11 ++-- .../queries/cartLinesUpdateDefault.test.ts | 2 +- .../cart/queries/cartLinesUpdateDefault.ts | 11 ++-- .../cartMetafieldDeleteDefault.test.ts | 2 +- .../queries/cartMetafieldDeleteDefault.ts | 30 +++++----- .../queries/cartMetafieldsSetDefault.test.ts | 2 +- .../cart/queries/cartMetafieldsSetDefault.ts | 28 +++++---- .../queries/cartNoteUpdateDefault.test.ts | 2 +- .../src/cart/queries/cartNoteUpdateDefault.ts | 11 ++-- ...lectedDeliveryOptionsUpdateDefault.test.ts | 2 +- ...artSelectedDeliveryOptionsUpdateDefault.ts | 11 ++-- packages/hydrogen/src/customer/customer.ts | 32 +++++----- packages/hydrogen/src/index.ts | 3 +- packages/hydrogen/src/storefront.test.ts | 17 ++++-- packages/hydrogen/src/storefront.ts | 60 ++++++++++++------- packages/hydrogen/src/utils/graphql.ts | 37 +++++++++++- templates/demo-store/app/components/Cart.tsx | 3 +- .../demo-store/app/routes/($locale).cart.tsx | 7 ++- templates/skeleton/app/routes/cart.tsx | 4 +- templates/skeleton/app/routes/search.tsx | 2 +- 46 files changed, 326 insertions(+), 188 deletions(-) create mode 100644 .changeset/happy-pigs-collect.md diff --git a/.changeset/happy-pigs-collect.md b/.changeset/happy-pigs-collect.md new file mode 100644 index 0000000000..d899ca3b39 --- /dev/null +++ b/.changeset/happy-pigs-collect.md @@ -0,0 +1,22 @@ +--- +'@shopify/hydrogen': patch +'@shopify/cli-hydrogen': patch +--- + +Better Hydrogen error handling + +* Fix storefront client throwing on partial successful errors +* Fix subrequest profiler to better display network errors with url information for SFAPI requests + +### Breaking change + + Mutation methods of `createCartHandler` used to return an `errors` object that contains `userErrors`. This is now changed back to `userErrors` to be consistent with SFAPI schema. + + The `errors` object will now be used for Graphql execution errors. + + `storefront.isApiError` is deprecated. + + Updated types: + + * `cart.get()` used to return a `Cart` type. Now it returns `CartReturn` type to accommodate the `errors` object + * All other `cart` methods (ie. `cart.addLines`) used to return a `CartQueryData` type. Now it returns `CartQueryDataReturn` type to accommodate the `errors` object. diff --git a/examples/multipass/app/routes/cart.tsx b/examples/multipass/app/routes/cart.tsx index 9287963a64..39b1387ef4 100644 --- a/examples/multipass/app/routes/cart.tsx +++ b/examples/multipass/app/routes/cart.tsx @@ -1,6 +1,6 @@ import {Await, type MetaFunction} from '@remix-run/react'; import {Suspense} from 'react'; -import type {CartQueryData} from '@shopify/hydrogen'; +import type {CartQueryDataReturn} from '@shopify/hydrogen'; import {CartForm} from '@shopify/hydrogen'; import {json, type ActionFunctionArgs} from '@shopify/remix-oxygen'; import {CartMain} from '~/components/Cart'; @@ -25,7 +25,7 @@ export async function action({request, context}: ActionFunctionArgs) { } let status = 200; - let result: CartQueryData; + let result: CartQueryDataReturn; switch (action) { case CartForm.ACTIONS.LinesAdd: diff --git a/examples/multipass/app/routes/search.tsx b/examples/multipass/app/routes/search.tsx index 09d829e0e7..407718ebc4 100644 --- a/examples/multipass/app/routes/search.tsx +++ b/examples/multipass/app/routes/search.tsx @@ -21,7 +21,7 @@ export async function loader({request, context}: LoaderFunctionArgs) { }; } - const data = await context.storefront.query(SEARCH_QUERY, { + const {errors, ...data} = await context.storefront.query(SEARCH_QUERY, { variables: { query: searchTerm, ...variables, diff --git a/examples/subscriptions/app/routes/cart.tsx b/examples/subscriptions/app/routes/cart.tsx index 6174735d0f..be481db45a 100644 --- a/examples/subscriptions/app/routes/cart.tsx +++ b/examples/subscriptions/app/routes/cart.tsx @@ -1,6 +1,6 @@ import {Await, type MetaFunction} from '@remix-run/react'; import {Suspense} from 'react'; -import type {CartQueryData} from '@shopify/hydrogen'; +import type {CartQueryDataReturn} from '@shopify/hydrogen'; import {CartForm} from '@shopify/hydrogen'; import {json, type ActionFunctionArgs} from '@shopify/remix-oxygen'; import {CartMain} from '~/components/Cart'; @@ -22,7 +22,7 @@ export async function action({request, context}: ActionFunctionArgs) { } let status = 200; - let result: CartQueryData; + let result: CartQueryDataReturn; switch (action) { case CartForm.ACTIONS.LinesAdd: diff --git a/packages/cli/src/lib/request-events.ts b/packages/cli/src/lib/request-events.ts index 0d60b3d3cc..014db5ee40 100644 --- a/packages/cli/src/lib/request-events.ts +++ b/packages/cli/src/lib/request-events.ts @@ -60,6 +60,7 @@ export type H2OEvent = { key?: string | readonly unknown[]; }; displayName?: string; + url?: string; }; async function getRequestInfo(request: RequestKind) { @@ -104,19 +105,27 @@ export function createLogRequestEvent(options?: {absoluteBundlePath?: string}) { return createResponse(); } - const {eventType, purpose, graphql, stackInfo, ...data} = - await getRequestInfo(request); + const { + url: displayUrl, + displayName: displayNameData, + eventType, + purpose, + graphql, + stackInfo, + ...data + } = await getRequestInfo(request); let graphiqlLink = ''; - let description = request.url; + let descriptionUrl = request.url; + let displayName = displayNameData; if (eventType === 'subrequest') { - description = + displayName = + displayName || graphql?.query .match(/(query|mutation)\s+(\w+)/)?.[0] - ?.replace(/\s+/, ' ') || - decodeURIComponent(url.search.slice(1)) || - request.url; + ?.replace(/\s+/, ' '); + descriptionUrl = displayUrl || request.url; if (graphql) { graphiqlLink = getGraphiQLUrl({graphql}); @@ -150,7 +159,8 @@ export function createLogRequestEvent(options?: {absoluteBundlePath?: string}) { event: EVENT_MAP[eventType] || eventType, data: JSON.stringify({ ...data, - url: `${purpose} ${description}`.trim(), + displayName, + url: `${purpose} ${descriptionUrl}`.trim(), graphiqlLink, stackLine, stackLink, diff --git a/packages/hydrogen/src/cache/fetch.ts b/packages/hydrogen/src/cache/fetch.ts index 7c461fb6eb..4b037d32d7 100644 --- a/packages/hydrogen/src/cache/fetch.ts +++ b/packages/hydrogen/src/cache/fetch.ts @@ -21,6 +21,7 @@ import { export type CacheKey = string | readonly unknown[]; export type FetchDebugInfo = { + url?: string; requestId?: string | null; graphql?: string | null; purpose?: string | null; @@ -134,7 +135,6 @@ export async function runWithCache( }) => { globalThis.__H2O_LOG_EVENT?.({ eventType: 'subrequest', - url: debugData?.url || getKeyUrl(key), startTime: overrideStartTime || startTime, cacheStatus, responsePayload: (result && result[0]) || result, @@ -146,6 +146,7 @@ export async function runWithCache( }, waitUntil, ...debugInfo, + url: debugData?.url || debugInfo?.url, displayName: debugInfo?.displayName || debugData?.displayName, } as any); } @@ -271,11 +272,7 @@ export async function fetchWithServerCache( data = await response.text(); } catch { // Getting a response without a valid body - throw new Error( - `Storefront API response code: ${ - response.status - } (Request Id: ${response.headers.get('x-request-id')})`, - ); + return toSerializableResponse('', response); } } diff --git a/packages/hydrogen/src/cart/CartForm.custom.example.tsx b/packages/hydrogen/src/cart/CartForm.custom.example.tsx index 20354d0d95..97926e9522 100644 --- a/packages/hydrogen/src/cart/CartForm.custom.example.tsx +++ b/packages/hydrogen/src/cart/CartForm.custom.example.tsx @@ -1,6 +1,6 @@ import {type ActionFunctionArgs, json} from '@remix-run/server-runtime'; import { - type CartQueryData, + type CartQueryDataReturn, type HydrogenCart, CartForm, } from '@shopify/hydrogen'; @@ -36,7 +36,7 @@ export async function action({request, context}: ActionFunctionArgs) { const {action, inputs} = CartForm.getFormInput(formData); let status = 200; - let result: CartQueryData; + let result: CartQueryDataReturn; if (action === 'CustomEditInPlace') { result = await cart.addLines(inputs.addLines as CartLineInput[]); diff --git a/packages/hydrogen/src/cart/CartForm.example.tsx b/packages/hydrogen/src/cart/CartForm.example.tsx index fe0260d842..b1af1539ad 100644 --- a/packages/hydrogen/src/cart/CartForm.example.tsx +++ b/packages/hydrogen/src/cart/CartForm.example.tsx @@ -1,6 +1,6 @@ import {type ActionFunctionArgs, json} from '@remix-run/server-runtime'; import { - type CartQueryData, + type CartQueryDataReturn, type HydrogenCart, CartForm, } from '@shopify/hydrogen'; @@ -35,7 +35,7 @@ export async function action({request, context}: ActionFunctionArgs) { const {action, inputs} = CartForm.getFormInput(formData); let status = 200; - let result: CartQueryData; + let result: CartQueryDataReturn; if (action === CartForm.ACTIONS.LinesUpdate) { result = await cart.updateLines(inputs.lines); diff --git a/packages/hydrogen/src/cart/CartForm.fetcher.example.tsx b/packages/hydrogen/src/cart/CartForm.fetcher.example.tsx index 1f5b50029d..d80ff25005 100644 --- a/packages/hydrogen/src/cart/CartForm.fetcher.example.tsx +++ b/packages/hydrogen/src/cart/CartForm.fetcher.example.tsx @@ -1,7 +1,7 @@ import {useFetcher} from '@remix-run/react'; import {type ActionFunctionArgs, json} from '@remix-run/server-runtime'; import { - type CartQueryData, + type CartQueryDataReturn, type HydrogenCart, CartForm, type CartActionInput, @@ -57,7 +57,7 @@ export async function action({request, context}: ActionFunctionArgs) { const {action, inputs} = CartForm.getFormInput(formData); let status = 200; - let result: CartQueryData; + let result: CartQueryDataReturn; if (action === CartForm.ACTIONS.MetafieldsSet) { result = await cart.setMetafields(inputs.metafields); diff --git a/packages/hydrogen/src/cart/CartForm.input-tag.example.tsx b/packages/hydrogen/src/cart/CartForm.input-tag.example.tsx index 39186335dd..54d4586eb3 100644 --- a/packages/hydrogen/src/cart/CartForm.input-tag.example.tsx +++ b/packages/hydrogen/src/cart/CartForm.input-tag.example.tsx @@ -1,6 +1,6 @@ import {type ActionFunctionArgs, json} from '@remix-run/server-runtime'; import { - type CartQueryData, + type CartQueryDataReturn, type HydrogenCart, CartForm, } from '@shopify/hydrogen'; @@ -25,7 +25,7 @@ export async function action({request, context}: ActionFunctionArgs) { const {action, inputs} = CartForm.getFormInput(formData); let status = 200; - let result: CartQueryData; + let result: CartQueryDataReturn; if (action === CartForm.ACTIONS.NoteUpdate) { result = await cart.updateNote(inputs.note); diff --git a/packages/hydrogen/src/cart/cart-test-helper.ts b/packages/hydrogen/src/cart/cart-test-helper.ts index 453a8eb73f..b612e0d97b 100644 --- a/packages/hydrogen/src/cart/cart-test-helper.ts +++ b/packages/hydrogen/src/cart/cart-test-helper.ts @@ -35,14 +35,25 @@ function storefrontMutate( cartId = 'c1-new-cart-id'; } - return Promise.resolve({ - [keyWrapper]: { - cart: { - id: cartId, + if ( + keyWrapper === 'cartMetafieldsSet' || + keyWrapper === 'cartMetafieldDelete' + ) { + return Promise.resolve({ + [keyWrapper]: { + userErrors: [query], }, - errors: [query], - }, - }); + }); + } else { + return Promise.resolve({ + [keyWrapper]: { + cart: { + id: cartId, + }, + userErrors: [query], + }, + }); + } } export function mockHeaders(cartId?: string) { diff --git a/packages/hydrogen/src/cart/createCartHandler.test.ts b/packages/hydrogen/src/cart/createCartHandler.test.ts index cc58e17f1b..e32784a7e3 100644 --- a/packages/hydrogen/src/cart/createCartHandler.test.ts +++ b/packages/hydrogen/src/cart/createCartHandler.test.ts @@ -98,25 +98,25 @@ describe('createCartHandler', () => { }); const result1 = await cart.create({}); - expect(result1.errors?.[0]).toContain(cartMutateFragment); + expect(result1.userErrors?.[0]).toContain(cartMutateFragment); const result2 = await cart.addLines([]); - expect(result2.errors?.[0]).toContain(cartMutateFragment); + expect(result2.userErrors?.[0]).toContain(cartMutateFragment); const result3 = await cart.updateLines([]); - expect(result3.errors?.[0]).toContain(cartMutateFragment); + expect(result3.userErrors?.[0]).toContain(cartMutateFragment); const result4 = await cart.removeLines([]); - expect(result4.errors?.[0]).toContain(cartMutateFragment); + expect(result4.userErrors?.[0]).toContain(cartMutateFragment); const result5 = await cart.updateDiscountCodes([]); - expect(result5.errors?.[0]).toContain(cartMutateFragment); + expect(result5.userErrors?.[0]).toContain(cartMutateFragment); const result6 = await cart.updateBuyerIdentity({}); - expect(result6.errors?.[0]).toContain(cartMutateFragment); + expect(result6.userErrors?.[0]).toContain(cartMutateFragment); const result7 = await cart.updateNote(''); - expect(result7.errors?.[0]).toContain(cartMutateFragment); + expect(result7.userErrors?.[0]).toContain(cartMutateFragment); const result8 = await cart.updateSelectedDeliveryOption([ { @@ -124,16 +124,16 @@ describe('createCartHandler', () => { deliveryOptionHandle: 'Postal Service', }, ]); - expect(result8.errors?.[0]).toContain(cartMutateFragment); + expect(result8.userErrors?.[0]).toContain(cartMutateFragment); const result9 = await cart.updateAttributes([]); - expect(result9.errors?.[0]).toContain(cartMutateFragment); + expect(result9.userErrors?.[0]).toContain(cartMutateFragment); const result10 = await cart.setMetafields([]); - expect(result10.errors?.[0]).not.toContain(cartMutateFragment); + expect(result10.userErrors?.[0]).not.toContain(cartMutateFragment); const result11 = await cart.deleteMetafield('some.key'); - expect(result11.errors?.[0]).not.toContain(cartMutateFragment); + expect(result11.userErrors?.[0]).not.toContain(cartMutateFragment); }); it('function get has a working default implementation', async () => { diff --git a/packages/hydrogen/src/cart/queries/cart-types.ts b/packages/hydrogen/src/cart/queries/cart-types.ts index a04cac6495..f3e54438e8 100644 --- a/packages/hydrogen/src/cart/queries/cart-types.ts +++ b/packages/hydrogen/src/cart/queries/cart-types.ts @@ -8,7 +8,7 @@ import type { MetafieldsSetUserError, MetafieldDeleteUserError, } from '@shopify/hydrogen-react/storefront-api-types'; -import {type Storefront} from '../../storefront'; +import type {StorefrontApiErrors, Storefront} from '../../storefront'; export type CartOptionalInput = { /** @@ -45,14 +45,22 @@ export type CartQueryOptions = { cartFragment?: string; }; +export type CartReturn = Cart & { + errors?: StorefrontApiErrors; +}; + export type CartQueryData = { cart: Cart; - errors?: + userErrors?: | CartUserError[] | MetafieldsSetUserError[] | MetafieldDeleteUserError[]; }; +export type CartQueryDataReturn = CartQueryData & { + errors?: StorefrontApiErrors; +}; + export type CartQueryReturn = ( requiredParams: T, optionalParams?: CartOptionalInput, diff --git a/packages/hydrogen/src/cart/queries/cartAttributesUpdateDefault.test.ts b/packages/hydrogen/src/cart/queries/cartAttributesUpdateDefault.test.ts index 93081663cb..452ef0abfa 100644 --- a/packages/hydrogen/src/cart/queries/cartAttributesUpdateDefault.test.ts +++ b/packages/hydrogen/src/cart/queries/cartAttributesUpdateDefault.test.ts @@ -25,6 +25,6 @@ describe('cartAttributesUpdateDefault', () => { const result = await cartAttribute([]); expect(result.cart).toHaveProperty('id', CART_ID); - expect(result.errors?.[0]).toContain(cartFragment); + expect(result.userErrors?.[0]).toContain(cartFragment); }); }); diff --git a/packages/hydrogen/src/cart/queries/cartAttributesUpdateDefault.ts b/packages/hydrogen/src/cart/queries/cartAttributesUpdateDefault.ts index 820212d305..3cc8fa70cf 100644 --- a/packages/hydrogen/src/cart/queries/cartAttributesUpdateDefault.ts +++ b/packages/hydrogen/src/cart/queries/cartAttributesUpdateDefault.ts @@ -1,7 +1,9 @@ +import {StorefrontApiErrors, formatAPIResult} from '../../storefront'; import {MINIMAL_CART_FRAGMENT, USER_ERROR_FRAGMENT} from './cart-fragments'; import type { CartOptionalInput, CartQueryData, + CartQueryDataReturn, CartQueryOptions, } from './cart-types'; import type {AttributeInput} from '@shopify/hydrogen-react/storefront-api-types'; @@ -9,21 +11,22 @@ import type {AttributeInput} from '@shopify/hydrogen-react/storefront-api-types' export type CartAttributesUpdateFunction = ( attributes: AttributeInput[], optionalParams?: CartOptionalInput, -) => Promise; +) => Promise; export function cartAttributesUpdateDefault( options: CartQueryOptions, ): CartAttributesUpdateFunction { return async (attributes, optionalParams) => { - const {cartAttributesUpdate} = await options.storefront.mutate<{ + const {cartAttributesUpdate, errors} = await options.storefront.mutate<{ cartAttributesUpdate: CartQueryData; + errors: StorefrontApiErrors; }>(CART_ATTRIBUTES_UPDATE_MUTATION(options.cartFragment), { variables: { cartId: optionalParams?.cartId || options.getCartId(), attributes, }, }); - return cartAttributesUpdate; + return formatAPIResult(cartAttributesUpdate, errors); }; } @@ -38,7 +41,7 @@ export const CART_ATTRIBUTES_UPDATE_MUTATION = ( cart { ...CartApiMutation } - errors: userErrors { + userErrors { ...CartApiError } } diff --git a/packages/hydrogen/src/cart/queries/cartBuyerIdentityUpdateDefault.test.ts b/packages/hydrogen/src/cart/queries/cartBuyerIdentityUpdateDefault.test.ts index 389de224ad..1646f06b87 100644 --- a/packages/hydrogen/src/cart/queries/cartBuyerIdentityUpdateDefault.test.ts +++ b/packages/hydrogen/src/cart/queries/cartBuyerIdentityUpdateDefault.test.ts @@ -25,6 +25,6 @@ describe('cartBuyerIdentityUpdateDefault', () => { const result = await cartUpdate({}); expect(result.cart).toHaveProperty('id', CART_ID); - expect(result.errors?.[0]).toContain(cartFragment); + expect(result.userErrors?.[0]).toContain(cartFragment); }); }); diff --git a/packages/hydrogen/src/cart/queries/cartBuyerIdentityUpdateDefault.ts b/packages/hydrogen/src/cart/queries/cartBuyerIdentityUpdateDefault.ts index 65ee9c864c..b588dcdbb8 100644 --- a/packages/hydrogen/src/cart/queries/cartBuyerIdentityUpdateDefault.ts +++ b/packages/hydrogen/src/cart/queries/cartBuyerIdentityUpdateDefault.ts @@ -1,7 +1,9 @@ +import {StorefrontApiErrors, formatAPIResult} from '../../storefront'; import {MINIMAL_CART_FRAGMENT, USER_ERROR_FRAGMENT} from './cart-fragments'; import type { CartOptionalInput, CartQueryData, + CartQueryDataReturn, CartQueryOptions, } from './cart-types'; import type {CartBuyerIdentityInput} from '@shopify/hydrogen-react/storefront-api-types'; @@ -9,14 +11,15 @@ import type {CartBuyerIdentityInput} from '@shopify/hydrogen-react/storefront-ap export type CartBuyerIdentityUpdateFunction = ( buyerIdentity: CartBuyerIdentityInput, optionalParams?: CartOptionalInput, -) => Promise; +) => Promise; export function cartBuyerIdentityUpdateDefault( options: CartQueryOptions, ): CartBuyerIdentityUpdateFunction { return async (buyerIdentity, optionalParams) => { - const {cartBuyerIdentityUpdate} = await options.storefront.mutate<{ + const {cartBuyerIdentityUpdate, errors} = await options.storefront.mutate<{ cartBuyerIdentityUpdate: CartQueryData; + errors: StorefrontApiErrors; }>(CART_BUYER_IDENTITY_UPDATE_MUTATION(options.cartFragment), { variables: { cartId: options.getCartId(), @@ -24,7 +27,7 @@ export function cartBuyerIdentityUpdateDefault( ...optionalParams, }, }); - return cartBuyerIdentityUpdate; + return formatAPIResult(cartBuyerIdentityUpdate, errors); }; } @@ -42,7 +45,7 @@ export const CART_BUYER_IDENTITY_UPDATE_MUTATION = ( cart { ...CartApiMutation } - errors: userErrors { + userErrors { ...CartApiError } } diff --git a/packages/hydrogen/src/cart/queries/cartCreateDefault.test.ts b/packages/hydrogen/src/cart/queries/cartCreateDefault.test.ts index 75cb03591c..f263d50aba 100644 --- a/packages/hydrogen/src/cart/queries/cartCreateDefault.test.ts +++ b/packages/hydrogen/src/cart/queries/cartCreateDefault.test.ts @@ -29,6 +29,6 @@ describe('cartCreateDefault', () => { const result = await cartCreate({}); expect(result.cart).toHaveProperty('id', NEW_CART_ID); - expect(result.errors?.[0]).toContain(cartFragment); + expect(result.userErrors?.[0]).toContain(cartFragment); }); }); diff --git a/packages/hydrogen/src/cart/queries/cartCreateDefault.ts b/packages/hydrogen/src/cart/queries/cartCreateDefault.ts index 5f983c81c7..e0cd883f93 100644 --- a/packages/hydrogen/src/cart/queries/cartCreateDefault.ts +++ b/packages/hydrogen/src/cart/queries/cartCreateDefault.ts @@ -1,30 +1,33 @@ +import {StorefrontApiErrors, formatAPIResult} from '../../storefront'; import {MINIMAL_CART_FRAGMENT, USER_ERROR_FRAGMENT} from './cart-fragments'; import type { CartQueryData, CartQueryOptions, CartOptionalInput, + CartQueryDataReturn, } from './cart-types'; import type {CartInput} from '@shopify/hydrogen-react/storefront-api-types'; export type CartCreateFunction = ( input: CartInput, optionalParams?: CartOptionalInput, -) => Promise; +) => Promise; export function cartCreateDefault( options: CartQueryOptions, ): CartCreateFunction { return async (input, optionalParams) => { const {cartId, ...restOfOptionalParams} = optionalParams || {}; - const {cartCreate} = await options.storefront.mutate<{ + const {cartCreate, errors} = await options.storefront.mutate<{ cartCreate: CartQueryData; + errors: StorefrontApiErrors; }>(CART_CREATE_MUTATION(options.cartFragment), { variables: { input, ...restOfOptionalParams, }, }); - return cartCreate; + return formatAPIResult(cartCreate, errors); }; } @@ -42,7 +45,7 @@ export const CART_CREATE_MUTATION = ( ...CartApiMutation checkoutUrl } - errors: userErrors { + userErrors { ...CartApiError } } diff --git a/packages/hydrogen/src/cart/queries/cartDiscountCodesUpdateDefault.test.ts b/packages/hydrogen/src/cart/queries/cartDiscountCodesUpdateDefault.test.ts index 5934c6c920..0d7c83621f 100644 --- a/packages/hydrogen/src/cart/queries/cartDiscountCodesUpdateDefault.test.ts +++ b/packages/hydrogen/src/cart/queries/cartDiscountCodesUpdateDefault.test.ts @@ -25,6 +25,6 @@ describe('cartDiscountCodesUpdateDefault', () => { const result = await cartDiscountCode([]); expect(result.cart).toHaveProperty('id', CART_ID); - expect(result.errors?.[0]).toContain(cartFragment); + expect(result.userErrors?.[0]).toContain(cartFragment); }); }); diff --git a/packages/hydrogen/src/cart/queries/cartDiscountCodesUpdateDefault.ts b/packages/hydrogen/src/cart/queries/cartDiscountCodesUpdateDefault.ts index 40ea888340..4d8bd578e0 100644 --- a/packages/hydrogen/src/cart/queries/cartDiscountCodesUpdateDefault.ts +++ b/packages/hydrogen/src/cart/queries/cartDiscountCodesUpdateDefault.ts @@ -1,15 +1,16 @@ +import {StorefrontApiErrors, formatAPIResult} from '../../storefront'; import {MINIMAL_CART_FRAGMENT, USER_ERROR_FRAGMENT} from './cart-fragments'; import type { CartOptionalInput, CartQueryData, + CartQueryDataReturn, CartQueryOptions, - CartQueryReturn, } from './cart-types'; export type CartDiscountCodesUpdateFunction = ( discountCodes: string[], optionalParams?: CartOptionalInput, -) => Promise; +) => Promise; export function cartDiscountCodesUpdateDefault( options: CartQueryOptions, @@ -20,8 +21,9 @@ export function cartDiscountCodesUpdateDefault( return array.indexOf(value) === index; }); - const {cartDiscountCodesUpdate} = await options.storefront.mutate<{ + const {cartDiscountCodesUpdate, errors} = await options.storefront.mutate<{ cartDiscountCodesUpdate: CartQueryData; + errors: StorefrontApiErrors; }>(CART_DISCOUNT_CODE_UPDATE_MUTATION(options.cartFragment), { variables: { cartId: options.getCartId(), @@ -29,7 +31,7 @@ export function cartDiscountCodesUpdateDefault( ...optionalParams, }, }); - return cartDiscountCodesUpdate; + return formatAPIResult(cartDiscountCodesUpdate, errors); }; } @@ -47,7 +49,7 @@ export const CART_DISCOUNT_CODE_UPDATE_MUTATION = ( cart { ...CartApiMutation } - errors: userErrors { + userErrors { ...CartApiError } } diff --git a/packages/hydrogen/src/cart/queries/cartGetDefault.test.ts b/packages/hydrogen/src/cart/queries/cartGetDefault.test.ts index a61f86d17d..d5c8fca2d3 100644 --- a/packages/hydrogen/src/cart/queries/cartGetDefault.test.ts +++ b/packages/hydrogen/src/cart/queries/cartGetDefault.test.ts @@ -22,7 +22,6 @@ describe('cartGetDefault', () => { const result = await cartGet(); - console.log(result); expect(result).toStrictEqual(null); }); diff --git a/packages/hydrogen/src/cart/queries/cartGetDefault.ts b/packages/hydrogen/src/cart/queries/cartGetDefault.ts index 2e8e9900aa..ecaa3a9820 100644 --- a/packages/hydrogen/src/cart/queries/cartGetDefault.ts +++ b/packages/hydrogen/src/cart/queries/cartGetDefault.ts @@ -1,4 +1,5 @@ -import type {CartQueryOptions} from './cart-types'; +import {StorefrontApiErrors, formatAPIResult} from '../../storefront'; +import type {CartQueryOptions, CartReturn} from './cart-types'; import type { Cart, CountryCode, @@ -30,7 +31,7 @@ type CartGetProps = { export type CartGetFunction = ( cartInput?: CartGetProps, -) => Promise; +) => Promise; export function cartGetDefault(options: CartQueryOptions): CartGetFunction { return async (cartInput?: CartGetProps) => { @@ -38,18 +39,18 @@ export function cartGetDefault(options: CartQueryOptions): CartGetFunction { if (!cartId) return null; - const {cart} = await options.storefront.query<{cart: Cart}>( - CART_QUERY(options.cartFragment), - { - variables: { - cartId, - ...cartInput, - }, - cache: options.storefront.CacheNone(), + const {cart, errors} = await options.storefront.query<{ + cart: Cart; + errors: StorefrontApiErrors; + }>(CART_QUERY(options.cartFragment), { + variables: { + cartId, + ...cartInput, }, - ); + cache: options.storefront.CacheNone(), + }); - return cart; + return formatAPIResult(cart, errors); }; } diff --git a/packages/hydrogen/src/cart/queries/cartLinesAddDefault.test.ts b/packages/hydrogen/src/cart/queries/cartLinesAddDefault.test.ts index a2a5d44e2f..5f8310e3c2 100644 --- a/packages/hydrogen/src/cart/queries/cartLinesAddDefault.test.ts +++ b/packages/hydrogen/src/cart/queries/cartLinesAddDefault.test.ts @@ -25,6 +25,6 @@ describe('cartLinesAddDefault', () => { const result = await cartAdd([]); expect(result.cart).toHaveProperty('id', CART_ID); - expect(result.errors?.[0]).toContain(cartFragment); + expect(result.userErrors?.[0]).toContain(cartFragment); }); }); diff --git a/packages/hydrogen/src/cart/queries/cartLinesAddDefault.ts b/packages/hydrogen/src/cart/queries/cartLinesAddDefault.ts index 6c7e5c9966..ed4124e345 100644 --- a/packages/hydrogen/src/cart/queries/cartLinesAddDefault.ts +++ b/packages/hydrogen/src/cart/queries/cartLinesAddDefault.ts @@ -1,7 +1,9 @@ +import {StorefrontApiErrors, formatAPIResult} from '../../storefront'; import {MINIMAL_CART_FRAGMENT, USER_ERROR_FRAGMENT} from './cart-fragments'; import type { CartOptionalInput, CartQueryData, + CartQueryDataReturn, CartQueryOptions, } from './cart-types'; import type {CartLineInput} from '@shopify/hydrogen-react/storefront-api-types'; @@ -9,14 +11,15 @@ import type {CartLineInput} from '@shopify/hydrogen-react/storefront-api-types'; export type CartLinesAddFunction = ( lines: CartLineInput[], optionalParams?: CartOptionalInput, -) => Promise; +) => Promise; export function cartLinesAddDefault( options: CartQueryOptions, ): CartLinesAddFunction { return async (lines, optionalParams) => { - const {cartLinesAdd} = await options.storefront.mutate<{ + const {cartLinesAdd, errors} = await options.storefront.mutate<{ cartLinesAdd: CartQueryData; + errors: StorefrontApiErrors; }>(CART_LINES_ADD_MUTATION(options.cartFragment), { variables: { cartId: options.getCartId(), @@ -24,7 +27,8 @@ export function cartLinesAddDefault( ...optionalParams, }, }); - return cartLinesAdd; + + return formatAPIResult(cartLinesAdd, errors); }; } @@ -42,7 +46,7 @@ export const CART_LINES_ADD_MUTATION = ( cart { ...CartApiMutation } - errors: userErrors { + userErrors { ...CartApiError } } diff --git a/packages/hydrogen/src/cart/queries/cartLinesRemoveDefault.test.ts b/packages/hydrogen/src/cart/queries/cartLinesRemoveDefault.test.ts index cb5a9e0735..3c18550dbc 100644 --- a/packages/hydrogen/src/cart/queries/cartLinesRemoveDefault.test.ts +++ b/packages/hydrogen/src/cart/queries/cartLinesRemoveDefault.test.ts @@ -25,6 +25,6 @@ describe('cartLinesRemoveDefault', () => { const result = await cartRemove([]); expect(result.cart).toHaveProperty('id', CART_ID); - expect(result.errors?.[0]).toContain(cartFragment); + expect(result.userErrors?.[0]).toContain(cartFragment); }); }); diff --git a/packages/hydrogen/src/cart/queries/cartLinesRemoveDefault.ts b/packages/hydrogen/src/cart/queries/cartLinesRemoveDefault.ts index 8ab3869b76..135d52b52c 100644 --- a/packages/hydrogen/src/cart/queries/cartLinesRemoveDefault.ts +++ b/packages/hydrogen/src/cart/queries/cartLinesRemoveDefault.ts @@ -1,21 +1,24 @@ +import {StorefrontApiErrors, formatAPIResult} from '../../storefront'; import {MINIMAL_CART_FRAGMENT, USER_ERROR_FRAGMENT} from './cart-fragments'; import type { CartOptionalInput, CartQueryData, + CartQueryDataReturn, CartQueryOptions, } from './cart-types'; export type CartLinesRemoveFunction = ( lineIds: string[], optionalParams?: CartOptionalInput, -) => Promise; +) => Promise; export function cartLinesRemoveDefault( options: CartQueryOptions, ): CartLinesRemoveFunction { return async (lineIds, optionalParams) => { - const {cartLinesRemove} = await options.storefront.mutate<{ + const {cartLinesRemove, errors} = await options.storefront.mutate<{ cartLinesRemove: CartQueryData; + errors: StorefrontApiErrors; }>(CART_LINES_REMOVE_MUTATION(options.cartFragment), { variables: { cartId: options.getCartId(), @@ -23,7 +26,7 @@ export function cartLinesRemoveDefault( ...optionalParams, }, }); - return cartLinesRemove; + return formatAPIResult(cartLinesRemove, errors); }; } @@ -41,7 +44,7 @@ export const CART_LINES_REMOVE_MUTATION = ( cart { ...CartApiMutation } - errors: userErrors { + userErrors { ...CartApiError } } diff --git a/packages/hydrogen/src/cart/queries/cartLinesUpdateDefault.test.ts b/packages/hydrogen/src/cart/queries/cartLinesUpdateDefault.test.ts index 1b02782976..6685bf633b 100644 --- a/packages/hydrogen/src/cart/queries/cartLinesUpdateDefault.test.ts +++ b/packages/hydrogen/src/cart/queries/cartLinesUpdateDefault.test.ts @@ -25,6 +25,6 @@ describe('cartLinesUpdateDefault', () => { const result = await cartUpdate([]); expect(result.cart).toHaveProperty('id', CART_ID); - expect(result.errors?.[0]).toContain(cartFragment); + expect(result.userErrors?.[0]).toContain(cartFragment); }); }); diff --git a/packages/hydrogen/src/cart/queries/cartLinesUpdateDefault.ts b/packages/hydrogen/src/cart/queries/cartLinesUpdateDefault.ts index 8e8f39070c..2a9f4b1ae1 100644 --- a/packages/hydrogen/src/cart/queries/cartLinesUpdateDefault.ts +++ b/packages/hydrogen/src/cart/queries/cartLinesUpdateDefault.ts @@ -1,7 +1,9 @@ +import {StorefrontApiErrors, formatAPIResult} from '../../storefront'; import {MINIMAL_CART_FRAGMENT, USER_ERROR_FRAGMENT} from './cart-fragments'; import type { CartOptionalInput, CartQueryData, + CartQueryDataReturn, CartQueryOptions, } from './cart-types'; import type {CartLineUpdateInput} from '@shopify/hydrogen-react/storefront-api-types'; @@ -9,14 +11,15 @@ import type {CartLineUpdateInput} from '@shopify/hydrogen-react/storefront-api-t export type CartLinesUpdateFunction = ( lines: CartLineUpdateInput[], optionalParams?: CartOptionalInput, -) => Promise; +) => Promise; export function cartLinesUpdateDefault( options: CartQueryOptions, ): CartLinesUpdateFunction { return async (lines, optionalParams) => { - const {cartLinesUpdate} = await options.storefront.mutate<{ + const {cartLinesUpdate, errors} = await options.storefront.mutate<{ cartLinesUpdate: CartQueryData; + errors: StorefrontApiErrors; }>(CART_LINES_UPDATE_MUTATION(options.cartFragment), { variables: { cartId: options.getCartId(), @@ -24,7 +27,7 @@ export function cartLinesUpdateDefault( ...optionalParams, }, }); - return cartLinesUpdate; + return formatAPIResult(cartLinesUpdate, errors); }; } @@ -42,7 +45,7 @@ export const CART_LINES_UPDATE_MUTATION = ( cart { ...CartApiMutation } - errors: userErrors { + userErrors { ...CartApiError } } diff --git a/packages/hydrogen/src/cart/queries/cartMetafieldDeleteDefault.test.ts b/packages/hydrogen/src/cart/queries/cartMetafieldDeleteDefault.test.ts index acf0a63aa5..9bc8f61332 100644 --- a/packages/hydrogen/src/cart/queries/cartMetafieldDeleteDefault.test.ts +++ b/packages/hydrogen/src/cart/queries/cartMetafieldDeleteDefault.test.ts @@ -25,6 +25,6 @@ describe('cartMetafieldsSetDefault', () => { const result = await cartMetafieldDelete(''); expect(result.cart).toHaveProperty('id', CART_ID); - expect(result.errors?.[0]).not.toContain(cartFragment); + expect(result.userErrors?.[0]).not.toContain(cartFragment); }); }); diff --git a/packages/hydrogen/src/cart/queries/cartMetafieldDeleteDefault.ts b/packages/hydrogen/src/cart/queries/cartMetafieldDeleteDefault.ts index cabc896060..b6be7bc727 100644 --- a/packages/hydrogen/src/cart/queries/cartMetafieldDeleteDefault.ts +++ b/packages/hydrogen/src/cart/queries/cartMetafieldDeleteDefault.ts @@ -1,8 +1,8 @@ +import {StorefrontApiErrors, formatAPIResult} from '../../storefront'; import type { - CartQueryData, CartQueryOptions, - CartQueryReturn, CartOptionalInput, + CartQueryDataReturn, } from './cart-types'; import type { Cart, @@ -13,18 +13,18 @@ import type { export type CartMetafieldDeleteFunction = ( key: Scalars['String']['input'], optionalParams?: CartOptionalInput, -) => Promise; +) => Promise; export function cartMetafieldDeleteDefault( options: CartQueryOptions, ): CartMetafieldDeleteFunction { return async (key, optionalParams) => { const ownerId = optionalParams?.cartId || options.getCartId(); - const {cartMetafieldDelete} = await options.storefront.mutate<{ + const {cartMetafieldDelete, errors} = await options.storefront.mutate<{ cartMetafieldDelete: { - cart: Cart; - errors: MetafieldDeleteUserError[]; + userErrors: MetafieldDeleteUserError[]; }; + errors: StorefrontApiErrors; }>(CART_METAFIELD_DELETE_MUTATION(), { variables: { input: { @@ -33,13 +33,15 @@ export function cartMetafieldDeleteDefault( }, }, }); - return { - cart: { - id: ownerId, - } as Cart, - errors: - cartMetafieldDelete.errors as unknown as MetafieldDeleteUserError[], - }; + return formatAPIResult( + { + cart: { + id: ownerId, + } as Cart, + ...cartMetafieldDelete, + }, + errors, + ); }; } @@ -49,7 +51,7 @@ export const CART_METAFIELD_DELETE_MUTATION = () => `#graphql $input: CartMetafieldDeleteInput! ) { cartMetafieldDelete(input: $input) { - errors: userErrors { + userErrors { code field message diff --git a/packages/hydrogen/src/cart/queries/cartMetafieldsSetDefault.test.ts b/packages/hydrogen/src/cart/queries/cartMetafieldsSetDefault.test.ts index 196f81336d..0f0aeb3c2d 100644 --- a/packages/hydrogen/src/cart/queries/cartMetafieldsSetDefault.test.ts +++ b/packages/hydrogen/src/cart/queries/cartMetafieldsSetDefault.test.ts @@ -25,6 +25,6 @@ describe('cartMetafieldsSetDefault', () => { const result = await cartMetafieldsSet([]); expect(result.cart).toHaveProperty('id', CART_ID); - expect(result.errors?.[0]).not.toContain(cartFragment); + expect(result.userErrors?.[0]).not.toContain(cartFragment); }); }); diff --git a/packages/hydrogen/src/cart/queries/cartMetafieldsSetDefault.ts b/packages/hydrogen/src/cart/queries/cartMetafieldsSetDefault.ts index f8ba1c3496..4b4191af91 100644 --- a/packages/hydrogen/src/cart/queries/cartMetafieldsSetDefault.ts +++ b/packages/hydrogen/src/cart/queries/cartMetafieldsSetDefault.ts @@ -1,8 +1,9 @@ +import {StorefrontApiErrors, formatAPIResult} from '../../storefront'; import type { - CartQueryData, CartOptionalInput, CartQueryOptions, MetafieldWithoutOwnerId, + CartQueryDataReturn, } from './cart-types'; import type { Cart, @@ -12,7 +13,7 @@ import type { export type CartMetafieldsSetFunction = ( metafields: MetafieldWithoutOwnerId[], optionalParams?: CartOptionalInput, -) => Promise; +) => Promise; export function cartMetafieldsSetDefault( options: CartQueryOptions, @@ -25,21 +26,24 @@ export function cartMetafieldsSetDefault( ownerId, }), ); - const {cartMetafieldsSet} = await options.storefront.mutate<{ + const {cartMetafieldsSet, errors} = await options.storefront.mutate<{ cartMetafieldsSet: { - cart: Cart; - errors: MetafieldsSetUserError[]; + userErrors: MetafieldsSetUserError[]; }; + errors: StorefrontApiErrors; }>(CART_METAFIELD_SET_MUTATION(), { variables: {metafields: metafieldsWithOwnerId}, }); - return { - cart: { - id: ownerId, - } as Cart, - errors: cartMetafieldsSet.errors as unknown as MetafieldsSetUserError[], - }; + return formatAPIResult( + { + cart: { + id: ownerId, + } as Cart, + ...cartMetafieldsSet, + }, + errors, + ); }; } @@ -51,7 +55,7 @@ export const CART_METAFIELD_SET_MUTATION = () => `#graphql $country: CountryCode ) @inContext(country: $country, language: $language) { cartMetafieldsSet(metafields: $metafields) { - errors: userErrors { + userErrors { code elementIndex field diff --git a/packages/hydrogen/src/cart/queries/cartNoteUpdateDefault.test.ts b/packages/hydrogen/src/cart/queries/cartNoteUpdateDefault.test.ts index 28662b3d6f..f65ed29698 100644 --- a/packages/hydrogen/src/cart/queries/cartNoteUpdateDefault.test.ts +++ b/packages/hydrogen/src/cart/queries/cartNoteUpdateDefault.test.ts @@ -25,6 +25,6 @@ describe('cartNoteUpdateDefault', () => { const result = await cartNote(''); expect(result.cart).toHaveProperty('id', CART_ID); - expect(result.errors?.[0]).toContain(cartFragment); + expect(result.userErrors?.[0]).toContain(cartFragment); }); }); diff --git a/packages/hydrogen/src/cart/queries/cartNoteUpdateDefault.ts b/packages/hydrogen/src/cart/queries/cartNoteUpdateDefault.ts index bf4a337105..7824dc6835 100644 --- a/packages/hydrogen/src/cart/queries/cartNoteUpdateDefault.ts +++ b/packages/hydrogen/src/cart/queries/cartNoteUpdateDefault.ts @@ -1,21 +1,24 @@ +import {StorefrontApiErrors, formatAPIResult} from '../../storefront'; import {MINIMAL_CART_FRAGMENT, USER_ERROR_FRAGMENT} from './cart-fragments'; import type { CartOptionalInput, CartQueryData, + CartQueryDataReturn, CartQueryOptions, } from './cart-types'; export type CartNoteUpdateFunction = ( note: string, optionalParams?: CartOptionalInput, -) => Promise; +) => Promise; export function cartNoteUpdateDefault( options: CartQueryOptions, ): CartNoteUpdateFunction { return async (note, optionalParams) => { - const {cartNoteUpdate} = await options.storefront.mutate<{ + const {cartNoteUpdate, errors} = await options.storefront.mutate<{ cartNoteUpdate: CartQueryData; + errors: StorefrontApiErrors; }>(CART_NOTE_UPDATE_MUTATION(options.cartFragment), { variables: { cartId: options.getCartId(), @@ -23,7 +26,7 @@ export function cartNoteUpdateDefault( ...optionalParams, }, }); - return cartNoteUpdate; + return formatAPIResult(cartNoteUpdate, errors); }; } @@ -41,7 +44,7 @@ export const CART_NOTE_UPDATE_MUTATION = ( cart { ...CartApiMutation } - errors: userErrors { + userErrors { ...CartApiError } } diff --git a/packages/hydrogen/src/cart/queries/cartSelectedDeliveryOptionsUpdateDefault.test.ts b/packages/hydrogen/src/cart/queries/cartSelectedDeliveryOptionsUpdateDefault.test.ts index 7d6e71a0bc..05e0d19af3 100644 --- a/packages/hydrogen/src/cart/queries/cartSelectedDeliveryOptionsUpdateDefault.test.ts +++ b/packages/hydrogen/src/cart/queries/cartSelectedDeliveryOptionsUpdateDefault.test.ts @@ -35,6 +35,6 @@ describe('cartSelectedDeliveryOptionsUpdateDefault', () => { ]); expect(result.cart).toHaveProperty('id', CART_ID); - expect(result.errors?.[0]).toContain(cartFragment); + expect(result.userErrors?.[0]).toContain(cartFragment); }); }); diff --git a/packages/hydrogen/src/cart/queries/cartSelectedDeliveryOptionsUpdateDefault.ts b/packages/hydrogen/src/cart/queries/cartSelectedDeliveryOptionsUpdateDefault.ts index a74e27d1fa..d85132d426 100644 --- a/packages/hydrogen/src/cart/queries/cartSelectedDeliveryOptionsUpdateDefault.ts +++ b/packages/hydrogen/src/cart/queries/cartSelectedDeliveryOptionsUpdateDefault.ts @@ -1,7 +1,9 @@ +import {StorefrontApiErrors, formatAPIResult} from '../../storefront'; import {MINIMAL_CART_FRAGMENT, USER_ERROR_FRAGMENT} from './cart-fragments'; import type { CartOptionalInput, CartQueryData, + CartQueryDataReturn, CartQueryOptions, } from './cart-types'; import type {CartSelectedDeliveryOptionInput} from '@shopify/hydrogen-react/storefront-api-types'; @@ -9,15 +11,16 @@ import type {CartSelectedDeliveryOptionInput} from '@shopify/hydrogen-react/stor export type CartSelectedDeliveryOptionsUpdateFunction = ( selectedDeliveryOptions: CartSelectedDeliveryOptionInput[], optionalParams?: CartOptionalInput, -) => Promise; +) => Promise; export function cartSelectedDeliveryOptionsUpdateDefault( options: CartQueryOptions, ): CartSelectedDeliveryOptionsUpdateFunction { return async (selectedDeliveryOptions, optionalParams) => { - const {cartSelectedDeliveryOptionsUpdate} = + const {cartSelectedDeliveryOptionsUpdate, errors} = await options.storefront.mutate<{ cartSelectedDeliveryOptionsUpdate: CartQueryData; + errors: StorefrontApiErrors; }>(CART_SELECTED_DELIVERY_OPTIONS_UPDATE_MUTATION(options.cartFragment), { variables: { cartId: options.getCartId(), @@ -25,7 +28,7 @@ export function cartSelectedDeliveryOptionsUpdateDefault( ...optionalParams, }, }); - return cartSelectedDeliveryOptionsUpdate; + return formatAPIResult(cartSelectedDeliveryOptionsUpdate, errors); }; } @@ -43,7 +46,7 @@ export const CART_SELECTED_DELIVERY_OPTIONS_UPDATE_MUTATION = ( cart { ...CartApiMutation } - errors: userErrors { + userErrors { ...CartApiError } } diff --git a/packages/hydrogen/src/customer/customer.ts b/packages/hydrogen/src/customer/customer.ts index 8d20f3ade5..3146c3ccbb 100644 --- a/packages/hydrogen/src/customer/customer.ts +++ b/packages/hydrogen/src/customer/customer.ts @@ -206,30 +206,28 @@ export function createCustomerClient({ }); const startTime = new Date().getTime(); - - const response = await fetch( - `${customerAccountUrl}/account/customer/api/${customerApiVersion}/graphql`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'User-Agent': USER_AGENT, - Origin: origin, - Authorization: accessToken, - }, - body: JSON.stringify({ - operationName: 'SomeQuery', - query, - variables, - }), + const url = `${customerAccountUrl}/account/customer/api/${customerApiVersion}/graphql`; + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': USER_AGENT, + Origin: origin, + Authorization: accessToken, }, - ); + body: JSON.stringify({ + operationName: 'SomeQuery', + query, + variables, + }), + }); logSubRequestEvent?.(query, startTime); const body = await response.text(); const errorOptions: GraphQLErrorOptions = { + url, response, type, query, diff --git a/packages/hydrogen/src/index.ts b/packages/hydrogen/src/index.ts index d272235843..9209adfc09 100644 --- a/packages/hydrogen/src/index.ts +++ b/packages/hydrogen/src/index.ts @@ -48,7 +48,8 @@ export { } from './cart/createCartHandler'; export type { MetafieldWithoutOwnerId, - CartQueryData, + CartReturn, + CartQueryDataReturn, CartQueryOptions, CartQueryReturn, } from './cart/queries/cart-types'; diff --git a/packages/hydrogen/src/storefront.test.ts b/packages/hydrogen/src/storefront.test.ts index c1b89eb031..2e907d47bd 100644 --- a/packages/hydrogen/src/storefront.test.ts +++ b/packages/hydrogen/src/storefront.test.ts @@ -1,5 +1,5 @@ import {vi, describe, it, expect} from 'vitest'; -import {StorefrontApiError, createStorefrontClient} from './storefront'; +import {createStorefrontClient} from './storefront'; import {fetchWithServerCache} from './cache/fetch'; import {STOREFRONT_ACCESS_TOKEN_HEADER} from './constants'; import { @@ -155,7 +155,7 @@ describe('createStorefrontClient', () => { ); }); - it('throws when the response contains SFAPI errors', async () => { + it('does not throw when the response contains partial SFAPI errors', async () => { const {storefront} = createStorefrontClient({ storeDomain, storefrontId, @@ -164,13 +164,18 @@ describe('createStorefrontClient', () => { }); vi.mocked(fetchWithServerCache).mockResolvedValueOnce([ - {errors: [{message: 'first'}, {message: 'second'}]}, + { + data: {cart: {}}, + errors: [{message: 'first'}, {message: 'second'}], + }, new Response('ok', {status: 200}), ]); - await expect(storefront.query('query {}')).rejects.toThrowError( - StorefrontApiError, - ); + const data = await storefront.query('query {}'); + expect(data).toMatchObject({ + cart: {}, + errors: [{message: 'first'}, {message: 'second'}], + }); }); }); }); diff --git a/packages/hydrogen/src/storefront.ts b/packages/hydrogen/src/storefront.ts index c91ddac82f..635601fa02 100644 --- a/packages/hydrogen/src/storefront.ts +++ b/packages/hydrogen/src/storefront.ts @@ -45,6 +45,7 @@ import { throwGraphQLError, type GraphQLApiResponse, type GraphQLErrorOptions, + GraphQLFormattedError, } from './utils/graphql'; import {getCallerStackLine} from './utils/callsites'; @@ -53,6 +54,11 @@ export type I18nBase = { country: CountryCode; }; +export type StorefrontApiErrors = GraphQLFormattedError[] | undefined; +export type StorefrontError = { + errors?: StorefrontApiErrors; +}; + /** * Wraps all the returned utilities from `createStorefrontClient`. */ @@ -84,6 +90,7 @@ type AutoAddedVariableNames = 'country' | 'language'; type StorefrontCommonExtraParams = { headers?: HeadersInit; storefrontApiVersion?: string; + displayName?: string; }; /** @@ -103,7 +110,8 @@ export type Storefront = { AutoAddedVariableNames > ) => Promise< - ClientReturn + ClientReturn & + StorefrontError >; /** The function to run a mutation on Storefront API. */ mutate: < @@ -118,7 +126,8 @@ export type Storefront = { AutoAddedVariableNames > ) => Promise< - ClientReturn + ClientReturn & + StorefrontError >; /** The cache instance passed in from the `createStorefrontClient` argument. */ cache?: Cache; @@ -148,7 +157,9 @@ export type Storefront = { getApiUrl: ReturnType< typeof createStorefrontUtilities >['getStorefrontApiUrl']; - /** Determines if the error is resulted from a Storefront API call. */ + /** + * @deprecated Use the `errors` object returned from the API if exists. + * */ isApiError: (error: any) => boolean; /** The `i18n` object passed in from the `createStorefrontClient` argument. */ i18n: TI18n; @@ -185,22 +196,14 @@ type StorefrontQueryOptions = StorefrontCommonExtraParams & { query: string; mutation?: never; cache?: CachingStrategy; - /** The name to be shown in the Subrequest Profiler */ - displayName?: string; }; type StorefrontMutationOptions = StorefrontCommonExtraParams & { query?: never; mutation: string; cache?: never; - /** The name to be shown in the Subrequest Profiler */ - displayName?: string; }; -export const StorefrontApiError = class extends Error {} as ErrorConstructor; -export const isStorefrontApiError = (error: any) => - error instanceof StorefrontApiError; - const defaultI18n: I18nBase = {language: 'EN', country: 'US'}; /** @@ -281,7 +284,7 @@ export function createStorefrontClient( }: {variables?: GenericVariables} & ( | StorefrontQueryOptions | StorefrontMutationOptions - )): Promise { + )): Promise { const userHeaders = headers instanceof Headers ? Object.fromEntries(headers.entries()) @@ -335,6 +338,7 @@ export function createStorefrontClient( shouldCacheResponse: checkGraphQLErrors, waitUntil, debugInfo: { + url, graphql: graphqlData, requestId: requestInit.headers[STOREFRONT_REQUEST_GROUP_ID_HEADER], purpose: storefrontHeaders?.purpose, @@ -344,6 +348,7 @@ export function createStorefrontClient( }); const errorOptions: GraphQLErrorOptions = { + url, response, type: mutation ? 'mutation' : 'query', query, @@ -368,15 +373,7 @@ export function createStorefrontClient( const {data, errors} = body as GraphQLApiResponse; - if (errors?.length) { - throwGraphQLError({ - ...errorOptions, - errors, - ErrorConstructor: StorefrontApiError, - }); - } - - return data as T; + return formatAPIResult(data, errors as StorefrontApiErrors); } return { @@ -449,6 +446,7 @@ export function createStorefrontClient( getShopifyDomain, getApiUrl: getStorefrontApiUrl, /** + * @deprecated * Wether it's a GraphQL error returned in the Storefront API response. * * Example: @@ -467,8 +465,26 @@ export function createStorefrontClient( * } * ``` */ - isApiError: isStorefrontApiError, + isApiError: (error: any) => { + if (process.env.NODE_ENV === 'development') { + warnOnce( + '`isApiError` is deprecated. An `errors` object would be returned from the API if there is an error.', + ); + } + return false; + }, i18n: (i18n ?? defaultI18n) as TI18n, }, }; } + +export function formatAPIResult(data: T, errors: StorefrontApiErrors) { + let result = data; + if (errors) { + result = { + ...data, + errors, + }; + } + return result as T & StorefrontError; +} diff --git a/packages/hydrogen/src/utils/graphql.ts b/packages/hydrogen/src/utils/graphql.ts index 177664d5a1..e2eebe3c12 100644 --- a/packages/hydrogen/src/utils/graphql.ts +++ b/packages/hydrogen/src/utils/graphql.ts @@ -26,6 +26,7 @@ export function assertMutation(query: string, callerName: string) { export type GraphQLApiResponse = StorefrontApiResponseOk; export type GraphQLErrorOptions = { + url: string; response: Response; errors: GraphQLApiResponse['errors']; type: 'query' | 'mutation'; @@ -35,7 +36,41 @@ export type GraphQLErrorOptions = { client?: string; }; +// Reference: https://github.com/graphql/graphql-js/blob/main/src/language/location.ts#L10-L13 +type SourceLocation = { + readonly line: number; + readonly column: number; +}; + +// Reference: https://github.com/graphql/graphql-js/blob/main/src/error/GraphQLError.ts#L218-L242 +export type GraphQLFormattedError = { + /** + * A short, human-readable summary of the problem that **SHOULD NOT** change + * from occurrence to occurrence of the problem, except for purposes of + * localization. + */ + readonly message: string; + /** + * If an error can be associated to a particular point in the requested + * GraphQL document, it should contain a list of locations. + */ + readonly locations?: ReadonlyArray; + /** + * If an error can be associated to a particular field in the GraphQL result, + * it _must_ contain an entry with the key `path` that details the path of + * the response field which experienced the error. This allows clients to + * identify whether a null result is intentional or caused by a runtime error. + */ + readonly path?: ReadonlyArray; + /** + * Reserved for implementors to extend the protocol however they see fit, + * and hence there are no additional restrictions on its contents. + */ + readonly extensions?: {[key: string]: unknown}; +}; + export function throwGraphQLError({ + url, response, errors, type, @@ -49,7 +84,7 @@ export function throwGraphQLError({ (typeof errors === 'string' ? errors : errors?.map?.((error) => error.message).join('\n')) || - `API response error: ${response.status}`; + `URL: ${url}\nAPI response error: ${response.status}`; throw new ErrorConstructor( `[h2:error:${client}.${type}] ` + diff --git a/templates/demo-store/app/components/Cart.tsx b/templates/demo-store/app/components/Cart.tsx index 76cfc82262..df4fd38702 100644 --- a/templates/demo-store/app/components/Cart.tsx +++ b/templates/demo-store/app/components/Cart.tsx @@ -8,6 +8,7 @@ import { Money, useOptimisticData, OptimisticInput, + type CartReturn, } from '@shopify/hydrogen'; import type { Cart as CartType, @@ -35,7 +36,7 @@ export function Cart({ }: { layout: Layouts; onClose?: () => void; - cart: CartType | null; + cart: CartReturn | null; }) { const linesCount = Boolean(cart?.lines?.edges?.length || 0); diff --git a/templates/demo-store/app/routes/($locale).cart.tsx b/templates/demo-store/app/routes/($locale).cart.tsx index 455c2fa1df..1060005e3b 100644 --- a/templates/demo-store/app/routes/($locale).cart.tsx +++ b/templates/demo-store/app/routes/($locale).cart.tsx @@ -5,7 +5,7 @@ import { type ActionFunctionArgs, json, } from '@shopify/remix-oxygen'; -import {CartForm, type CartQueryData} from '@shopify/hydrogen'; +import {CartForm, type CartQueryDataReturn} from '@shopify/hydrogen'; import {isLocalPath} from '~/lib/utils'; import {Cart} from '~/components'; @@ -32,7 +32,7 @@ export async function action({request, context}: ActionFunctionArgs) { invariant(action, 'No cartAction defined'); let status = 200; - let result: CartQueryData; + let result: CartQueryDataReturn; switch (action) { case CartForm.ACTIONS.LinesAdd: @@ -79,13 +79,14 @@ export async function action({request, context}: ActionFunctionArgs) { headers.set('Location', redirectTo); } - const {cart: cartResult, errors} = result; + const {cart: cartResult, errors, userErrors} = result; headers.append('Set-Cookie', await context.session.commit()); return json( { cart: cartResult, + userErrors, errors, analytics: { cartId, diff --git a/templates/skeleton/app/routes/cart.tsx b/templates/skeleton/app/routes/cart.tsx index 193600c8ef..e944c23216 100644 --- a/templates/skeleton/app/routes/cart.tsx +++ b/templates/skeleton/app/routes/cart.tsx @@ -1,6 +1,6 @@ import {Await, type MetaFunction} from '@remix-run/react'; import {Suspense} from 'react'; -import type {CartQueryData} from '@shopify/hydrogen'; +import type {CartQueryDataReturn} from '@shopify/hydrogen'; import {CartForm} from '@shopify/hydrogen'; import {json, type ActionFunctionArgs} from '@shopify/remix-oxygen'; import {CartMain} from '~/components/Cart'; @@ -34,7 +34,7 @@ export async function action({request, context}: ActionFunctionArgs) { } let status = 200; - let result: CartQueryData; + let result: CartQueryDataReturn; switch (action) { case CartForm.ACTIONS.LinesAdd: diff --git a/templates/skeleton/app/routes/search.tsx b/templates/skeleton/app/routes/search.tsx index 09d829e0e7..407718ebc4 100644 --- a/templates/skeleton/app/routes/search.tsx +++ b/templates/skeleton/app/routes/search.tsx @@ -21,7 +21,7 @@ export async function loader({request, context}: LoaderFunctionArgs) { }; } - const data = await context.storefront.query(SEARCH_QUERY, { + const {errors, ...data} = await context.storefront.query(SEARCH_QUERY, { variables: { query: searchTerm, ...variables,