Skip to content
This repository has been archived by the owner on Apr 11, 2024. It is now read-only.

Commit

Permalink
Stop sending deprecated field in webhook queries
Browse files Browse the repository at this point in the history
  • Loading branch information
paulomarg committed Jun 22, 2023
1 parent cac0811 commit 9735d0c
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-games-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/shopify-api': minor
---

Stop sending the privateMetafieldNamespaces field in webhook queries to avoid the API duplication warning, and added a new shopify.utils.versionPriorTo method to help with cases like this one where apps will need to stop doing something that was deprecated.
13 changes: 7 additions & 6 deletions docs/reference/utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

This object contains generic helper functions that might simplify app code.

| Property | Description |
| ------------------------------------------- | ------------------------------------------------------------------------------------ |
| [sanitizeShop](./sanitizeShop.md) | Validates and sanitizes Shopify shop domains to make user input safer. |
| [sanitizeHost](./sanitizeHost.md) | Validates and sanitizes the `host` argument from Shopify for embedded apps. |
| [validateHmac](./validateHmac.md) | Validates the HMAC signature in requests from Shopify. |
| [versionCompatible](./versionCompatible.md) | Checks whether the given API version is compatible with the current library version. |
| Property | Description |
| ------------------------------------------- | ------------------------------------------------------------------------------------------- |
| [sanitizeShop](./sanitizeShop.md) | Validates and sanitizes Shopify shop domains to make user input safer. |
| [sanitizeHost](./sanitizeHost.md) | Validates and sanitizes the `host` argument from Shopify for embedded apps. |
| [validateHmac](./validateHmac.md) | Validates the HMAC signature in requests from Shopify. |
| [versionCompatible](./versionCompatible.md) | Checks whether the current library version is equal to or newer than the given API version. |
| [versionPriorTo](./versionPriorTo.md) | Checks whether the current library version is older than the given API version. |

[Back to shopifyApi](../shopifyApi.md)
36 changes: 36 additions & 0 deletions docs/reference/utils/versionPriorTo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# shopify.utils.versionPriorTo

This method determines if the given version is older than the configured `apiVersion` for the `shopifyApi` object.
Its main use is when you want to tweak behaviour depending on your current API version, though apps won't typically need this kind of check.

## Example

```ts
const shopify = shopifyApi({
apiVersion: ApiVersion.July23,
});

if (shopify.utils.versionPriorTo(ApiVersion.July23)) {
// false in this example, as both versions are July23
}
if (shopify.utils.versionPriorTo(ApiVersion.October23)) {
// true in this example, as ApiVersion.October23 is newer than ApiVersion.July23, i.e. the configured version is older
// than the reference one
}
```

## Parameters

### apiVersion

`ApiVersion` | :exclamation: required

The API version to check against.

## Return

`boolean`

Whether the reference version is older than the configured version.

[Back to shopify.utils](./README.md)
70 changes: 70 additions & 0 deletions lib/utils/__tests__/version-compatible.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {shopify} from '../../__tests__/test-helper';
import {ApiVersion} from '../../types';

describe('versionCompatible', () => {
it('returns true if version is Unstable', () => {
shopify.config.apiVersion = ApiVersion.Unstable;

const result = shopify.utils.versionCompatible(ApiVersion.April23);

expect(result).toBe(true);
});

it('returns true if version is equal to the configured one', () => {
shopify.config.apiVersion = ApiVersion.April23;

const result = shopify.utils.versionCompatible(ApiVersion.April23);

expect(result).toBe(true);
});

it('returns true if version is newer than the configured one', () => {
shopify.config.apiVersion = ApiVersion.April23;

const result = shopify.utils.versionCompatible(ApiVersion.January23);

expect(result).toBe(true);
});

it('returns false if version is older than the configured one', () => {
shopify.config.apiVersion = ApiVersion.January23;

const result = shopify.utils.versionCompatible(ApiVersion.April23);

expect(result).toBe(false);
});
});

describe('versionPriorTo', () => {
it('returns false if version is Unstable (unstable is newer than any version)', () => {
shopify.config.apiVersion = ApiVersion.Unstable;

const result = shopify.utils.versionPriorTo(ApiVersion.April23);

expect(result).toBe(false);
});

it('returns false if version is equal to the configured one', () => {
shopify.config.apiVersion = ApiVersion.April23;

const result = shopify.utils.versionPriorTo(ApiVersion.April23);

expect(result).toBe(false);
});

it('returns false if version is newer than the configured one', () => {
shopify.config.apiVersion = ApiVersion.April23;

const result = shopify.utils.versionPriorTo(ApiVersion.January23);

expect(result).toBe(false);
});

it('returns true if version is older than the configured one', () => {
shopify.config.apiVersion = ApiVersion.January23;

const result = shopify.utils.versionPriorTo(ApiVersion.April23);

expect(result).toBe(true);
});
});
3 changes: 2 additions & 1 deletion lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import {ConfigInterface} from '../base-types';

import {sanitizeShop, sanitizeHost} from './shop-validator';
import {validateHmac} from './hmac-validator';
import {versionCompatible} from './version-compatible';
import {versionCompatible, versionPriorTo} from './version-compatible';

export function shopifyUtils(config: ConfigInterface) {
return {
sanitizeShop: sanitizeShop(config),
sanitizeHost: sanitizeHost(config),
validateHmac: validateHmac(config),
versionCompatible: versionCompatible(config),
versionPriorTo: versionPriorTo(config),
};
}

Expand Down
9 changes: 9 additions & 0 deletions lib/utils/version-compatible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ export function versionCompatible(config: ConfigInterface) {
return current >= reference;
};
}

export function versionPriorTo(config: ConfigInterface) {
return (
referenceVersion: ApiVersion,
currentVersion: ApiVersion = config.apiVersion,
): boolean => {
return !versionCompatible(config)(referenceVersion, currentVersion);
};
}
69 changes: 65 additions & 4 deletions lib/webhooks/__tests__/register.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
WebhookHandler,
WebhookOperation,
} from '../types';
import {gdprTopics, ShopifyHeader} from '../../types';
import {ApiVersion, gdprTopics, ShopifyHeader} from '../../types';
import {DataType} from '../../clients/types';
import {
queueMockResponse,
Expand All @@ -28,6 +28,7 @@ interface RegisterTestWebhook {
handler: WebhookHandler;
checkMockResponse?: MockResponse;
responses?: MockResponse[];
includePrivateMetafieldNamespaces?: boolean;
}

interface RegisterTestResponse {
Expand Down Expand Up @@ -394,13 +395,64 @@ describe('shopify.webhooks.register', () => {
WebhookOperation.Update,
);
});

it('stops sending the privateMetafieldNamespaces field in versions after 2022-04', async () => {
shopify.config.apiVersion = ApiVersion.April23;

const topic = 'PRODUCTS_CREATE';
const handler = {...HTTP_HANDLER, privateMetafieldNamespaces: ['test']};
const responses = [mockResponses.successResponse];

const registerReturn = await registerWebhook({
topic,
handler,
responses,
});

assertWebhookRegistrationRequest(
'webhookSubscriptionCreate',
`topic: ${topic}`,
{callbackUrl: `"https://test_host_name/webhooks"`},
);
assertRegisterResponse({registerReturn, topic, responses});
});

// The test above this becomes moot when this API version is removed. Delete it as well
it('sends the privateMetafieldNamespaces field up until version 2022-04', async () => {
shopify.config.apiVersion = ApiVersion.April22;

const topic = 'PRODUCTS_CREATE';
const handler = {...HTTP_HANDLER, privateMetafieldNamespaces: ['test']};
const responses = [mockResponses.successResponse];

const registerReturn = await registerWebhook({
topic,
handler,
responses,
includePrivateMetafieldNamespaces: true,
});

assertWebhookRegistrationRequest(
'webhookSubscriptionCreate',
`topic: ${topic}`,
{
callbackUrl: `"https://test_host_name/webhooks", privateMetafieldNamespaces: ["test"]`,
},
);
assertRegisterResponse({
registerReturn,
topic,
responses,
});
});
});

async function registerWebhook({
topic,
handler,
checkMockResponse = mockResponses.webhookCheckEmptyResponse,
responses = [],
includePrivateMetafieldNamespaces = false,
}: RegisterTestWebhook): Promise<RegisterReturn> {
shopify.webhooks.addHandlers({[topic]: handler});

Expand All @@ -413,7 +465,7 @@ async function registerWebhook({

expect(mockTestRequests.requestList).toHaveLength(responses.length + 1);

assertWebhookCheckRequest({session});
assertWebhookCheckRequest({session}, includePrivateMetafieldNamespaces);

return result;
}
Expand All @@ -432,7 +484,16 @@ function assertRegisterResponse({
});
}

function assertWebhookCheckRequest({session}: RegisterParams) {
function assertWebhookCheckRequest(
{session}: RegisterParams,
includePrivateMetafieldNamespaces = false,
) {
let query = queryTemplate(TEMPLATE_GET_HANDLERS, {END_CURSOR: 'null'});

if (!includePrivateMetafieldNamespaces) {
query = query.replace('privateMetafieldNamespaces', '');
}

expect({
method: Method.Post.toString(),
domain: session.shop,
Expand All @@ -441,7 +502,7 @@ function assertWebhookCheckRequest({session}: RegisterParams) {
[Header.ContentType]: DataType.GraphQL.toString(),
[ShopifyHeader.AccessToken]: session.accessToken,
},
data: queryTemplate(TEMPLATE_GET_HANDLERS, {END_CURSOR: 'null'}),
data: query,
}).toMatchMadeHttpRequest();
}

Expand Down
21 changes: 16 additions & 5 deletions lib/webhooks/register.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {versionCompatible, versionPriorTo} from '../utils/version-compatible';
import {
graphqlClientClass,
GraphqlClient,
} from '../clients/graphql/graphql_client';
import {InvalidDeliveryMethodError, ShopifyError} from '../error';
import {logger} from '../logger';
import {gdprTopics} from '../types';
import {ApiVersion, gdprTopics} from '../types';
import {ConfigInterface} from '../base-types';
import {Session} from '../session/session';

Expand Down Expand Up @@ -126,7 +127,7 @@ async function getExistingHandlers(
let hasNextPage: boolean;
let endCursor: string | null = null;
do {
const query = buildCheckQuery(endCursor);
const query = buildCheckQuery(config, endCursor);

const response = await client.query<WebhookCheckResponse>({
data: query,
Expand All @@ -149,10 +150,17 @@ async function getExistingHandlers(
return existingHandlers;
}

function buildCheckQuery(endCursor: string | null) {
return queryTemplate(TEMPLATE_GET_HANDLERS, {
function buildCheckQuery(config: ConfigInterface, endCursor: string | null) {
let query = queryTemplate(TEMPLATE_GET_HANDLERS, {
END_CURSOR: JSON.stringify(endCursor),
});

// The privateMetafieldNamespaces field was deprecated in the July22 version, so we need to stop sending it
if (versionCompatible(config)(ApiVersion.July22)) {
query = query.replace('privateMetafieldNamespaces', '');
}

return query;
}

function buildHandlerFromNode(edge: WebhookCheckResponseNode): WebhookHandler {
Expand Down Expand Up @@ -433,9 +441,12 @@ function buildMutation(
if (handler.metafieldNamespaces) {
params.metafieldNamespaces = JSON.stringify(handler.metafieldNamespaces);
}

if (
handler.deliveryMethod === DeliveryMethod.Http &&
handler.privateMetafieldNamespaces
handler.privateMetafieldNamespaces &&
// This field was deprecated in the July22 version
versionPriorTo(config)(ApiVersion.July22)
) {
params.privateMetafieldNamespaces = JSON.stringify(
handler.privateMetafieldNamespaces,
Expand Down

0 comments on commit 9735d0c

Please sign in to comment.