@shopify/admin-api-client
Version:
Shopify Admin API Client - A lightweight JS client to interact with Shopify's Admin API
218 lines (195 loc) • 5.7 kB
text/typescript
import {
CustomFetchApi,
LogContentTypes,
Logger,
generateHttpFetch,
getCurrentSupportedApiVersions,
validateApiVersion,
validateDomainAndGetStoreUrl,
validateRetries,
} from '@shopify/graphql-client';
import {
validateRequiredAccessToken,
validateServerSideUsage,
} from '../validations';
import {
ACCESS_TOKEN_HEADER,
CLIENT,
DEFAULT_CLIENT_VERSION,
DEFAULT_CONTENT_TYPE,
DEFAULT_RETRY_WAIT_TIME,
RETRIABLE_STATUS_CODES,
} from '../constants';
import {
AdminRestApiClient,
AdminRestApiClientOptions,
DeleteRequestOptions,
GetRequestOptions,
HeaderOptions,
Method,
PostRequestOptions,
PutRequestOptions,
RequestOptions,
SearchParamFields,
SearchParams,
} from './types';
export function createAdminRestApiClient({
storeDomain,
apiVersion,
accessToken,
userAgentPrefix,
logger,
customFetchApi = fetch,
retries: clientRetries = 0,
scheme = 'https',
defaultRetryTime = DEFAULT_RETRY_WAIT_TIME,
formatPaths = true,
isTesting,
}: AdminRestApiClientOptions): AdminRestApiClient {
const currentSupportedApiVersions = getCurrentSupportedApiVersions();
const storeUrl = validateDomainAndGetStoreUrl({
client: CLIENT,
storeDomain,
}).replace('https://', `${scheme}://`);
const baseApiVersionValidationParams = {
client: CLIENT,
currentSupportedApiVersions,
logger,
};
validateServerSideUsage(isTesting);
validateApiVersion({
client: CLIENT,
currentSupportedApiVersions,
apiVersion,
logger,
});
validateRequiredAccessToken(accessToken);
validateRetries({client: CLIENT, retries: clientRetries});
const apiUrlFormatter = generateApiUrlFormatter(
storeUrl,
apiVersion,
baseApiVersionValidationParams,
formatPaths,
);
const clientLogger = generateClientLogger(logger);
const httpFetch = generateHttpFetch({
customFetchApi,
clientLogger,
defaultRetryWaitTime: defaultRetryTime,
client: CLIENT,
retriableCodes: RETRIABLE_STATUS_CODES,
});
const request = async (
path: string,
{
method,
data,
headers: requestHeadersObj,
searchParams,
retries = 0,
apiVersion,
}: RequestOptions,
): ReturnType<CustomFetchApi> => {
validateRetries({client: CLIENT, retries});
const url = apiUrlFormatter(path, searchParams ?? {}, apiVersion);
const requestHeaders = normalizedHeaders(requestHeadersObj ?? {});
const userAgent = [
...(requestHeaders['user-agent'] ? [requestHeaders['user-agent']] : []),
...(userAgentPrefix ? [userAgentPrefix] : []),
`${CLIENT} v${DEFAULT_CLIENT_VERSION}`,
].join(' | ');
const headers = normalizedHeaders({
'Content-Type': DEFAULT_CONTENT_TYPE,
...requestHeaders,
Accept: DEFAULT_CONTENT_TYPE,
[ACCESS_TOKEN_HEADER]: accessToken,
'User-Agent': userAgent,
});
const body = data && typeof data !== 'string' ? JSON.stringify(data) : data;
return httpFetch(
[url, {method, headers, ...(body ? {body} : undefined)}],
1,
retries ?? clientRetries,
);
};
return {
get: (path: string, options?: GetRequestOptions) =>
request(path, {method: Method.Get, ...options}),
put: (path: string, options?: PutRequestOptions) =>
request(path, {method: Method.Put, ...options}),
post: (path: string, options?: PostRequestOptions) =>
request(path, {method: Method.Post, ...options}),
delete: (path: string, options?: DeleteRequestOptions) =>
request(path, {method: Method.Delete, ...options}),
};
}
function generateApiUrlFormatter(
storeUrl: string,
defaultApiVersion: string,
baseApiVersionValidationParams: Omit<
Parameters<typeof validateApiVersion>[0],
'apiVersion'
>,
formatPaths = true,
) {
return (path: string, searchParams: SearchParams, apiVersion?: string) => {
if (apiVersion) {
validateApiVersion({
...baseApiVersionValidationParams,
apiVersion,
});
}
function convertValue(
params: URLSearchParams,
key: string,
value: SearchParamFields,
) {
if (Array.isArray(value)) {
value.forEach((arrayValue) =>
convertValue(params, `${key}[]`, arrayValue),
);
return;
} else if (typeof value === 'object') {
Object.entries(value).forEach(([objKey, objValue]) =>
convertValue(params, `${key}[${objKey}]`, objValue),
);
return;
}
params.append(key, String(value));
}
const urlApiVersion = (apiVersion ?? defaultApiVersion).trim();
let cleanPath = path.replace(/^\//, '');
if (formatPaths) {
if (!cleanPath.startsWith('admin')) {
cleanPath = `admin/api/${urlApiVersion}/${cleanPath}`;
}
if (!cleanPath.endsWith('.json')) {
cleanPath = `${cleanPath}.json`;
}
}
const params = new URLSearchParams();
if (searchParams) {
for (const [key, value] of Object.entries(searchParams)) {
convertValue(params, key, value);
}
}
const queryString = params.toString() ? `?${params.toString()}` : '';
return `${storeUrl}/${cleanPath}${queryString}`;
};
}
function generateClientLogger(logger?: Logger): Logger {
return (logContent: LogContentTypes) => {
if (logger) {
logger(logContent);
}
};
}
function normalizedHeaders(headersObj: HeaderOptions): Record<string, string> {
const normalizedHeaders: Record<string, string> = {};
for (const [key, value] of Object.entries(headersObj)) {
normalizedHeaders[key.toLowerCase()] = Array.isArray(value)
? value.join(', ')
: String(value);
}
return normalizedHeaders;
}