From 2965e1a9f07ef99e50fd0647f28b17c213dc0476 Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Wed, 4 Jan 2023 14:10:42 -0500 Subject: [PATCH 1/4] Reinstate REST resource tests --- .../rest/__tests__/resources/base.test.ts | 494 ++++++++++++++++++ .../fake-resource-with-custom-prefix.ts | 42 ++ .../rest/__tests__/resources/fake-resource.ts | 136 +++++ .../resources/load-rest-resources.test.ts | 33 ++ .../__tests__/resources/test-resources.ts | 14 + rest/__tests__/load-rest-resources.test.ts | 32 -- 6 files changed, 719 insertions(+), 32 deletions(-) create mode 100644 lib/clients/rest/__tests__/resources/base.test.ts create mode 100644 lib/clients/rest/__tests__/resources/fake-resource-with-custom-prefix.ts create mode 100644 lib/clients/rest/__tests__/resources/fake-resource.ts create mode 100644 lib/clients/rest/__tests__/resources/load-rest-resources.test.ts create mode 100644 lib/clients/rest/__tests__/resources/test-resources.ts delete mode 100644 rest/__tests__/load-rest-resources.test.ts diff --git a/lib/clients/rest/__tests__/resources/base.test.ts b/lib/clients/rest/__tests__/resources/base.test.ts new file mode 100644 index 000000000..4cd752f86 --- /dev/null +++ b/lib/clients/rest/__tests__/resources/base.test.ts @@ -0,0 +1,494 @@ +import {Session} from '../../../../session/session'; +import {HttpResponseError} from '../../../../error'; +import { + testConfig, + queueMockResponse, + queueMockResponses, +} from '../../../../__tests__/test-helper'; +import {shopifyApi, Shopify} from '../../../../index'; + +import {restResources} from './test-resources'; + +let prefix: string; +let shopify: Shopify; + +beforeEach(() => { + shopify = shopifyApi({ + ...testConfig, + restResources, + }); + + prefix = `/admin/api/${shopify.config.apiVersion}`; +}); + +describe('Base REST resource', () => { + const domain = 'test-shop.myshopify.io'; + const headers = {'X-Shopify-Access-Token': 'access-token'}; + const session = new Session({ + id: '1234', + shop: domain, + state: '1234', + isOnline: true, + }); + session.accessToken = 'access-token'; + + it('finds resource by id', async () => { + const body = {fake_resource: {id: 1, attribute: 'attribute'}}; + queueMockResponse(JSON.stringify(body)); + + const got = await shopify.rest.FakeResource.find({id: 1, session}); + + expect([got!.id, got!.attribute]).toEqual([1, 'attribute']); + expect({ + method: 'GET', + domain, + path: `${prefix}/fake_resources/1.json`, + headers, + }).toMatchMadeHttpRequest(); + }); + + it('finds resource with param', async () => { + const body = {fake_resource: {id: 1, attribute: 'attribute'}}; + queueMockResponse(JSON.stringify(body)); + + const got = await shopify.rest.FakeResource.find({ + id: 1, + session, + params: {param: 'value'}, + }); + + expect([got!.id, got!.attribute]).toEqual([1, 'attribute']); + expect({ + method: 'GET', + domain, + path: `${prefix}/fake_resources/1.json?param=value`, + headers, + }).toMatchMadeHttpRequest(); + }); + + it('finds resource and children by id', async () => { + const body = { + fake_resource: { + id: 1, + attribute: 'attribute1', + has_one_attribute: {id: 2, attribute: 'attribute2'}, + has_many_attribute: [{id: 3, attribute: 'attribute3'}], + }, + }; + queueMockResponse(JSON.stringify(body)); + + const got = await shopify.rest.FakeResource.find({id: 1, session}); + + expect([got!.id, got!.attribute]).toEqual([1, 'attribute1']); + + expect(got!.has_one_attribute!.constructor.name).toEqual('FakeResource'); + expect([ + got!.has_one_attribute!.id, + got!.has_one_attribute!.attribute, + ]).toEqual([2, 'attribute2']); + + expect(got!.has_many_attribute![0].constructor.name).toEqual( + 'FakeResource', + ); + expect([ + got!.has_many_attribute![0].id, + got!.has_many_attribute![0].attribute, + ]).toEqual([3, 'attribute3']); + + expect({ + method: 'GET', + domain, + path: `${prefix}/fake_resources/1.json`, + headers, + }).toMatchMadeHttpRequest(); + }); + + it('fails on finding nonexistent resource by id', async () => { + const body = {errors: 'Not Found'}; + queueMockResponse(JSON.stringify(body), { + statusCode: 404, + statusText: 'Not Found', + headers: {'X-Test-Header': 'value'}, + }); + + const expectedError = await expect( + shopify.rest.FakeResource.find({id: 1, session}), + ).rejects; + expectedError.toThrowError(HttpResponseError); + expectedError.toMatchObject({ + response: { + body: {errors: 'Not Found'}, + code: 404, + statusText: 'Not Found', + headers: {'X-Test-Header': ['value']}, + }, + }); + + expect({ + method: 'GET', + domain, + path: `${prefix}/fake_resources/1.json`, + headers, + }).toMatchMadeHttpRequest(); + }); + + it('finds all resources', async () => { + const body = { + fake_resources: [ + {id: 1, attribute: 'attribute1'}, + {id: 2, attribute: 'attribute2'}, + ], + }; + queueMockResponse(JSON.stringify(body)); + + const got = await shopify.rest.FakeResource.all({session}); + + expect([got![0].id, got![0].attribute]).toEqual([1, 'attribute1']); + expect([got![1].id, got![1].attribute]).toEqual([2, 'attribute2']); + expect({ + method: 'GET', + domain, + path: `${prefix}/fake_resources.json`, + headers, + }).toMatchMadeHttpRequest(); + }); + + it('saves', async () => { + const expectedRequestBody = {fake_resource: {attribute: 'attribute'}}; + const responseBody = {fake_resource: {id: 1, attribute: 'attribute'}}; + queueMockResponse(JSON.stringify(responseBody)); + + const resource = new shopify.rest.FakeResource({session}); + resource.attribute = 'attribute'; + await resource.save(); + + expect(resource.id).toBeUndefined(); + expect({ + method: 'POST', + domain, + path: `${prefix}/fake_resources.json`, + headers, + data: JSON.stringify(expectedRequestBody), + }).toMatchMadeHttpRequest(); + }); + + it('saves and updates', async () => { + const expectedRequestBody = {fake_resource: {attribute: 'attribute'}}; + const responseBody = {fake_resource: {id: 1, attribute: 'attribute'}}; + queueMockResponse(JSON.stringify(responseBody)); + + const resource = new shopify.rest.FakeResource({session}); + resource.attribute = 'attribute'; + await resource.saveAndUpdate(); + + expect(resource.id).toEqual(1); + expect({ + method: 'POST', + domain, + path: `${prefix}/fake_resources.json`, + headers, + data: JSON.stringify(expectedRequestBody), + }).toMatchMadeHttpRequest(); + }); + + it('saves existing resource', async () => { + const expectedRequestBody = { + fake_resource: {id: 1, attribute: 'attribute'}, + }; + const responseBody = {fake_resource: {id: 1, attribute: 'attribute'}}; + queueMockResponse(JSON.stringify(responseBody)); + + const resource = new shopify.rest.FakeResource({session}); + resource.id = 1; + resource.attribute = 'attribute'; + await resource.save(); + + expect(resource.id).toEqual(1); + expect({ + method: 'PUT', + domain, + path: `${prefix}/fake_resources/1.json`, + headers, + data: JSON.stringify(expectedRequestBody), + }).toMatchMadeHttpRequest(); + }); + + it('saves with children', async () => { + const expectedRequestBody = { + fake_resource: { + id: 1, + attribute: 'attribute', + has_one_attribute: {attribute: 'attribute1'}, + has_many_attribute: [ + {attribute: 'attribute2'}, + {attribute: 'attribute3'}, + ], + }, + }; + queueMockResponse(JSON.stringify({})); + + const child1 = new shopify.rest.FakeResource({session}); + child1.attribute = 'attribute1'; + + const child2 = new shopify.rest.FakeResource({session}); + child2.attribute = 'attribute2'; + + const child3 = new shopify.rest.FakeResource({session}); + child3.attribute = 'attribute3'; + + const resource = new shopify.rest.FakeResource({session}); + resource.id = 1; + resource.attribute = 'attribute'; + resource.has_one_attribute = child1; + resource.has_many_attribute = [child2, child3]; + + await resource.save(); + + expect({ + method: 'PUT', + domain, + path: `${prefix}/fake_resources/1.json`, + headers, + data: JSON.stringify(expectedRequestBody), + }).toMatchMadeHttpRequest(); + }); + + it('loads unknown attribute', async () => { + const responseBody = { + fake_resource: {attribute: 'attribute', 'unknown?': 'some-value'}, + }; + queueMockResponse(JSON.stringify(responseBody)); + + const got = await shopify.rest.FakeResource.find({id: 1, session}); + + expect(got!.attribute).toEqual('attribute'); + expect(got!['unknown?']).toEqual('some-value'); + expect(got!.serialize()['unknown?']).toEqual('some-value'); + }); + + it('saves with unknown attribute', async () => { + const expectedRequestBody = {fake_resource: {unknown: 'some-value'}}; + queueMockResponse(JSON.stringify({})); + + const resource = new shopify.rest.FakeResource({session}); + resource.unknown = 'some-value'; + await resource.save(); + + expect({ + method: 'POST', + domain, + path: `${prefix}/fake_resources.json`, + headers, + data: JSON.stringify(expectedRequestBody), + }).toMatchMadeHttpRequest(); + }); + + it('saves forced null attributes', async () => { + const expectedRequestBody = { + fake_resource: {id: 1, has_one_attribute: null}, + }; + queueMockResponse(JSON.stringify({})); + + const resource = new shopify.rest.FakeResource({session}); + resource.id = 1; + resource.has_one_attribute = null; + await resource.save(); + + expect({ + method: 'PUT', + domain, + path: `${prefix}/fake_resources/1.json`, + headers, + data: JSON.stringify(expectedRequestBody), + }).toMatchMadeHttpRequest(); + }); + + it('ignores unsaveable attribute', async () => { + const expectedRequestBody = {fake_resource: {attribute: 'attribute'}}; + const responseBody = {fake_resource: {id: 1, attribute: 'attribute'}}; + queueMockResponse(JSON.stringify(responseBody)); + + const resource = new shopify.rest.FakeResource({session}); + resource.attribute = 'attribute'; + resource.unsaveable_attribute = 'unsaveable_attribute'; + await resource.save(); + + expect(resource.id).toBeUndefined(); + expect({ + method: 'POST', + domain, + path: `${prefix}/fake_resources.json`, + headers, + data: JSON.stringify(expectedRequestBody), + }).toMatchMadeHttpRequest(); + }); + + it('deletes existing resource', async () => { + queueMockResponse(JSON.stringify({})); + + const resource = new shopify.rest.FakeResource({session}); + resource.id = 1; + + await resource.delete(); + + expect({ + method: 'DELETE', + domain, + path: `${prefix}/fake_resources/1.json`, + headers, + }).toMatchMadeHttpRequest(); + }); + + it('deletes with other resource', async () => { + queueMockResponse(JSON.stringify({})); + + const resource = new shopify.rest.FakeResource({session}); + resource.id = 1; + resource.other_resource_id = 2; + + await resource.delete(); + + expect({ + method: 'DELETE', + domain, + path: `${prefix}/other_resources/2/fake_resources/1.json`, + headers, + }).toMatchMadeHttpRequest(); + }); + + it('fails to delete nonexistent resource', async () => { + const body = {errors: 'Not Found'}; + queueMockResponse(JSON.stringify(body), { + statusCode: 404, + statusText: 'Not Found', + }); + + const resource = new shopify.rest.FakeResource({session}); + resource.id = 1; + + await expect(resource.delete()).rejects.toThrowError(HttpResponseError); + + expect({ + method: 'DELETE', + domain, + path: `${prefix}/fake_resources/1.json`, + headers, + }).toMatchMadeHttpRequest(); + }); + + it('makes custom request', async () => { + const body = {fake_resource: {id: 1, attribute: 'attribute'}}; + queueMockResponse(JSON.stringify(body)); + + const got = await shopify.rest.FakeResource.custom({ + session, + id: 1, + other_resource_id: 2, + }); + + expect(got).toEqual(body); + expect({ + method: 'GET', + domain, + path: `${prefix}/other_resources/2/fake_resources/1/custom.json`, + headers, + }).toMatchMadeHttpRequest(); + }); + + it('paginates requests', async () => { + const previousUrl = `https://${domain}/admin/api/${shopify.config.apiVersion}/fake_resources.json?page_info=previousToken`; + const nextUrl = `https://${domain}/admin/api/${shopify.config.apiVersion}/fake_resources.json?page_info=nextToken`; + + const body = {fake_resources: []}; + queueMockResponses( + [JSON.stringify(body), {headers: {link: `<${nextUrl}>; rel="next"`}}], + [ + JSON.stringify(body), + {headers: {link: `<${previousUrl}>; rel="previous"`}}, + ], + [JSON.stringify(body), {}], + ); + + await shopify.rest.FakeResource.all({session}); + expect(shopify.rest.FakeResource.NEXT_PAGE_INFO).not.toBeUndefined(); + expect(shopify.rest.FakeResource.PREV_PAGE_INFO).toBeUndefined(); + + await shopify.rest.FakeResource.all({ + session, + params: shopify.rest.FakeResource.NEXT_PAGE_INFO?.query, + }); + expect(shopify.rest.FakeResource.NEXT_PAGE_INFO).toBeUndefined(); + expect(shopify.rest.FakeResource.PREV_PAGE_INFO).not.toBeUndefined(); + + await shopify.rest.FakeResource.all({ + session, + params: shopify.rest.FakeResource.PREV_PAGE_INFO?.query, + }); + expect(shopify.rest.FakeResource.NEXT_PAGE_INFO).toBeUndefined(); + expect(shopify.rest.FakeResource.PREV_PAGE_INFO).toBeUndefined(); + + expect({ + method: 'GET', + domain, + path: `${prefix}/fake_resources.json`, + headers, + }).toMatchMadeHttpRequest(); + expect({ + method: 'GET', + domain, + path: `${prefix}/fake_resources.json?page_info=nextToken`, + headers, + }).toMatchMadeHttpRequest(); + expect({ + method: 'GET', + domain, + path: `${prefix}/fake_resources.json?page_info=previousToken`, + headers, + }).toMatchMadeHttpRequest(); + }); + + it('allows custom prefixes', async () => { + const body = { + fake_resource_with_custom_prefix: {id: 1, attribute: 'attribute'}, + }; + queueMockResponse(JSON.stringify(body)); + + const got = await shopify.rest.FakeResourceWithCustomPrefix.find({ + id: 1, + session, + }); + + expect([got!.id, got!.attribute]).toEqual([1, 'attribute']); + expect({ + method: 'GET', + domain, + path: `/admin/custom_prefix/fake_resource_with_custom_prefix/1.json`, + headers, + }).toMatchMadeHttpRequest(); + }); + + it('includes unsaveable attributes when default serialize called', async () => { + const resource = new shopify.rest.FakeResource({session}); + resource.attribute = 'attribute'; + resource.unsaveable_attribute = 'unsaveable_attribute'; + + const hash = resource.serialize(); + + expect(hash).toHaveProperty('unsaveable_attribute', 'unsaveable_attribute'); + expect(hash).toHaveProperty('attribute', 'attribute'); + }); + + it('excludes unsaveable attributes when serialize called for saving', async () => { + const resource = new shopify.rest.FakeResource({session}); + resource.attribute = 'attribute'; + resource.unsaveable_attribute = 'unsaveable_attribute'; + + const hash = resource.serialize(true); + + expect(hash).not.toHaveProperty( + 'unsaveable_attribute', + 'unsaveable_attribute', + ); + expect(hash).toHaveProperty('attribute', 'attribute'); + }); +}); diff --git a/lib/clients/rest/__tests__/resources/fake-resource-with-custom-prefix.ts b/lib/clients/rest/__tests__/resources/fake-resource-with-custom-prefix.ts new file mode 100644 index 000000000..ed0667a2e --- /dev/null +++ b/lib/clients/rest/__tests__/resources/fake-resource-with-custom-prefix.ts @@ -0,0 +1,42 @@ +import {Base} from '../../../../../rest/base'; +import {ResourcePath} from '../../../../../rest/types'; +import {Session} from '../../../../session/session'; +import {LATEST_API_VERSION} from '../../../../types'; + +interface FakeResourceWithCustomPrefixFindArgs { + session: Session; + id: string | number; +} + +export class FakeResourceWithCustomPrefix extends Base { + public static API_VERSION = LATEST_API_VERSION; + protected static NAME = 'fake_resource_with_custom_prefix'; + protected static PLURAL_NAME = 'fake_resource_with_custom_prefixes'; + protected static CUSTOM_PREFIX = '/admin/custom_prefix'; + + protected static HAS_ONE = {}; + protected static HAS_MANY = {}; + + protected static PATHS: ResourcePath[] = [ + { + http_method: 'get', + operation: 'get', + ids: ['id'], + path: 'fake_resource_with_custom_prefix/.json', + }, + ]; + + public static async find({ + session, + id, + }: FakeResourceWithCustomPrefixFindArgs): Promise { + const result = await this.baseFind({ + session, + urlIds: {id}, + }); + return result ? result[0] : null; + } + + id?: number | string | null; + attribute?: string | null; +} diff --git a/lib/clients/rest/__tests__/resources/fake-resource.ts b/lib/clients/rest/__tests__/resources/fake-resource.ts new file mode 100644 index 000000000..f0f03e955 --- /dev/null +++ b/lib/clients/rest/__tests__/resources/fake-resource.ts @@ -0,0 +1,136 @@ +import {Base} from '../../../../../rest/base'; +import {ParamSet, ResourcePath} from '../../../../../rest/types'; +import {Session} from '../../../../session/session'; +import {LATEST_API_VERSION} from '../../../../types'; + +interface FakeResourceFindArgs { + params?: ParamSet; + session: Session; + id: number; + other_resource_id?: number | null; +} + +interface FakeResourceAllArgs { + params?: ParamSet; + session: Session; +} + +interface FakeResourceCustomArgs { + session: Session; + id: number; + other_resource_id: number; +} + +export class FakeResource extends Base { + public static API_VERSION = LATEST_API_VERSION; + protected static NAME = 'fake_resource'; + protected static PLURAL_NAME = 'fake_resources'; + + protected static READ_ONLY_ATTRIBUTES: string[] = ['unsaveable_attribute']; + + protected static HAS_ONE = { + has_one_attribute: this, + }; + + protected static HAS_MANY = { + has_many_attribute: this, + }; + + protected static PATHS: ResourcePath[] = [ + { + http_method: 'get', + operation: 'get', + ids: ['id'], + path: 'fake_resources/.json', + }, + { + http_method: 'get', + operation: 'get', + ids: [], + path: 'fake_resources.json', + }, + { + http_method: 'post', + operation: 'post', + ids: [], + path: 'fake_resources.json', + }, + { + http_method: 'put', + operation: 'put', + ids: ['id'], + path: 'fake_resources/.json', + }, + { + http_method: 'delete', + operation: 'delete', + ids: ['id'], + path: 'fake_resources/.json', + }, + { + http_method: 'get', + operation: 'get', + ids: ['id', 'other_resource_id'], + path: 'other_resources//fake_resources/.json', + }, + { + http_method: 'get', + operation: 'custom', + ids: ['id', 'other_resource_id'], + path: 'other_resources//fake_resources//custom.json', + }, + { + http_method: 'delete', + operation: 'delete', + ids: ['id', 'other_resource_id'], + path: 'other_resources//fake_resources/.json', + }, + ]; + + public static async find({ + session, + params, + id, + other_resource_id = null, + ...otherArgs + }: FakeResourceFindArgs): Promise { + const result = await this.baseFind({ + session, + urlIds: {id, other_resource_id}, + params: {...params, ...otherArgs}, + }); + return result ? result[0] : null; + } + + public static async all({ + session, + params, + }: FakeResourceAllArgs): Promise { + return this.baseFind({ + session, + params, + urlIds: {}, + }); + } + + public static async custom({ + session, + id, + other_resource_id, + }: FakeResourceCustomArgs): Promise { + const response = await this.request({ + http_method: 'get', + operation: 'custom', + session, + urlIds: {id, other_resource_id}, + }); + + return response.body; + } + + id?: number | string | null; + attribute?: string | null; + has_one_attribute?: FakeResource | null; + has_many_attribute?: FakeResource[] | null; + other_resource_id?: number | null; +} diff --git a/lib/clients/rest/__tests__/resources/load-rest-resources.test.ts b/lib/clients/rest/__tests__/resources/load-rest-resources.test.ts new file mode 100644 index 000000000..9c028d133 --- /dev/null +++ b/lib/clients/rest/__tests__/resources/load-rest-resources.test.ts @@ -0,0 +1,33 @@ +import {testConfig} from '../../../../__tests__/test-helper'; +import {LogSeverity, ApiVersion, LATEST_API_VERSION} from '../../../../types'; +import {shopifyApi} from '../../../../'; + +import {restResources} from './test-resources'; + +describe('Load REST resources', () => { + it('sets up objects with a client', async () => { + const shopify = shopifyApi({ + ...testConfig, + restResources, + }); + + expect(shopify.rest).toHaveProperty('FakeResource'); + expect(shopify.rest.FakeResource.CLIENT).toBeDefined(); + }); + + it('warns if the API versions mismatch', async () => { + const shopify = shopifyApi({ + ...testConfig, + apiVersion: '2020-01' as any as ApiVersion, + restResources, + }); + + expect(shopify.config.logger.log).toHaveBeenCalledWith( + LogSeverity.Warning, + `Current API version '2020-01' does not match resource API version '${LATEST_API_VERSION}'`, + ); + + expect(shopify.rest).toHaveProperty('FakeResource'); + expect(shopify.rest.FakeResource.CLIENT).toBeDefined(); + }); +}); diff --git a/lib/clients/rest/__tests__/resources/test-resources.ts b/lib/clients/rest/__tests__/resources/test-resources.ts new file mode 100644 index 000000000..1ef949dd0 --- /dev/null +++ b/lib/clients/rest/__tests__/resources/test-resources.ts @@ -0,0 +1,14 @@ +import {ShopifyRestResources} from '../../../../../rest/types'; + +import {FakeResource} from './fake-resource'; +import {FakeResourceWithCustomPrefix} from './fake-resource-with-custom-prefix'; + +interface TestRestResources extends ShopifyRestResources { + FakeResource: typeof FakeResource; + FakeResourceWithCustomPrefix: typeof FakeResourceWithCustomPrefix; +} + +export const restResources: TestRestResources = { + FakeResource, + FakeResourceWithCustomPrefix, +}; diff --git a/rest/__tests__/load-rest-resources.test.ts b/rest/__tests__/load-rest-resources.test.ts deleted file mode 100644 index 2b658a234..000000000 --- a/rest/__tests__/load-rest-resources.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {RestResourceError} from '../../error'; -import {testConfig} from '../../__tests__/test-helper'; -import {ApiVersion, LATEST_API_VERSION} from '../../base-types'; -import {shopifyApi} from '../../lib'; - -import {restResources} from './test-resources'; - -describe('Load REST resources', () => { - it('throws an error if the API versions mismatch', async () => { - expect(() => - shopifyApi({ - ...testConfig, - apiVersion: 'wrong version' as any as ApiVersion, - restResources, - }), - ).toThrowError( - new RestResourceError( - `Current API version 'wrong version' does not match resource API version '${LATEST_API_VERSION}'`, - ), - ); - }); - - it('sets up objects with a client', async () => { - const shopify = shopifyApi({ - ...testConfig, - restResources, - }); - - expect(shopify.rest).toHaveProperty('FakeResource'); - expect(shopify.rest.FakeResource.CLIENT).toBeDefined(); - }); -}); From 96d68022edfeb52dd0fc5d9f0af7532ea22fea92 Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Wed, 4 Jan 2023 15:37:02 -0500 Subject: [PATCH 2/4] Allow overriding the API version in clients --- .../graphql/__tests__/graphql_client.test.ts | 27 ++++- .../__tests__/storefront_client.test.ts | 31 +++++- lib/clients/graphql/graphql_client.ts | 13 ++- lib/clients/graphql/storefront_client.ts | 13 ++- lib/clients/graphql/types.ts | 3 + .../rest/__tests__/resources/base.test.ts | 105 ++++++++++++++++-- .../resources/load-rest-resources.test.ts | 2 +- .../rest/__tests__/rest_client.test.ts | 26 ++++- lib/clients/rest/rest_client.ts | 13 ++- lib/clients/rest/types.ts | 2 + lib/index.ts | 2 +- rest/base.ts | 6 +- rest/load-rest-resources.ts | 15 +-- 13 files changed, 227 insertions(+), 31 deletions(-) diff --git a/lib/clients/graphql/__tests__/graphql_client.test.ts b/lib/clients/graphql/__tests__/graphql_client.test.ts index ef987ff50..67e565102 100644 --- a/lib/clients/graphql/__tests__/graphql_client.test.ts +++ b/lib/clients/graphql/__tests__/graphql_client.test.ts @@ -1,5 +1,5 @@ import * as ShopifyErrors from '../../../error'; -import {ShopifyHeader} from '../../../types'; +import {ApiVersion, LogSeverity, ShopifyHeader} from '../../../types'; import {queueMockResponse, shopify} from '../../../__tests__/test-helper'; import {Session} from '../../../session/session'; import {JwtPayload} from '../../../session/types'; @@ -238,6 +238,31 @@ describe('GraphQL client', () => { data: JSON.stringify(query), }).toMatchMadeHttpRequest(); }); + + it('allows overriding the API version', async () => { + expect(shopify.config.apiVersion).not.toBe('2020-01'); + const client = new shopify.clients.Graphql({ + session, + apiVersion: '2020-01' as any as ApiVersion, + }); + + queueMockResponse(JSON.stringify(successResponse)); + + const response = await client.query({data: QUERY}); + + expect(response).toEqual(buildExpectedResponse(successResponse)); + expect({ + method: 'POST', + domain, + path: `/admin/api/2020-01/graphql.json`, + data: QUERY, + }).toMatchMadeHttpRequest(); + + expect(shopify.config.logger.log).toHaveBeenCalledWith( + LogSeverity.Debug, + 'GraphQL client overriding API version to 2020-01', + ); + }); }); function buildExpectedResponse(obj: unknown) { diff --git a/lib/clients/graphql/__tests__/storefront_client.test.ts b/lib/clients/graphql/__tests__/storefront_client.test.ts index 79b04de08..197330b4a 100644 --- a/lib/clients/graphql/__tests__/storefront_client.test.ts +++ b/lib/clients/graphql/__tests__/storefront_client.test.ts @@ -1,5 +1,5 @@ import {shopify, queueMockResponse} from '../../../__tests__/test-helper'; -import {ShopifyHeader} from '../../../types'; +import {ApiVersion, LogSeverity, ShopifyHeader} from '../../../types'; import {Session} from '../../../session/session'; import {JwtPayload} from '../../../session/types'; import {SHOPIFY_API_LIBRARY_VERSION} from '../../../version'; @@ -120,6 +120,35 @@ describe('Storefront GraphQL client', () => { }, }).toMatchMadeHttpRequest(); }); + + it('allows overriding the API version', async () => { + const client = new shopify.clients.Storefront({ + domain: session.shop, + storefrontAccessToken, + apiVersion: '2020-01' as any as ApiVersion, + }); + + queueMockResponse(JSON.stringify(successResponse)); + + await expect(client.query({data: QUERY})).resolves.toEqual( + buildExpectedResponse(successResponse), + ); + + expect({ + method: 'POST', + domain, + path: `/api/2020-01/graphql.json`, + data: QUERY, + headers: { + [ShopifyHeader.StorefrontAccessToken]: storefrontAccessToken, + }, + }).toMatchMadeHttpRequest(); + + expect(shopify.config.logger.log).toHaveBeenCalledWith( + LogSeverity.Debug, + 'Storefront client overriding API version to 2020-01', + ); + }); }); function buildExpectedResponse(obj: unknown) { diff --git a/lib/clients/graphql/graphql_client.ts b/lib/clients/graphql/graphql_client.ts index b96107979..1f1f3b046 100644 --- a/lib/clients/graphql/graphql_client.ts +++ b/lib/clients/graphql/graphql_client.ts @@ -1,4 +1,4 @@ -import {ShopifyHeader} from '../../types'; +import {ApiVersion, LogSeverity, ShopifyHeader} from '../../types'; import {ConfigInterface} from '../../base-types'; import {httpClientClass, HttpClient} from '../http_client/http_client'; import {DataType, HeaderParams, RequestReturn} from '../http_client/types'; @@ -19,6 +19,7 @@ export class GraphqlClient { baseApiPath = '/admin/api'; readonly session: Session; readonly client: HttpClient; + readonly apiVersion?: ApiVersion; constructor(params: GraphqlClientParams) { if ( @@ -30,7 +31,15 @@ export class GraphqlClient { ); } + if (params.apiVersion) { + this.graphqlClass().CONFIG.logger.log( + LogSeverity.Debug, + `GraphQL client overriding API version to ${params.apiVersion}`, + ); + } + this.session = params.session; + this.apiVersion = params.apiVersion; this.client = new (this.graphqlClass().HTTP_CLIENT)({ domain: this.session.shop, }); @@ -50,7 +59,7 @@ export class GraphqlClient { params.extraHeaders = {...apiHeaders, ...params.extraHeaders}; const path = `${this.baseApiPath}/${ - this.graphqlClass().CONFIG.apiVersion + this.apiVersion || this.graphqlClass().CONFIG.apiVersion }/graphql.json`; let dataType: DataType.GraphQL | DataType.JSON; diff --git a/lib/clients/graphql/storefront_client.ts b/lib/clients/graphql/storefront_client.ts index cc9b9b656..db3048042 100644 --- a/lib/clients/graphql/storefront_client.ts +++ b/lib/clients/graphql/storefront_client.ts @@ -1,5 +1,5 @@ import {SHOPIFY_API_LIBRARY_VERSION} from '../../version'; -import {LIBRARY_NAME, ShopifyHeader} from '../../types'; +import {LIBRARY_NAME, LogSeverity, ShopifyHeader} from '../../types'; import {httpClientClass} from '../http_client/http_client'; import {Session} from '../../session/session'; import {HeaderParams} from '../http_client/types'; @@ -20,7 +20,16 @@ export class StorefrontClient extends GraphqlClient { isOnline: true, accessToken: params.storefrontAccessToken, }); - super({session}); + + super({session, apiVersion: params.apiVersion}); + + if (params.apiVersion) { + this.storefrontClass().CONFIG.logger.log( + LogSeverity.Debug, + `Storefront client overriding API version to ${params.apiVersion}`, + ); + } + this.domain = params.domain; this.storefrontAccessToken = params.storefrontAccessToken; } diff --git a/lib/clients/graphql/types.ts b/lib/clients/graphql/types.ts index 1a8ff13a7..3ebe87003 100644 --- a/lib/clients/graphql/types.ts +++ b/lib/clients/graphql/types.ts @@ -1,3 +1,4 @@ +import {ApiVersion} from '../../types'; import {Session} from '../../session/session'; import {PostRequestParams} from '../http_client/types'; @@ -5,11 +6,13 @@ export type GraphqlParams = Omit; export interface GraphqlClientParams { session: Session; + apiVersion?: ApiVersion; } export interface StorefrontClientParams { domain: string; storefrontAccessToken: string; + apiVersion?: ApiVersion; } export interface GraphqlProxyParams { diff --git a/lib/clients/rest/__tests__/resources/base.test.ts b/lib/clients/rest/__tests__/resources/base.test.ts index 4cd752f86..934c2abbb 100644 --- a/lib/clients/rest/__tests__/resources/base.test.ts +++ b/lib/clients/rest/__tests__/resources/base.test.ts @@ -1,27 +1,28 @@ -import {Session} from '../../../../session/session'; -import {HttpResponseError} from '../../../../error'; import { testConfig, queueMockResponse, queueMockResponses, } from '../../../../__tests__/test-helper'; +import {Session} from '../../../../session/session'; +import {HttpResponseError} from '../../../../error'; +import {ApiVersion, LATEST_API_VERSION} from '../../../../types'; import {shopifyApi, Shopify} from '../../../../index'; import {restResources} from './test-resources'; -let prefix: string; -let shopify: Shopify; +describe('Base REST resource', () => { + let prefix: string; + let shopify: Shopify; -beforeEach(() => { - shopify = shopifyApi({ - ...testConfig, - restResources, - }); + beforeEach(() => { + shopify = shopifyApi({ + ...testConfig, + restResources, + }); - prefix = `/admin/api/${shopify.config.apiVersion}`; -}); + prefix = `/admin/api/${shopify.config.apiVersion}`; + }); -describe('Base REST resource', () => { const domain = 'test-shop.myshopify.io'; const headers = {'X-Shopify-Access-Token': 'access-token'}; const session = new Session({ @@ -492,3 +493,83 @@ describe('Base REST resource', () => { expect(hash).toHaveProperty('attribute', 'attribute'); }); }); + +describe('REST resources with a different API version', () => { + const domain = 'test-shop.myshopify.io'; + const headers = {'X-Shopify-Access-Token': 'access-token'}; + const session = new Session({ + id: '1234', + shop: domain, + state: '1234', + isOnline: true, + }); + session.accessToken = 'access-token'; + + it('can load class an run requests', async () => { + const shopify = shopifyApi({ + ...testConfig, + apiVersion: '2020-01' as any as ApiVersion, + restResources, + }); + + // The shopify object is set to an older version, but the resources use the latest + expect(shopify.rest.FakeResource.API_VERSION).toBe(LATEST_API_VERSION); + expect(shopify.config.apiVersion).not.toBe(LATEST_API_VERSION); + + queueMockResponses( + [JSON.stringify({fake_resource: {attribute: 'attribute'}})], + [JSON.stringify({fake_resource: {id: 1, attribute: 'attribute'}})], + [JSON.stringify({fake_resource: {id: 1, attribute: 'attribute'}})], + [JSON.stringify({})], + ); + + // POST + const fakeResource = new shopify.rest.FakeResource({session}); + fakeResource.attribute = 'attribute'; + await fakeResource.save(); + expect(fakeResource!.attribute).toEqual('attribute'); + expect({ + method: 'POST', + domain, + path: `/admin/api/${LATEST_API_VERSION}/fake_resources.json`, + headers, + data: {fake_resource: {attribute: 'attribute'}}, + }).toMatchMadeHttpRequest(); + + // GET + const fakeResource2 = (await shopify.rest.FakeResource.find({ + id: 1, + session, + }))!; + expect(fakeResource).not.toEqual(fakeResource2); + expect(fakeResource2.id).toEqual(1); + expect(fakeResource2.attribute).toEqual('attribute'); + expect({ + method: 'GET', + domain, + path: `/admin/api/${LATEST_API_VERSION}/fake_resources/1.json`, + headers, + }).toMatchMadeHttpRequest(); + + // PUT + fakeResource2.attribute = 'attribute2'; + await fakeResource2.save(); + expect(fakeResource!.attribute).toEqual('attribute'); + expect({ + method: 'PUT', + domain, + path: `/admin/api/${LATEST_API_VERSION}/fake_resources/1.json`, + headers, + data: {fake_resource: {attribute: 'attribute2'}}, + }).toMatchMadeHttpRequest(); + + // DELETE + await fakeResource2.delete(); + expect({ + method: 'DELETE', + domain, + path: `/admin/api/${LATEST_API_VERSION}/fake_resources/1.json`, + headers, + }).toMatchMadeHttpRequest(); + }); +}); diff --git a/lib/clients/rest/__tests__/resources/load-rest-resources.test.ts b/lib/clients/rest/__tests__/resources/load-rest-resources.test.ts index 9c028d133..75b7011b3 100644 --- a/lib/clients/rest/__tests__/resources/load-rest-resources.test.ts +++ b/lib/clients/rest/__tests__/resources/load-rest-resources.test.ts @@ -1,6 +1,6 @@ import {testConfig} from '../../../../__tests__/test-helper'; import {LogSeverity, ApiVersion, LATEST_API_VERSION} from '../../../../types'; -import {shopifyApi} from '../../../../'; +import {shopifyApi} from '../../../..'; import {restResources} from './test-resources'; diff --git a/lib/clients/rest/__tests__/rest_client.test.ts b/lib/clients/rest/__tests__/rest_client.test.ts index b217918d5..62c4be2b6 100644 --- a/lib/clients/rest/__tests__/rest_client.test.ts +++ b/lib/clients/rest/__tests__/rest_client.test.ts @@ -6,7 +6,7 @@ import { import {DataType, GetRequestParams} from '../../http_client/types'; import {RestRequestReturn, PageInfo} from '../types'; import * as ShopifyErrors from '../../../error'; -import {ShopifyHeader} from '../../../types'; +import {ApiVersion, LogSeverity, ShopifyHeader} from '../../../types'; import {Session} from '../../../session/session'; import {JwtPayload} from '../../../session/types'; @@ -407,6 +407,30 @@ describe('REST client', () => { path: '/admin/some-path.json', }).toMatchMadeHttpRequest(); }); + + it('allows overriding the API version', async () => { + expect(shopify.config.apiVersion).not.toBe('2020-01'); + const client = new shopify.clients.Rest({ + session, + apiVersion: '2020-01' as any as ApiVersion, + }); + + queueMockResponse(JSON.stringify(successResponse)); + + await expect(client.get({path: 'products'})).resolves.toEqual( + buildExpectedResponse(successResponse), + ); + expect({ + method: 'GET', + domain, + path: `/admin/api/2020-01/products.json`, + }).toMatchMadeHttpRequest(); + + expect(shopify.config.logger.log).toHaveBeenCalledWith( + LogSeverity.Debug, + 'REST client overriding API version to 2020-01', + ); + }); }); function getDefaultPageInfo(): PageInfo { diff --git a/lib/clients/rest/rest_client.ts b/lib/clients/rest/rest_client.ts index d114759bf..45637fb18 100644 --- a/lib/clients/rest/rest_client.ts +++ b/lib/clients/rest/rest_client.ts @@ -1,5 +1,5 @@ import {getHeader} from '../../../runtime/http'; -import {ShopifyHeader} from '../../types'; +import {ApiVersion, LogSeverity, ShopifyHeader} from '../../types'; import {ConfigInterface} from '../../base-types'; import {RequestParams, GetRequestParams} from '../http_client/types'; import * as ShopifyErrors from '../../error'; @@ -19,6 +19,7 @@ export class RestClient extends HttpClient { public static CONFIG: ConfigInterface; readonly session: Session; + readonly apiVersion?: ApiVersion; public constructor(params: RestClientParams) { super({domain: params.session.shop}); @@ -29,7 +30,15 @@ export class RestClient extends HttpClient { ); } + if (params.apiVersion) { + this.restClass().CONFIG.logger.log( + LogSeverity.Debug, + `REST client overriding API version to ${params.apiVersion}`, + ); + } + this.session = params.session; + this.apiVersion = params.apiVersion; } protected async request( @@ -97,7 +106,7 @@ export class RestClient extends HttpClient { return `${cleanPath.replace(/\.json$/, '')}.json`; } else { return `/admin/api/${ - this.restClass().CONFIG.apiVersion + this.apiVersion || this.restClass().CONFIG.apiVersion }${cleanPath.replace(/\.json$/, '')}.json`; } } diff --git a/lib/clients/rest/types.ts b/lib/clients/rest/types.ts index fde632867..20eaea0be 100644 --- a/lib/clients/rest/types.ts +++ b/lib/clients/rest/types.ts @@ -1,3 +1,4 @@ +import {ApiVersion} from '../../types'; import {Session} from '../../session/session'; import {RequestReturn, GetRequestParams} from '../http_client/types'; @@ -16,4 +17,5 @@ export type RestRequestReturn = RequestReturn & { export interface RestClientParams { session: Session; + apiVersion?: ApiVersion; } diff --git a/lib/index.ts b/lib/index.ts index 28d9950de..410d55f36 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -64,7 +64,7 @@ export function shopifyApi( if (restResources) { shopify.rest = loadRestResources({ resources: restResources, - apiVersion: config.apiVersion, + config: validatedConfig, RestClient: shopify.clients.Rest, }) as T; } diff --git a/rest/base.ts b/rest/base.ts index f5e9f7c9b..1ba12b52b 100644 --- a/rest/base.ts +++ b/rest/base.ts @@ -3,6 +3,7 @@ import {Session} from '../lib/session/session'; import {RestRequestReturn} from '../lib/clients/rest/types'; import {DataType, GetRequestParams} from '../lib/clients/http_client/types'; import {RestClient} from '../lib/clients/rest/rest_client'; +import {ApiVersion} from '../lib/types'; import {IdSet, Body, ResourcePath, ParamSet} from './types'; @@ -80,7 +81,10 @@ export class Base { body, entity, }: RequestArgs): Promise> { - const client = new this.CLIENT({session}); + const client = new this.CLIENT({ + session, + apiVersion: this.API_VERSION as ApiVersion, + }); const path = this.getPath({http_method, operation, urlIds, entity}); diff --git a/rest/load-rest-resources.ts b/rest/load-rest-resources.ts index 07286ece9..2e6b1fb9b 100644 --- a/rest/load-rest-resources.ts +++ b/rest/load-rest-resources.ts @@ -1,24 +1,25 @@ -import {ApiVersion} from '../lib/types'; -import {RestResourceError} from '../lib/error'; +import {LogSeverity} from '../lib/types'; +import {ConfigInterface} from '../lib/base-types'; import {RestClient} from '../lib/clients/rest/rest_client'; import {ShopifyRestResources} from './types'; export interface LoadRestResourcesParams { resources: ShopifyRestResources; - apiVersion: ApiVersion; + config: ConfigInterface; RestClient: typeof RestClient; } export function loadRestResources({ resources, - apiVersion, + config, RestClient, }: LoadRestResourcesParams): ShopifyRestResources { const firstResource = Object.keys(resources)[0]; - if (apiVersion !== resources[firstResource].API_VERSION) { - throw new RestResourceError( - `Current API version '${apiVersion}' does not match ` + + if (config.apiVersion !== resources[firstResource].API_VERSION) { + config.logger.log( + LogSeverity.Warning, + `Current API version '${config.apiVersion}' does not match ` + `resource API version '${resources[firstResource].API_VERSION}'`, ); } From 0550b39e25ca8c80c51a6bcf14acb09bc14b3fe8 Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Wed, 4 Jan 2023 15:52:50 -0500 Subject: [PATCH 3/4] Update documentation --- CHANGELOG.md | 1 + docs/guides/rest-resources.md | 11 ++++++----- docs/reference/clients/Graphql.md | 12 +++++++++++- docs/reference/clients/Rest.md | 12 +++++++++++- docs/reference/clients/Storefront.md | 8 ++++++++ 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb398c7ec..72b86ca91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## Unreleased +- [Minor] Allow api version overrides [#660](https://github.com/Shopify/shopify-api-js/pull/660) - [Minor] Add support for 2023-01 API version [#659](https://github.com/Shopify/shopify-api-js/pull/659) - [Patch] Force `/` path on session cookie [#658](https://github.com/Shopify/shopify-api-js/pull/658) - [Patch] Don't ignore previous headers when beginning OAuth [#652](https://github.com/Shopify/shopify-api-js/pull/652) diff --git a/docs/guides/rest-resources.md b/docs/guides/rest-resources.md index e2b169695..7314d12c6 100644 --- a/docs/guides/rest-resources.md +++ b/docs/guides/rest-resources.md @@ -5,7 +5,8 @@ To call the Admin REST API, you can use the [REST client](../reference/clients/R The Admin API has a lot of endpoints, and the differences between them can be subtle. To make it easier to interact with the API, this library provides resource classes, which map these endpoints to OO code and can make API queries feel more natural. -> **Note**: we provide auto-generated resources for all **_stable_** API versions, so your app must include the appropriate set (see [mounting REST resources](#mounting-rest-resources) below), matching your `apiVersion` configuration. The library will throw an error if the versions don't match. +> **Note**: we provide auto-generated resources for all **_stable_** API versions. +> If your app is using `unstable` or a Release Candidate, you can still import REST resources (see [mounting REST resources](#mounting-rest-resources) below) for other versions, but we'll log a warning to remind you to update when you're ready. Below is an example of how REST resources can make it easier to fetch the first product and update it: @@ -35,7 +36,7 @@ const client = new shopify.clients.Rest({session}); // The following line sends a HTTP GET request to this constructed URL: // https://${session.shop}/admin/api/${shopify.config.api_version}/products/7504536535062.json const response = await client.get({ - path: 'products/7504536535062' + path: 'products/7504536535062', }); // Apps needs to dig into the response body to find the object @@ -65,11 +66,11 @@ const session = await getSessionFromStorage(sessionId); // get a single product via its product id const product = await shopify.rest.Product.find({session, id: '7504536535062'}); - + product.title = 'A new title'; - + await product.save({ - update: true + update: true, }); ``` diff --git a/docs/reference/clients/Graphql.md b/docs/reference/clients/Graphql.md index 68600b2d5..a1a30ae12 100644 --- a/docs/reference/clients/Graphql.md +++ b/docs/reference/clients/Graphql.md @@ -21,7 +21,10 @@ app.get('/my-endpoint', async () => { // getSessionFromStorage() must be provided by application const session = await getSessionFromStorage(sessionId); - const client = new shopify.clients.Graphql({session}); + const client = new shopify.clients.Graphql({ + session, + apiVersion: ApiVersion.January23, + }); }); ``` @@ -35,6 +38,13 @@ Receives an object containing: The Shopify Session containing an access token to the API. +#### apiVersion + +`ApiVersion` + +This will override the default API version. +Any requests made by this client will reach this version instead. + ## Query Sends a request to the Admin API. diff --git a/docs/reference/clients/Rest.md b/docs/reference/clients/Rest.md index dd461eef7..28ff49ea8 100644 --- a/docs/reference/clients/Rest.md +++ b/docs/reference/clients/Rest.md @@ -19,7 +19,10 @@ app.get('/my-endpoint', async (req, res) => { // getSessionFromStorage() must be provided by application const session = await getSessionFromStorage(sessionId); - const client = new shopify.clients.Rest({session}); + const client = new shopify.clients.Rest({ + session, + apiVersion: ApiVersion.January23, + }); }); ``` @@ -33,6 +36,13 @@ Receives an object containing: The Shopify Session containing an access token to the API. +#### apiVersion + +`ApiVersion` + +This will override the default API version. +Any requests made by this client will reach this version instead. + ## Get Sends a GET request to the Admin API. diff --git a/docs/reference/clients/Storefront.md b/docs/reference/clients/Storefront.md index 1412882f8..88624c018 100644 --- a/docs/reference/clients/Storefront.md +++ b/docs/reference/clients/Storefront.md @@ -50,6 +50,7 @@ app.get('/my-endpoint', async (req, res) => { const storefrontClient = new shopify.clients.Storefront({ domain: session.shop, storefrontAccessToken, + apiVersion: ApiVersion.January23, }); }); ``` @@ -70,6 +71,13 @@ The shop domain for the request. The access token created using one of the Admin APIs. +#### apiVersion + +`ApiVersion` + +This will override the default API version. +Any requests made by this client will reach this version instead. + ## Query Sends a request to the Storefront API. From ea292fbb1fdb4a20213ff9ac54a7144562f71a62 Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Wed, 4 Jan 2023 16:41:12 -0500 Subject: [PATCH 4/4] Applying comments from code review --- .../graphql/__tests__/graphql_client.test.ts | 11 ++++-- .../__tests__/storefront_client.test.ts | 11 ++++-- lib/clients/graphql/graphql_client.ts | 36 ++++++++++--------- lib/clients/graphql/storefront_client.ts | 23 +++++++----- lib/clients/http_client/http_client.ts | 32 ++++++++--------- .../rest/__tests__/rest_client.test.ts | 11 ++++-- lib/clients/rest/rest_client.ts | 29 ++++++++------- 7 files changed, 93 insertions(+), 60 deletions(-) diff --git a/lib/clients/graphql/__tests__/graphql_client.test.ts b/lib/clients/graphql/__tests__/graphql_client.test.ts index 67e565102..8b6fbb810 100644 --- a/lib/clients/graphql/__tests__/graphql_client.test.ts +++ b/lib/clients/graphql/__tests__/graphql_client.test.ts @@ -1,5 +1,10 @@ import * as ShopifyErrors from '../../../error'; -import {ApiVersion, LogSeverity, ShopifyHeader} from '../../../types'; +import { + ApiVersion, + LATEST_API_VERSION, + LogSeverity, + ShopifyHeader, +} from '../../../types'; import {queueMockResponse, shopify} from '../../../__tests__/test-helper'; import {Session} from '../../../session/session'; import {JwtPayload} from '../../../session/types'; @@ -260,7 +265,9 @@ describe('GraphQL client', () => { expect(shopify.config.logger.log).toHaveBeenCalledWith( LogSeverity.Debug, - 'GraphQL client overriding API version to 2020-01', + expect.stringContaining( + `GraphQL client overriding default API version ${LATEST_API_VERSION} with 2020-01`, + ), ); }); }); diff --git a/lib/clients/graphql/__tests__/storefront_client.test.ts b/lib/clients/graphql/__tests__/storefront_client.test.ts index 197330b4a..3fceec1e7 100644 --- a/lib/clients/graphql/__tests__/storefront_client.test.ts +++ b/lib/clients/graphql/__tests__/storefront_client.test.ts @@ -1,5 +1,10 @@ import {shopify, queueMockResponse} from '../../../__tests__/test-helper'; -import {ApiVersion, LogSeverity, ShopifyHeader} from '../../../types'; +import { + ApiVersion, + LATEST_API_VERSION, + LogSeverity, + ShopifyHeader, +} from '../../../types'; import {Session} from '../../../session/session'; import {JwtPayload} from '../../../session/types'; import {SHOPIFY_API_LIBRARY_VERSION} from '../../../version'; @@ -146,7 +151,9 @@ describe('Storefront GraphQL client', () => { expect(shopify.config.logger.log).toHaveBeenCalledWith( LogSeverity.Debug, - 'Storefront client overriding API version to 2020-01', + expect.stringContaining( + `Storefront client overriding default API version ${LATEST_API_VERSION} with 2020-01`, + ), ); }); }); diff --git a/lib/clients/graphql/graphql_client.ts b/lib/clients/graphql/graphql_client.ts index 1f1f3b046..b722d6a45 100644 --- a/lib/clients/graphql/graphql_client.ts +++ b/lib/clients/graphql/graphql_client.ts @@ -1,8 +1,9 @@ -import {ApiVersion, LogSeverity, ShopifyHeader} from '../../types'; +import {ApiVersion, ShopifyHeader} from '../../types'; import {ConfigInterface} from '../../base-types'; import {httpClientClass, HttpClient} from '../http_client/http_client'; import {DataType, HeaderParams, RequestReturn} from '../http_client/types'; import {Session} from '../../session/session'; +import {logger} from '../../logger'; import * as ShopifyErrors from '../../error'; import {GraphqlParams, GraphqlClientParams} from './types'; @@ -13,8 +14,8 @@ export interface GraphqlClientClassParams { } export class GraphqlClient { - public static CONFIG: ConfigInterface; - public static HTTP_CLIENT: typeof HttpClient; + public static config: ConfigInterface; + public static HttpClient: typeof HttpClient; baseApiPath = '/admin/api'; readonly session: Session; @@ -22,25 +23,26 @@ export class GraphqlClient { readonly apiVersion?: ApiVersion; constructor(params: GraphqlClientParams) { - if ( - !this.graphqlClass().CONFIG.isPrivateApp && - !params.session.accessToken - ) { + const config = this.graphqlClass().config; + + if (!config.isPrivateApp && !params.session.accessToken) { throw new ShopifyErrors.MissingRequiredArgument( 'Missing access token when creating GraphQL client', ); } if (params.apiVersion) { - this.graphqlClass().CONFIG.logger.log( - LogSeverity.Debug, - `GraphQL client overriding API version to ${params.apiVersion}`, - ); + const message = + params.apiVersion === config.apiVersion + ? `GraphQL client has a redundant API version override to the default ${params.apiVersion}` + : `GraphQL client overriding default API version ${config.apiVersion} with ${params.apiVersion}`; + + logger(config).debug(message); } this.session = params.session; this.apiVersion = params.apiVersion; - this.client = new (this.graphqlClass().HTTP_CLIENT)({ + this.client = new (this.graphqlClass().HttpClient)({ domain: this.session.shop, }); } @@ -59,7 +61,7 @@ export class GraphqlClient { params.extraHeaders = {...apiHeaders, ...params.extraHeaders}; const path = `${this.baseApiPath}/${ - this.apiVersion || this.graphqlClass().CONFIG.apiVersion + this.apiVersion || this.graphqlClass().config.apiVersion }/graphql.json`; let dataType: DataType.GraphQL | DataType.JSON; @@ -87,8 +89,8 @@ export class GraphqlClient { protected getApiHeaders(): HeaderParams { return { - [ShopifyHeader.AccessToken]: this.graphqlClass().CONFIG.isPrivateApp - ? this.graphqlClass().CONFIG.apiSecretKey + [ShopifyHeader.AccessToken]: this.graphqlClass().config.isPrivateApp + ? this.graphqlClass().config.apiSecretKey : (this.session.accessToken as string), }; } @@ -108,8 +110,8 @@ export function graphqlClientClass( } class NewGraphqlClient extends GraphqlClient { - public static CONFIG = config; - public static HTTP_CLIENT = HttpClient!; + public static config = config; + public static HttpClient = HttpClient!; } Reflect.defineProperty(NewGraphqlClient, 'name', { diff --git a/lib/clients/graphql/storefront_client.ts b/lib/clients/graphql/storefront_client.ts index db3048042..4fd79463f 100644 --- a/lib/clients/graphql/storefront_client.ts +++ b/lib/clients/graphql/storefront_client.ts @@ -1,8 +1,9 @@ import {SHOPIFY_API_LIBRARY_VERSION} from '../../version'; -import {LIBRARY_NAME, LogSeverity, ShopifyHeader} from '../../types'; +import {LIBRARY_NAME, ShopifyHeader} from '../../types'; import {httpClientClass} from '../http_client/http_client'; import {Session} from '../../session/session'; import {HeaderParams} from '../http_client/types'; +import {logger} from '../../logger'; import {GraphqlClient, GraphqlClientClassParams} from './graphql_client'; import {StorefrontClientParams} from './types'; @@ -23,11 +24,15 @@ export class StorefrontClient extends GraphqlClient { super({session, apiVersion: params.apiVersion}); + const config = this.storefrontClass().config; + if (params.apiVersion) { - this.storefrontClass().CONFIG.logger.log( - LogSeverity.Debug, - `Storefront client overriding API version to ${params.apiVersion}`, - ); + const message = + params.apiVersion === config.apiVersion + ? `Storefront client has a redundant API version override to the default ${params.apiVersion}` + : `Storefront client overriding default API version ${config.apiVersion} with ${params.apiVersion}`; + + logger(config).debug(message); } this.domain = params.domain; @@ -38,9 +43,9 @@ export class StorefrontClient extends GraphqlClient { const sdkVariant = LIBRARY_NAME.toLowerCase().split(' ').join('-'); return { - [ShopifyHeader.StorefrontAccessToken]: this.storefrontClass().CONFIG + [ShopifyHeader.StorefrontAccessToken]: this.storefrontClass().config .isPrivateApp - ? this.storefrontClass().CONFIG.privateAppStorefrontAccessToken || + ? this.storefrontClass().config.privateAppStorefrontAccessToken || this.storefrontAccessToken : this.storefrontAccessToken, [ShopifyHeader.StorefrontSDKVariant]: sdkVariant, @@ -60,8 +65,8 @@ export function storefrontClientClass(params: GraphqlClientClassParams) { HttpClient = httpClientClass(config); } class NewStorefrontClient extends StorefrontClient { - public static CONFIG = config; - public static HTTP_CLIENT = HttpClient!; + public static config = config; + public static HttpClient = HttpClient!; } Reflect.defineProperty(NewStorefrontClient, 'name', { diff --git a/lib/clients/http_client/http_client.ts b/lib/clients/http_client/http_client.ts index 6e0ba7a9d..cce5783ad 100644 --- a/lib/clients/http_client/http_client.ts +++ b/lib/clients/http_client/http_client.ts @@ -39,14 +39,14 @@ interface HttpClientParams { } export class HttpClient { - public static CONFIG: ConfigInterface; - public static SCHEME: string; + public static config: ConfigInterface; + public static scheme: string; // 1 second static readonly RETRY_WAIT_TIME = 1000; // 5 minutes static readonly DEPRECATION_ALERT_DELAY = 300000; - LOGGED_DEPRECATIONS: {[key: string]: number} = {}; + loggedDeprecations: {[key: string]: number} = {}; readonly domain: string; public constructor(params: HttpClientParams) { @@ -93,8 +93,8 @@ export class HttpClient { let userAgent = `${LIBRARY_NAME} v${SHOPIFY_API_LIBRARY_VERSION} | ${abstractRuntimeString()}`; - if (this.httpClass().CONFIG.userAgentPrefix) { - userAgent = `${this.httpClass().CONFIG.userAgentPrefix} | ${userAgent}`; + if (this.httpClass().config.userAgentPrefix) { + userAgent = `${this.httpClass().config.userAgentPrefix} | ${userAgent}`; } if (params.extraHeaders) { @@ -139,7 +139,7 @@ export class HttpClient { } } - const url = `${this.httpClass().SCHEME}://${ + const url = `${this.httpClass().scheme}://${ this.domain }${this.getRequestPath(params.path)}${ProcessedQuery.stringify( params.query, @@ -151,7 +151,7 @@ export class HttpClient { body, }; - if (this.httpClass().CONFIG.logger.httpRequests) { + if (this.httpClass().config.logger.httpRequests) { const message = [ 'Making HTTP request', `${request.method} ${request.url}`, @@ -162,7 +162,7 @@ export class HttpClient { message.push(`Body: ${JSON.stringify(body).replace(/\n/g, '\\n ')}`); } - logger(this.httpClass().CONFIG).debug(message.join(' - ')); + logger(this.httpClass().config).debug(message.join(' - ')); } async function sleep(waitTime: number): Promise { @@ -270,11 +270,11 @@ export class HttpClient { private async doRequest( request: NormalizedRequest, ): Promise> { - const log = logger(this.httpClass().CONFIG); + const log = logger(this.httpClass().config); const response: NormalizedResponse = await abstractFetch(request); - if (this.httpClass().CONFIG.logger.httpRequests) { + if (this.httpClass().config.logger.httpRequests) { log.debug( `Completed HTTP request, received ${response.statusCode} ${response.statusText}`, ); @@ -310,17 +310,17 @@ export class HttpClient { } const depHash = await createSHA256HMAC( - this.httpClass().CONFIG.apiSecretKey, + this.httpClass().config.apiSecretKey, JSON.stringify(deprecation), HashFormat.Hex, ); if ( - !Object.keys(this.LOGGED_DEPRECATIONS).includes(depHash) || - Date.now() - this.LOGGED_DEPRECATIONS[depHash] >= + !Object.keys(this.loggedDeprecations).includes(depHash) || + Date.now() - this.loggedDeprecations[depHash] >= HttpClient.DEPRECATION_ALERT_DELAY ) { - this.LOGGED_DEPRECATIONS[depHash] = Date.now(); + this.loggedDeprecations[depHash] = Date.now(); const stack = new Error().stack; const message = `API Deprecation Notice ${new Date().toLocaleString()} : ${JSON.stringify( @@ -342,8 +342,8 @@ export function httpClientClass( scheme = 'https', ): typeof HttpClient { class NewHttpClient extends HttpClient { - public static CONFIG = config; - public static SCHEME = scheme; + public static config = config; + public static scheme = scheme; } Reflect.defineProperty(NewHttpClient, 'name', { diff --git a/lib/clients/rest/__tests__/rest_client.test.ts b/lib/clients/rest/__tests__/rest_client.test.ts index 62c4be2b6..454aba9c5 100644 --- a/lib/clients/rest/__tests__/rest_client.test.ts +++ b/lib/clients/rest/__tests__/rest_client.test.ts @@ -6,7 +6,12 @@ import { import {DataType, GetRequestParams} from '../../http_client/types'; import {RestRequestReturn, PageInfo} from '../types'; import * as ShopifyErrors from '../../../error'; -import {ApiVersion, LogSeverity, ShopifyHeader} from '../../../types'; +import { + ApiVersion, + LATEST_API_VERSION, + LogSeverity, + ShopifyHeader, +} from '../../../types'; import {Session} from '../../../session/session'; import {JwtPayload} from '../../../session/types'; @@ -428,7 +433,9 @@ describe('REST client', () => { expect(shopify.config.logger.log).toHaveBeenCalledWith( LogSeverity.Debug, - 'REST client overriding API version to 2020-01', + expect.stringContaining( + `REST client overriding default API version ${LATEST_API_VERSION} with 2020-01`, + ), ); }); }); diff --git a/lib/clients/rest/rest_client.ts b/lib/clients/rest/rest_client.ts index 45637fb18..44d59519c 100644 --- a/lib/clients/rest/rest_client.ts +++ b/lib/clients/rest/rest_client.ts @@ -1,10 +1,11 @@ import {getHeader} from '../../../runtime/http'; -import {ApiVersion, LogSeverity, ShopifyHeader} from '../../types'; +import {ApiVersion, ShopifyHeader} from '../../types'; import {ConfigInterface} from '../../base-types'; import {RequestParams, GetRequestParams} from '../http_client/types'; import * as ShopifyErrors from '../../error'; import {HttpClient} from '../http_client/http_client'; import {Session} from '../../session/session'; +import {logger} from '../../logger'; import {RestRequestReturn, PageInfo, RestClientParams} from './types'; @@ -16,7 +17,7 @@ export class RestClient extends HttpClient { static LINK_HEADER_REGEXP = /<([^<]+)>; rel="([^"]+)"/; static DEFAULT_LIMIT = '50'; - public static CONFIG: ConfigInterface; + public static config: ConfigInterface; readonly session: Session; readonly apiVersion?: ApiVersion; @@ -24,17 +25,21 @@ export class RestClient extends HttpClient { public constructor(params: RestClientParams) { super({domain: params.session.shop}); - if (!this.restClass().CONFIG.isPrivateApp && !params.session.accessToken) { + const config = this.restClass().config; + + if (!config.isPrivateApp && !params.session.accessToken) { throw new ShopifyErrors.MissingRequiredArgument( 'Missing access token when creating REST client', ); } if (params.apiVersion) { - this.restClass().CONFIG.logger.log( - LogSeverity.Debug, - `REST client overriding API version to ${params.apiVersion}`, - ); + const message = + params.apiVersion === config.apiVersion + ? `REST client has a redundant API version override to the default ${params.apiVersion}` + : `REST client overriding default API version ${config.apiVersion} with ${params.apiVersion}`; + + logger(config).debug(message); } this.session = params.session; @@ -45,8 +50,8 @@ export class RestClient extends HttpClient { params: RequestParams, ): Promise> { params.extraHeaders = { - [ShopifyHeader.AccessToken]: this.restClass().CONFIG.isPrivateApp - ? this.restClass().CONFIG.apiSecretKey + [ShopifyHeader.AccessToken]: this.restClass().config.isPrivateApp + ? this.restClass().config.apiSecretKey : (this.session.accessToken as string), ...params.extraHeaders, }; @@ -106,7 +111,7 @@ export class RestClient extends HttpClient { return `${cleanPath.replace(/\.json$/, '')}.json`; } else { return `/admin/api/${ - this.apiVersion || this.restClass().CONFIG.apiVersion + this.apiVersion || this.restClass().config.apiVersion }${cleanPath.replace(/\.json$/, '')}.json`; } } @@ -133,8 +138,8 @@ export function restClientClass( const {config} = params; class NewRestClient extends RestClient { - public static CONFIG = config; - public static SCHEME = 'https'; + public static config = config; + public static scheme = 'https'; } Reflect.defineProperty(NewRestClient, 'name', {