UNPKG

@shopify/shopify-api

Version:

Shopify API Library for Node - accelerate development with support for authentication, graphql proxy, webhooks

219 lines (215 loc) 8.98 kB
'use strict'; var adminApiClient = require('@shopify/admin-api-client'); var network = require('@shopify/network'); var common = require('../../common.js'); var index$1 = require('../../../../runtime/http/index.js'); var types = require('../../../../runtime/crypto/types.js'); require('../../../../runtime/crypto/crypto.js'); var utils = require('../../../../runtime/crypto/utils.js'); var error = require('../../../error.js'); var index = require('../../../logger/index.js'); var headers = require('../../../../runtime/http/headers.js'); class RestClient { static config; static formatPaths; static LINK_HEADER_REGEXP = /<([^<]+)>; rel="([^"]+)"/; static DEFAULT_LIMIT = '50'; static RETRY_WAIT_TIME = 1000; static DEPRECATION_ALERT_DELAY = 300000; loggedDeprecations = {}; client; session; apiVersion; constructor({ session, apiVersion }) { const config = this.restClass().config; if (!config.isCustomStoreApp && !session.accessToken) { throw new error.MissingRequiredArgument('Missing access token when creating REST client'); } if (apiVersion) { const message = apiVersion === config.apiVersion ? `REST client has a redundant API version override to the default ${apiVersion}` : `REST client overriding default API version ${config.apiVersion} with ${apiVersion}`; index.logger(config).debug(message); } const customStoreAppAccessToken = config.adminApiAccessToken ?? config.apiSecretKey; this.session = session; this.apiVersion = apiVersion ?? config.apiVersion; this.client = adminApiClient.createAdminRestApiClient({ scheme: config.hostScheme, storeDomain: session.shop, apiVersion: apiVersion ?? config.apiVersion, accessToken: config.isCustomStoreApp ? customStoreAppAccessToken : session.accessToken, customFetchApi: index$1.abstractFetch, logger: common.clientLoggerFactory(config), userAgentPrefix: common.getUserAgent(config), defaultRetryTime: this.restClass().RETRY_WAIT_TIME, formatPaths: this.restClass().formatPaths, isTesting: config.isTesting, }); } /** * Performs a GET request on the given path. */ async get(params) { return this.request({ method: network.Method.Get, ...params }); } /** * Performs a POST request on the given path. */ async post(params) { return this.request({ method: network.Method.Post, ...params }); } /** * Performs a PUT request on the given path. */ async put(params) { return this.request({ method: network.Method.Put, ...params }); } /** * Performs a DELETE request on the given path. */ async delete(params) { return this.request({ method: network.Method.Delete, ...params }); } async request(params) { const requestParams = { headers: { ...params.extraHeaders, ...(params.type ? { 'Content-Type': params.type.toString() } : {}), }, retries: params.tries ? params.tries - 1 : undefined, searchParams: params.query, }; let response; switch (params.method) { case network.Method.Get: response = await this.client.get(params.path, requestParams); break; case network.Method.Put: response = await this.client.put(params.path, { ...requestParams, data: params.data, }); break; case network.Method.Post: response = await this.client.post(params.path, { ...requestParams, data: params.data, }); break; case network.Method.Delete: response = await this.client.delete(params.path, requestParams); break; default: throw new error.InvalidRequestError(`Unsupported request method '${params.method}'`); } const bodyString = await response.text(); // Some DELETE requests return an empty body but are still valid responses, we want those to go through const body = params.method === network.Method.Delete && bodyString === '' ? {} : JSON.parse(bodyString); const responseHeaders = headers.canonicalizeHeaders(Object.fromEntries(response.headers.entries())); if (!response.ok) { common.throwFailedRequest(body, (params.tries ?? 1) > 1, response); } const requestReturn = { body, headers: responseHeaders, }; await this.logDeprecations({ method: params.method, url: params.path, headers: requestParams.headers, body: params.data ? JSON.stringify(params.data) : undefined, }, requestReturn); const link = response.headers.get('Link'); if (link !== undefined) { const pageInfo = { limit: params.query?.limit ? params.query?.limit.toString() : RestClient.DEFAULT_LIMIT, }; if (link) { const links = link.split(', '); for (const link of links) { const parsedLink = link.match(RestClient.LINK_HEADER_REGEXP); if (!parsedLink) { continue; } const linkRel = parsedLink[2]; const linkUrl = new URL(parsedLink[1]); const linkFields = linkUrl.searchParams.get('fields'); const linkPageToken = linkUrl.searchParams.get('page_info'); if (!pageInfo.fields && linkFields) { pageInfo.fields = linkFields.split(','); } if (linkPageToken) { switch (linkRel) { case 'previous': pageInfo.previousPageUrl = parsedLink[1]; pageInfo.prevPage = this.buildRequestParams(parsedLink[1]); break; case 'next': pageInfo.nextPageUrl = parsedLink[1]; pageInfo.nextPage = this.buildRequestParams(parsedLink[1]); break; } } } } requestReturn.pageInfo = pageInfo; } return requestReturn; } restClass() { return this.constructor; } buildRequestParams(newPageUrl) { const pattern = `^/admin/api/[^/]+/(.*).json$`; const url = new URL(newPageUrl); const path = url.pathname.replace(new RegExp(pattern), '$1'); return { path, query: Object.fromEntries(url.searchParams.entries()), }; } async logDeprecations(request, response) { const config = this.restClass().config; const deprecationReason = headers.getHeader(response.headers, 'X-Shopify-API-Deprecated-Reason'); if (deprecationReason) { const deprecation = { message: deprecationReason, path: request.url, }; if (request.body) { // This can only be a string, since we're always converting the body before calling this method deprecation.body = `${request.body.substring(0, 100)}...`; } const depHash = await utils.createSHA256HMAC(config.apiSecretKey, JSON.stringify(deprecation), types.HashFormat.Hex); if (!Object.keys(this.loggedDeprecations).includes(depHash) || Date.now() - this.loggedDeprecations[depHash] >= RestClient.DEPRECATION_ALERT_DELAY) { this.loggedDeprecations[depHash] = Date.now(); const stack = new Error().stack; const message = `API Deprecation Notice ${new Date().toLocaleString()} : ${JSON.stringify(deprecation)} - Stack Trace: ${stack}`; await index.logger(config).warning(message); } } } } function restClientClass(params) { const { config, formatPaths } = params; class NewRestClient extends RestClient { static config = config; static formatPaths = formatPaths === undefined ? true : formatPaths; } Reflect.defineProperty(NewRestClient, 'name', { value: 'RestClient', }); return NewRestClient; } exports.RestClient = RestClient; exports.restClientClass = restClientClass; //# sourceMappingURL=client.js.map