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. diff --git a/lib/clients/graphql/__tests__/graphql_client.test.ts b/lib/clients/graphql/__tests__/graphql_client.test.ts index ef987ff50..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 {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'; @@ -238,6 +243,33 @@ 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, + expect.stringContaining( + `GraphQL client overriding default API version ${LATEST_API_VERSION} with 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..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 {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'; @@ -120,6 +125,37 @@ 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, + expect.stringContaining( + `Storefront client overriding default API version ${LATEST_API_VERSION} with 2020-01`, + ), + ); + }); }); function buildExpectedResponse(obj: unknown) { diff --git a/lib/clients/graphql/graphql_client.ts b/lib/clients/graphql/graphql_client.ts index b96107979..b722d6a45 100644 --- a/lib/clients/graphql/graphql_client.ts +++ b/lib/clients/graphql/graphql_client.ts @@ -1,8 +1,9 @@ -import {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,25 +14,35 @@ 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; readonly client: HttpClient; + 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) { + 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.client = new (this.graphqlClass().HTTP_CLIENT)({ + this.apiVersion = params.apiVersion; + this.client = new (this.graphqlClass().HttpClient)({ domain: this.session.shop, }); } @@ -50,7 +61,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; @@ -78,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), }; } @@ -99,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 cc9b9b656..4fd79463f 100644 --- a/lib/clients/graphql/storefront_client.ts +++ b/lib/clients/graphql/storefront_client.ts @@ -3,6 +3,7 @@ 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'; @@ -20,7 +21,20 @@ export class StorefrontClient extends GraphqlClient { isOnline: true, accessToken: params.storefrontAccessToken, }); - super({session}); + + super({session, apiVersion: params.apiVersion}); + + const config = this.storefrontClass().config; + + if (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; this.storefrontAccessToken = params.storefrontAccessToken; } @@ -29,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, @@ -51,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/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/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__/resources/base.test.ts b/lib/clients/rest/__tests__/resources/base.test.ts new file mode 100644 index 000000000..934c2abbb --- /dev/null +++ b/lib/clients/rest/__tests__/resources/base.test.ts @@ -0,0 +1,575 @@ +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'; + +describe('Base REST resource', () => { + let prefix: string; + let shopify: Shopify; + + beforeEach(() => { + shopify = shopifyApi({ + ...testConfig, + restResources, + }); + + prefix = `/admin/api/${shopify.config.apiVersion}`; + }); + + 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'); + }); +}); + +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/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..75b7011b3 --- /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/lib/clients/rest/__tests__/rest_client.test.ts b/lib/clients/rest/__tests__/rest_client.test.ts index b217918d5..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 {ShopifyHeader} from '../../../types'; +import { + ApiVersion, + LATEST_API_VERSION, + LogSeverity, + ShopifyHeader, +} from '../../../types'; import {Session} from '../../../session/session'; import {JwtPayload} from '../../../session/types'; @@ -407,6 +412,32 @@ 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, + expect.stringContaining( + `REST client overriding default API version ${LATEST_API_VERSION} with 2020-01`, + ), + ); + }); }); function getDefaultPageInfo(): PageInfo { diff --git a/lib/clients/rest/rest_client.ts b/lib/clients/rest/rest_client.ts index d114759bf..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 {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,28 +17,41 @@ 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; 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) { + 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; + this.apiVersion = params.apiVersion; } protected async request( 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, }; @@ -97,7 +111,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`; } } @@ -124,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', { 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/__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(); - }); -}); 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}'`, ); }