From 671207fd8e53885a2e15a01fabc9e43c5054b30f Mon Sep 17 00:00:00 2001 From: Spencer Lepine <60903378+spencerlepine@users.noreply.github.com> Date: Sun, 5 Jan 2025 09:24:14 +0000 Subject: [PATCH 1/7] refactor: abstract http class from client --- README.md | 6 +- examples/.gitignore | 5 ++ package.json | 2 +- src/client.ts | 127 +++++++------------------------------- src/http.ts | 88 ++++++++++++++++++++++++++ src/v1/catalog/index.ts | 45 ++++++-------- src/v1/shops/deleteOne.ts | 18 ++---- src/v1/shops/index.ts | 20 +++--- src/v1/shops/list.ts | 5 +- src/v1/types.ts | 6 ++ src/v2/catalog/index.ts | 17 +++++ src/v2/types.ts | 5 ++ tests/catalog.test.ts | 6 +- tests/orders.test.ts | 2 +- tests/products.test.ts | 2 +- tests/shops.test.ts | 2 +- tests/uploads.test.ts | 2 +- tests/webhooks.test.ts | 2 +- 18 files changed, 195 insertions(+), 165 deletions(-) create mode 100644 examples/.gitignore create mode 100644 src/http.ts create mode 100644 src/v2/catalog/index.ts create mode 100644 src/v2/types.ts diff --git a/README.md b/README.md index b7441d9..33f4172 100644 --- a/README.md +++ b/README.md @@ -97,9 +97,9 @@ const printify = new Printify({ | Option | Default | Description | | --------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `accessToken` | `null` | The API access token for authenticating requests. Generate one at [Printify API](https://printify.com/app/account/api). | -| `shopId` | `null` | (optional) Your personal shop ID. Can be found using `printify.shops.list()`. | +| `shopId` | `null` | (optional) Your personal shop ID. Can be found using `printify.shops.list()`. | | `enableLogging` | `true` | (optional) Enables logging of API requests and responses. Enabled by default. | -| `host` | `'api.printify.com'` | (optional) The host for API requests. | +| `host` | `'api.printify.com'` | (optional) The host for API requests. | | `timeout` | `5000` | (optional) Request timeout in ms. | ## Development @@ -116,7 +116,7 @@ yarn test yarn build mv dist examples/development cd examples/development -yarn start +yarn && yarn start ``` ## Contributing diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..4d3582a --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,5 @@ +node_modules +package-lock.json +yarn.lock +!commonjs/app.js +app.js \ No newline at end of file diff --git a/package.json b/package.json index a9f48af..90a9e23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "printify-sdk-js", - "version": "1.2.0", + "version": "1.3.0-beta.1", "description": "Node.js SDK for the Printify API.", "author": "Spencer Lepine ", "license": "MIT", diff --git a/src/client.ts b/src/client.ts index 722968f..cc132a8 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,122 +1,43 @@ -import Catalog, { ICatalog } from './v1/catalog'; +import Catalog from './v1/catalog'; import Orders, { IOrders } from './v1/orders'; import Products, { IProducts } from './v1/products'; -import Shops, { IShops } from './v1/shops'; +import Shops from './v1/shops'; import Uploads, { IUploads } from './v1/uploads'; import Webhooks, { IWebhooks } from './v1/webhooks'; -import axios, { AxiosRequestConfig } from 'axios'; -import axiosRetry from 'axios-retry'; +import { AxiosRequestConfig } from 'axios'; +// TODO - delete me export type FetchDataFn = (url: string, config?: AxiosRequestConfig) => Promise; -axiosRetry(axios, { retries: 3, retryDelay: axiosRetry.exponentialDelay }); - export interface PrintifyConfig { - shopId: string; accessToken: string; - apiVersion?: string; + shopId?: string; enableLogging?: boolean; - host?: 'api.printify.com'; + host?: string; timeout?: number; } -class Printify { - shopId: string; - #accessToken: string; - apiVersion?: string; - enableLogging: boolean; - host: string; - timeout: number; - baseUrl: string; - // methods - catalog!: ICatalog; - orders: IOrders; - products: IProducts; - shops: IShops; - uploads: IUploads; - webhooks: IWebhooks; +class PrintifyClient { + shopId?: string; + catalog: Catalog; + // v2: { catalog: ICatalogV2 }; // Define v2.catalog structure + // orders: IOrders; + // products: IProducts; + shops: Shops; + // uploads: IUploads; + // webhooks: IWebhooks; constructor(config: PrintifyConfig) { this.shopId = config.shopId; - this.#accessToken = config.accessToken; - this.apiVersion = config.apiVersion || 'v1'; - this.enableLogging = config.enableLogging ?? true; - this.host = config.host || 'api.printify.com'; - this.timeout = config.timeout || 5000; - this.baseUrl = `https://${this.host}`; - // methods - this.catalog = new Catalog(this.fetchData.bind(this), this.shopId); - this.orders = new Orders(this.fetchData.bind(this), this.shopId); - this.products = new Products(this.fetchData.bind(this), this.shopId); - this.shops = new Shops(this.fetchData.bind(this), this.shopId); - this.uploads = new Uploads(this.fetchData.bind(this), this.shopId); - this.webhooks = new Webhooks(this.fetchData.bind(this), this.shopId); - } - - private logError(message: string) { - if (this.enableLogging) { - console.error(message); - } - } - - private logRequest(method: string, url: string) { - if (this.enableLogging) { - console.log(`Requesting ${method.toUpperCase()} ${this.baseUrl}${url}`); - } - } - - private async fetchData(url: string, config: AxiosRequestConfig = {}): Promise { - const defaultHeaders = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.#accessToken}`, - }; - - const requestConfig: AxiosRequestConfig = { - ...(config.method ? config : { ...config, method: 'GET' }), - baseURL: this.baseUrl, - timeout: this.timeout, - headers: { - ...defaultHeaders, - ...(config.headers || {}), - }, - }; - - const method = config.method?.toLowerCase() || 'get'; - this.logRequest(method, url); - - try { - let response; - switch (method) { - case 'post': - response = await axios.post(url, config.data, requestConfig); - break; - case 'put': - response = await axios.put(url, config.data, requestConfig); - break; - case 'delete': - response = await axios.delete(url, requestConfig); - break; - // case 'patch': - // response = await axios.patch(url, config.data, requestConfig); - // break; - case 'get': - default: - response = await axios.get(url, requestConfig); - break; - } - return response.data; - } catch (error) { - if (axios.isAxiosError(error)) { - const message = `Printify SDK Error: ${error.response?.status} ${error.response?.statusText} - Requested URL: ${this.baseUrl}${url}`; - this.logError(message); - throw new Error(message); - } else { - const message = 'Printify SDK Unknown Error'; - this.logError(message); - throw new Error(message); - } - } + this.catalog = new Catalog(config); + // this.v2 = { catalog: new ICatalogV2(this) }; + // this.orders = new Orders(this.fetchData.bind(this), this.shopId); + // this.products = new Products(this.fetchData.bind(this), this.shopId); + // this.shops = new Shops(this.fetchData.bind(this), this.shopId); + this.shops = new Shops(config); + // this.uploads = new Uploads(this.fetchData.bind(this), this.shopId); + // this.webhooks = new Webhooks(this.fetchData.bind(this), this.shopId); } } -export default Printify; +export default PrintifyClient; diff --git a/src/http.ts b/src/http.ts new file mode 100644 index 0000000..f3f56be --- /dev/null +++ b/src/http.ts @@ -0,0 +1,88 @@ +import axios, { AxiosRequestConfig } from 'axios'; +import axiosRetry from 'axios-retry'; + +axiosRetry(axios, { retries: 3, retryDelay: axiosRetry.exponentialDelay }); + +interface HttpConfig { + accessToken: string; + enableLogging?: boolean; + host?: string; + timeout?: number; +} + +class HttpClient { + private accessToken: string; + private host: string; + private timeout: number; + private enableLogging?: boolean; + private baseUrl: string; + + constructor(config: HttpConfig) { + this.accessToken = config.accessToken; + this.host = config.host || 'api.printify.com'; + this.timeout = config.timeout || 5000; + this.baseUrl = `https://${this.host}`; + } + + private logError(message: string) { + if (this.enableLogging) { + console.error(message); + } + } + + private logRequest(method: string, url: string) { + if (this.enableLogging) { + console.log(`Requesting ${method.toUpperCase()} ${this.baseUrl}${url}`); + } + } + + async request(url: string, config: AxiosRequestConfig = {}): Promise { + const defaultHeaders = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.accessToken}`, + }; + + const requestConfig: AxiosRequestConfig = { + ...config, + method: config.method || 'GET', + baseURL: this.baseUrl, + timeout: this.timeout, + headers: { + ...defaultHeaders, + ...(config.headers || {}), + }, + }; + + const method = (config.method || 'GET').toLowerCase(); + this.logRequest(method, url); + + try { + let response; + switch (method) { + case 'post': + response = await axios.post(url, config.data, requestConfig); + break; + case 'put': + response = await axios.put(url, config.data, requestConfig); + break; + case 'delete': + response = await axios.delete(url, requestConfig); + break; + case 'get': + default: + response = await axios.get(url, requestConfig); + break; + } + return response.data; + } catch (error) { + let message = 'Printify SDK Unknown Error'; + if (axios.isAxiosError(error)) { + message = `Printify SDK Error: ${error.response?.status} ${error.response?.statusText} - Requested URL: ${this.baseUrl}${url}`; + } + this.logError(message); + throw new Error(message); + } + } +} + +export default HttpClient; diff --git a/src/v1/catalog/index.ts b/src/v1/catalog/index.ts index 09564e1..a6f756d 100644 --- a/src/v1/catalog/index.ts +++ b/src/v1/catalog/index.ts @@ -1,4 +1,5 @@ -import { FetchDataFn } from '../../client'; +import { PrintifyConfig } from '../../client'; +import HttpClient from '../../http'; import listBlueprints from './listBlueprints'; import getBlueprint from './getBlueprint'; import getBlueprintProviders from './getBlueprintProviders'; @@ -7,33 +8,25 @@ import getVariantShipping from './getVariantShipping'; import listProviders from './listProviders'; import getProvider from './getProvider'; -export interface ICatalog { - listBlueprints: ReturnType; - getBlueprint: ReturnType; - getBlueprintProviders: ReturnType; - getBlueprintVariants: ReturnType; - getVariantShipping: ReturnType; - listProviders: ReturnType; - getProvider: ReturnType; -} +class Catalog extends HttpClient { + listBlueprints: typeof listBlueprints; + getBlueprint: typeof getBlueprint; + getBlueprintProviders: typeof getBlueprintProviders; + getBlueprintVariants: typeof getBlueprintVariants; + getVariantShipping: typeof getVariantShipping; + listProviders: typeof listProviders; + getProvider: typeof getProvider; -class Catalog implements ICatalog { - listBlueprints: ReturnType; - getBlueprint: ReturnType; - getBlueprintProviders: ReturnType; - getBlueprintVariants: ReturnType; - getVariantShipping: ReturnType; - listProviders: ReturnType; - getProvider: ReturnType; + constructor(config: PrintifyConfig) { + super(config); - constructor(fetchData: FetchDataFn, shopId: string) { - this.listBlueprints = listBlueprints(fetchData); - this.getBlueprint = getBlueprint(fetchData); - this.getBlueprintProviders = getBlueprintProviders(fetchData); - this.getBlueprintVariants = getBlueprintVariants(fetchData); - this.getVariantShipping = getVariantShipping(fetchData); - this.listProviders = listProviders(fetchData); - this.getProvider = getProvider(fetchData); + this.listBlueprints = listBlueprints.bind(this); + this.getBlueprint = getBlueprint.bind(this); + this.getBlueprintProviders = getBlueprintProviders.bind(this); + this.getBlueprintVariants = getBlueprintVariants.bind(this); + this.getVariantShipping = getVariantShipping.bind(this); + this.listProviders = listProviders.bind(this); + this.getProvider = getProvider.bind(this); } } diff --git a/src/v1/shops/deleteOne.ts b/src/v1/shops/deleteOne.ts index 129b750..fbc0e81 100644 --- a/src/v1/shops/deleteOne.ts +++ b/src/v1/shops/deleteOne.ts @@ -1,5 +1,3 @@ -import { FetchDataFn } from '../../client'; - /** * Disconnect a shop * @@ -7,18 +5,14 @@ import { FetchDataFn } from '../../client'; * @returns {Promise} * * @example - * await printify.shops.deleteOne(); // defaults to "printify.shopId" - * // Expected response: {} - * * const customShopId = "67890"; * await printify.shops.deleteOne(customShopId); + * // Expected response: {} */ -const deleteOne = - (fetchData: FetchDataFn, defaultShopId: string) => - (customShopId?: string): Promise => { - return fetchData(`/v1/shops/${customShopId || defaultShopId}/connection.json`, { - method: 'DELETE', - }); - }; +const deleteOne = function (this: method, customShopId?: string): Promise { + return this.request(`/v1/shops/${customShopId || this.shopId}/connection.json`, { + method: 'DELETE', + }); +}; export default deleteOne; diff --git a/src/v1/shops/index.ts b/src/v1/shops/index.ts index f25aecb..4cb18a7 100644 --- a/src/v1/shops/index.ts +++ b/src/v1/shops/index.ts @@ -1,19 +1,17 @@ -import { FetchDataFn } from '../../client'; +import { PrintifyConfig } from '../../client'; +import HttpClient from '../../http'; import deleteOne from './deleteOne'; import list from './list'; -export interface IShops { - deleteOne: ReturnType; - list: ReturnType; -} +class Shops extends HttpClient { + deleteOne: typeof deleteOne; + list: typeof list; -class Shops implements IShops { - deleteOne: ReturnType; - list: ReturnType; + constructor(config: PrintifyConfig) { + super(config); - constructor(fetchData: FetchDataFn, shopId: string) { - this.deleteOne = deleteOne(fetchData, shopId); - this.list = list(fetchData); + this.deleteOne = deleteOne.bind(this); + this.list = list.bind(this); } } diff --git a/src/v1/shops/list.ts b/src/v1/shops/list.ts index 1e2b872..8782255 100644 --- a/src/v1/shops/list.ts +++ b/src/v1/shops/list.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Shop } from '../types'; export type ListShopsResponse = Shop[]; @@ -16,8 +15,8 @@ export type ListShopsResponse = Shop[]; * // { id: 9876, title: "My other new store", sales_channel: "disconnected" } * // ] */ -const list = (fetchData: FetchDataFn) => (): Promise => { - return fetchData(`/v1/shops.json`, { method: 'GET' }); +const list = function (this: method): Promise { + return this.request(`/v1/shops.json`, { method: 'GET' }); }; export default list; diff --git a/src/v1/types.ts b/src/v1/types.ts index d57b6c5..cb2a239 100644 --- a/src/v1/types.ts +++ b/src/v1/types.ts @@ -1,3 +1,9 @@ +// Global +declare global { + type RequestFn = (url: string, config: Record) => Promise; + type method = { request: RequestFn; shopId?: string }; +} + // Catalog export interface PrintProvider { id: number; diff --git a/src/v2/catalog/index.ts b/src/v2/catalog/index.ts new file mode 100644 index 0000000..d632a11 --- /dev/null +++ b/src/v2/catalog/index.ts @@ -0,0 +1,17 @@ +// import { FetchDataFn } from '../../client'; +// import getSomething from './getSomething'; + +// TODO v1.3.0 +export interface ICatalogV2 { + // getSomething: ReturnType; +} + +class CatalogV2 implements ICatalogV2 { + // getSomething: ReturnType; + + constructor() { + // this.getSomething = getSomething(fetchData); + } +} + +export default CatalogV2; diff --git a/src/v2/types.ts b/src/v2/types.ts new file mode 100644 index 0000000..aae4523 --- /dev/null +++ b/src/v2/types.ts @@ -0,0 +1,5 @@ +// Catalog +// TODO v1.3.0 +export interface ReplaceMe { + example: any; +} diff --git a/tests/catalog.test.ts b/tests/catalog.test.ts index 7336057..f49b411 100644 --- a/tests/catalog.test.ts +++ b/tests/catalog.test.ts @@ -5,7 +5,7 @@ describe('Catalog', () => { let printify: Printify; beforeAll(() => { - printify = new Printify({ shopId: '123456', accessToken: 'mockAccessToken', apiVersion: 'v1' }); + printify = new Printify({ shopId: '123456', accessToken: 'mockAccessToken' }); }); it('should handle the get blueprint endpoint', async () => { @@ -71,3 +71,7 @@ describe('Catalog', () => { assertAxiosCall('get', '/v1/catalog/print_providers.json'); }); }); + +describe('Catalog V2', () => { + // TODO v1.3.0 +}); diff --git a/tests/orders.test.ts b/tests/orders.test.ts index 5933f8d..4c1dc53 100644 --- a/tests/orders.test.ts +++ b/tests/orders.test.ts @@ -5,7 +5,7 @@ describe('Orders', () => { let printify: Printify; beforeAll(() => { - printify = new Printify({ shopId: '123456', accessToken: 'mockAccessToken', apiVersion: 'v1' }); + printify = new Printify({ shopId: '123456', accessToken: 'mockAccessToken' }); }); it('should handle the calculate shipping endpoint', async () => { diff --git a/tests/products.test.ts b/tests/products.test.ts index 4b99a43..b4d8ca9 100644 --- a/tests/products.test.ts +++ b/tests/products.test.ts @@ -5,7 +5,7 @@ describe('Products', () => { let printify: Printify; beforeAll(() => { - printify = new Printify({ shopId: '123456', accessToken: 'mockAccessToken', apiVersion: 'v1' }); + printify = new Printify({ shopId: '123456', accessToken: 'mockAccessToken' }); }); it('should handle the create product endpoint', async () => { diff --git a/tests/shops.test.ts b/tests/shops.test.ts index 551654b..46df31a 100644 --- a/tests/shops.test.ts +++ b/tests/shops.test.ts @@ -5,7 +5,7 @@ describe('Shops', () => { let printify: Printify; beforeAll(() => { - printify = new Printify({ shopId: '123456', accessToken: 'mockAccessToken', apiVersion: 'v1' }); + printify = new Printify({ shopId: '123456', accessToken: 'mockAccessToken' }); }); it('should handle deleting a shop by default shopId', async () => { diff --git a/tests/uploads.test.ts b/tests/uploads.test.ts index 041d3b9..2bb3466 100644 --- a/tests/uploads.test.ts +++ b/tests/uploads.test.ts @@ -5,7 +5,7 @@ describe('Uploads', () => { let printify: Printify; beforeAll(() => { - printify = new Printify({ shopId: '123456', accessToken: 'mockAccessToken', apiVersion: 'v1' }); + printify = new Printify({ shopId: '123456', accessToken: 'mockAccessToken' }); }); it('should handle the archive upload endpoint', async () => { diff --git a/tests/webhooks.test.ts b/tests/webhooks.test.ts index 5364a1b..c769109 100644 --- a/tests/webhooks.test.ts +++ b/tests/webhooks.test.ts @@ -5,7 +5,7 @@ describe('Webhooks', () => { let printify: Printify; beforeAll(() => { - printify = new Printify({ shopId: '123456', accessToken: 'mockAccessToken', apiVersion: 'v1' }); + printify = new Printify({ shopId: '123456', accessToken: 'mockAccessToken' }); }); it('should handle the create webhook endpoint', async () => { From 0aab23f29ea3cc6392294c102ec762cf1d96a4a7 Mon Sep 17 00:00:00 2001 From: Spencer Lepine <60903378+spencerlepine@users.noreply.github.com> Date: Sun, 5 Jan 2025 11:01:26 +0000 Subject: [PATCH 2/7] refactors and v2 endpoint --- src/client.ts | 36 +++-- src/http.ts | 12 +- src/v1/catalog/getBlueprint.ts | 10 +- src/v1/catalog/getBlueprintProviders.ts | 9 +- src/v1/catalog/getBlueprintVariants.ts | 9 +- src/v1/catalog/getProvider.ts | 9 +- src/v1/catalog/getVariantShipping.ts | 9 +- src/v1/catalog/listBlueprints.ts | 5 +- src/v1/catalog/listProviders.ts | 5 +- src/v1/orders/calculateShipping.ts | 15 +-- src/v1/orders/cancelUnpaid.ts | 9 +- src/v1/orders/getOne.ts | 9 +- src/v1/orders/index.ts | 45 +++---- src/v1/orders/list.ts | 23 ++-- src/v1/orders/sendToProduction.ts | 13 +- src/v1/orders/submit.ts | 15 +-- src/v1/orders/submitExpress.ts | 15 +-- src/v1/products/create.ts | 15 +-- src/v1/products/deleteOne.ts | 14 +- src/v1/products/getOne.ts | 13 +- src/v1/products/index.ts | 55 ++++---- src/v1/products/list.ts | 25 ++-- src/v1/products/notifyUnpublished.ts | 14 +- src/v1/products/publishOne.ts | 16 +-- src/v1/products/setPublishFailed.ts | 16 +-- src/v1/products/setPublishSucceeded.ts | 15 +-- src/v1/products/updateOne.ts | 15 +-- src/v1/shops/deleteOne.ts | 2 +- src/v1/uploads/archive.ts | 10 +- src/v1/uploads/getById.ts | 9 +- src/v1/uploads/index.ts | 30 ++--- src/v1/uploads/list.ts | 17 +-- src/v1/uploads/uploadImage.ts | 15 +-- src/v1/webhooks/create.ts | 15 +-- src/v1/webhooks/deleteOne.ts | 14 +- src/v1/webhooks/index.ts | 30 ++--- src/v1/webhooks/list.ts | 5 +- src/v1/webhooks/updateOne.ts | 16 +-- src/v2/catalog/getShippingListInfo.ts | 56 ++++++++ src/v2/catalog/index.ts | 19 ++- tests/catalog.test.ts | 17 ++- tests/http.test.ts | 169 ++++++++++++++++++++++++ tests/orders.test.ts | 2 +- tests/printify.test.ts | 99 ++++++++------ tests/products.test.ts | 2 +- tests/shops.test.ts | 2 +- tests/uploads.test.ts | 2 +- tests/webhooks.test.ts | 2 +- 48 files changed, 556 insertions(+), 423 deletions(-) create mode 100644 src/v2/catalog/getShippingListInfo.ts create mode 100644 tests/http.test.ts diff --git a/src/client.ts b/src/client.ts index cc132a8..8e4ab7d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,13 +1,10 @@ import Catalog from './v1/catalog'; -import Orders, { IOrders } from './v1/orders'; -import Products, { IProducts } from './v1/products'; +import CatalogV2 from './v2/catalog'; +import Orders from './v1/orders'; +import Products from './v1/products'; import Shops from './v1/shops'; -import Uploads, { IUploads } from './v1/uploads'; -import Webhooks, { IWebhooks } from './v1/webhooks'; -import { AxiosRequestConfig } from 'axios'; - -// TODO - delete me -export type FetchDataFn = (url: string, config?: AxiosRequestConfig) => Promise; +import Uploads from './v1/uploads'; +import Webhooks from './v1/webhooks'; export interface PrintifyConfig { accessToken: string; @@ -20,23 +17,24 @@ export interface PrintifyConfig { class PrintifyClient { shopId?: string; catalog: Catalog; - // v2: { catalog: ICatalogV2 }; // Define v2.catalog structure - // orders: IOrders; - // products: IProducts; + orders: Orders; + products: Products; shops: Shops; - // uploads: IUploads; - // webhooks: IWebhooks; + uploads: Uploads; + webhooks: Webhooks; + v2: { catalog: CatalogV2 }; constructor(config: PrintifyConfig) { + if (!config.accessToken) throw new Error('accessToken is required'); + this.shopId = config.shopId; this.catalog = new Catalog(config); - // this.v2 = { catalog: new ICatalogV2(this) }; - // this.orders = new Orders(this.fetchData.bind(this), this.shopId); - // this.products = new Products(this.fetchData.bind(this), this.shopId); - // this.shops = new Shops(this.fetchData.bind(this), this.shopId); + this.v2 = { catalog: new CatalogV2(config) }; + this.orders = new Orders(config); + this.products = new Products(config); this.shops = new Shops(config); - // this.uploads = new Uploads(this.fetchData.bind(this), this.shopId); - // this.webhooks = new Webhooks(this.fetchData.bind(this), this.shopId); + this.uploads = new Uploads(config); + this.webhooks = new Webhooks(config); } } diff --git a/src/http.ts b/src/http.ts index f3f56be..a87f93e 100644 --- a/src/http.ts +++ b/src/http.ts @@ -1,10 +1,11 @@ -import axios, { AxiosRequestConfig } from 'axios'; +import axios, { AxiosError, AxiosRequestConfig } from 'axios'; import axiosRetry from 'axios-retry'; axiosRetry(axios, { retries: 3, retryDelay: axiosRetry.exponentialDelay }); interface HttpConfig { accessToken: string; + shopId?: string; enableLogging?: boolean; host?: string; timeout?: number; @@ -12,6 +13,7 @@ interface HttpConfig { class HttpClient { private accessToken: string; + shopId?: string; private host: string; private timeout: number; private enableLogging?: boolean; @@ -19,8 +21,10 @@ class HttpClient { constructor(config: HttpConfig) { this.accessToken = config.accessToken; + this.shopId = config.shopId; this.host = config.host || 'api.printify.com'; this.timeout = config.timeout || 5000; + this.enableLogging = config.enableLogging ?? true; this.baseUrl = `https://${this.host}`; } @@ -74,9 +78,9 @@ class HttpClient { break; } return response.data; - } catch (error) { - let message = 'Printify SDK Unknown Error'; - if (axios.isAxiosError(error)) { + } catch (error: any) { + let message = 'Printify SDK Error'; + if ((error as AxiosError).isAxiosError) { message = `Printify SDK Error: ${error.response?.status} ${error.response?.statusText} - Requested URL: ${this.baseUrl}${url}`; } this.logError(message); diff --git a/src/v1/catalog/getBlueprint.ts b/src/v1/catalog/getBlueprint.ts index ac00955..0c23f98 100644 --- a/src/v1/catalog/getBlueprint.ts +++ b/src/v1/catalog/getBlueprint.ts @@ -1,5 +1,3 @@ -import { FetchDataFn } from '../../client'; - export type GetBlueprintResponse = { id: number; title: string; @@ -28,10 +26,8 @@ export type GetBlueprintResponse = { * // "images": ["https://images.printify.com/5853fe7dce46f30f8327f5cd", "https://images.printify.com/5c487ee2a342bc9b8b2fc4d2"] * // } */ -const getBlueprint = - (fetchData: FetchDataFn) => - (blueprintId: string): Promise => { - return fetchData(`/v1/catalog/blueprints/${blueprintId}.json`, { method: 'GET' }); - }; +const getBlueprint = function (this: method, blueprintId: string): Promise { + return this.request(`/v1/catalog/blueprints/${blueprintId}.json`, { method: 'GET' }); +}; export default getBlueprint; diff --git a/src/v1/catalog/getBlueprintProviders.ts b/src/v1/catalog/getBlueprintProviders.ts index 82716eb..176feb5 100644 --- a/src/v1/catalog/getBlueprintProviders.ts +++ b/src/v1/catalog/getBlueprintProviders.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { PrintProvider } from '../types'; export type GetBlueprintProviderResponse = PrintProvider[]; @@ -21,10 +20,8 @@ export type GetBlueprintProviderResponse = PrintProvider[]; * // { "id": 24, "title": "Inklocker" } * // ] */ -const getBlueprintProviders = - (fetchData: FetchDataFn) => - (blueprintId: string): Promise => { - return fetchData(`/v1/catalog/blueprints/${blueprintId}/print_providers.json`, { method: 'GET' }); - }; +const getBlueprintProviders = function (this: method, blueprintId: string): Promise { + return this.request(`/v1/catalog/blueprints/${blueprintId}/print_providers.json`, { method: 'GET' }); +}; export default getBlueprintProviders; diff --git a/src/v1/catalog/getBlueprintVariants.ts b/src/v1/catalog/getBlueprintVariants.ts index 060fddc..967ce1e 100644 --- a/src/v1/catalog/getBlueprintVariants.ts +++ b/src/v1/catalog/getBlueprintVariants.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Variant } from '../types'; export type GetBlueprintVariantsResponse = { @@ -29,10 +28,8 @@ export type GetBlueprintVariantsResponse = { * // ] * // } */ -const getBlueprintVariants = - (fetchData: FetchDataFn) => - (blueprintId: string, printProviderId: string): Promise => { - return fetchData(`/v1/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/variants.json`, { method: 'GET' }); - }; +const getBlueprintVariants = function (this: method, blueprintId: string, printProviderId: string): Promise { + return this.request(`/v1/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/variants.json`, { method: 'GET' }); +}; export default getBlueprintVariants; diff --git a/src/v1/catalog/getProvider.ts b/src/v1/catalog/getProvider.ts index ba58d85..0e6a841 100644 --- a/src/v1/catalog/getProvider.ts +++ b/src/v1/catalog/getProvider.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Blueprint, Location } from '../types'; export type GetProviderResponse = { @@ -28,10 +27,8 @@ export type GetProviderResponse = { * ] * } */ -const getProvider = - (fetchData: FetchDataFn) => - (printProviderId: string): Promise => { - return fetchData(`/v1/catalog/print_providers/${printProviderId}.json`, { method: 'GET' }); - }; +const getProvider = function (this: method, printProviderId: string): Promise { + return this.request(`/v1/catalog/print_providers/${printProviderId}.json`, { method: 'GET' }); +}; export default getProvider; diff --git a/src/v1/catalog/getVariantShipping.ts b/src/v1/catalog/getVariantShipping.ts index 31f35a8..7cef8ee 100644 --- a/src/v1/catalog/getVariantShipping.ts +++ b/src/v1/catalog/getVariantShipping.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { ShippingProfile } from '../types'; export type GetVariantShippingResponse = { @@ -28,10 +27,8 @@ export type GetVariantShippingResponse = { * // ] * // } */ -const getVariantShipping = - (fetchData: FetchDataFn) => - (blueprintId: string, printProviderId: string): Promise => { - return fetchData(`/v1/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping.json`, { method: 'GET' }); - }; +const getVariantShipping = function (this: method, blueprintId: string, printProviderId: string): Promise { + return this.request(`/v1/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping.json`, { method: 'GET' }); +}; export default getVariantShipping; diff --git a/src/v1/catalog/listBlueprints.ts b/src/v1/catalog/listBlueprints.ts index 71d8041..ec1a6ba 100644 --- a/src/v1/catalog/listBlueprints.ts +++ b/src/v1/catalog/listBlueprints.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Blueprint } from '../types'; export type ListBlueprintsResponse = Blueprint[]; @@ -35,8 +34,8 @@ export type ListBlueprintsResponse = Blueprint[]; * // } * // ] */ -const listBlueprints = (fetchData: FetchDataFn) => (): Promise => { - return fetchData('/v1/catalog/blueprints.json', { method: 'GET' }); +const listBlueprints = function (this: method): Promise { + return this.request('/v1/catalog/blueprints.json', { method: 'GET' }); }; export default listBlueprints; diff --git a/src/v1/catalog/listProviders.ts b/src/v1/catalog/listProviders.ts index fc65c3a..8d3ab90 100644 --- a/src/v1/catalog/listProviders.ts +++ b/src/v1/catalog/listProviders.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { PrintProvider } from '../types'; export type ListProvidersResponse = PrintProvider[]; @@ -38,8 +37,8 @@ export type ListProvidersResponse = PrintProvider[]; * // // ... * // ] */ -const listProviders = (fetchData: FetchDataFn) => (): Promise => { - return fetchData('/v1/catalog/print_providers.json', { method: 'GET' }); +const listProviders = function (this: method): Promise { + return this.request('/v1/catalog/print_providers.json', { method: 'GET' }); }; export default listProviders; diff --git a/src/v1/orders/calculateShipping.ts b/src/v1/orders/calculateShipping.ts index 3a9abfe..e5e39b8 100644 --- a/src/v1/orders/calculateShipping.ts +++ b/src/v1/orders/calculateShipping.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { CalculateShippingData } from '../types'; export type CalculateShippingResponse = { @@ -38,13 +37,11 @@ export type CalculateShippingResponse = { * const shippingCosts = await printify.orders.calculateShipping(data); * // Expected response: { standard: 1000, express: 5000, priority: 5000, printify_express: 799, economy: 399 } */ -const calculateShipping = - (fetchData: FetchDataFn, shopId: string) => - (data: CalculateShippingData): Promise => { - return fetchData(`/v1/shops/${shopId}/orders/shipping.json`, { - method: 'POST', - data: JSON.stringify(data), - }); - }; +const calculateShipping = function (this: method, data: CalculateShippingData): Promise { + return this.request(`/v1/shops/${this.shopId}/orders/shipping.json`, { + method: 'POST', + data: JSON.stringify(data), + }); +}; export default calculateShipping; diff --git a/src/v1/orders/cancelUnpaid.ts b/src/v1/orders/cancelUnpaid.ts index 73168ea..4bbb220 100644 --- a/src/v1/orders/cancelUnpaid.ts +++ b/src/v1/orders/cancelUnpaid.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Address, LineItem } from '../types'; export type CancelUnpaidOrderResponse = { @@ -75,10 +74,8 @@ export type CancelUnpaidOrderResponse = { * // created_at: "2019-12-09 10:46:53+00:00" * // } */ -const cancelUnpaid = - (fetchData: FetchDataFn, shopId: string) => - (orderId: string): Promise => { - return fetchData(`/v1/shops/${shopId}/orders/${orderId}/cancel.json`, { method: 'POST' }); - }; +const cancelUnpaid = function (this: method, orderId: string): Promise { + return this.request(`/v1/shops/${this.shopId}/orders/${orderId}/cancel.json`, { method: 'POST' }); +}; export default cancelUnpaid; diff --git a/src/v1/orders/getOne.ts b/src/v1/orders/getOne.ts index c745d31..41b6aba 100644 --- a/src/v1/orders/getOne.ts +++ b/src/v1/orders/getOne.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Order } from '../types'; export type GetOrderResponse = Order; @@ -76,10 +75,8 @@ export type GetOrderResponse = Order; * // } * // } */ -const getOne = - (fetchData: FetchDataFn, shopId: string) => - (orderId: string): Promise => { - return fetchData(`/v1/shops/${shopId}/orders/${orderId}.json`, { method: 'GET' }); - }; +const getOne = function (this: method, orderId: string): Promise { + return this.request(`/v1/shops/${this.shopId}/orders/${orderId}.json`, { method: 'GET' }); +}; export default getOne; diff --git a/src/v1/orders/index.ts b/src/v1/orders/index.ts index ab65ef7..f754a37 100644 --- a/src/v1/orders/index.ts +++ b/src/v1/orders/index.ts @@ -1,4 +1,5 @@ -import { FetchDataFn } from '../../client'; +import { PrintifyConfig } from '../../client'; +import HttpClient from '../../http'; import list from './list'; import getOne from './getOne'; import submit from './submit'; @@ -7,33 +8,25 @@ import sendToProduction from './sendToProduction'; import calculateShipping from './calculateShipping'; import cancelUnpaid from './cancelUnpaid'; -export interface IOrders { - list: ReturnType; - getOne: ReturnType; - submit: ReturnType; - submitExpress: ReturnType; - sendToProduction: ReturnType; - calculateShipping: ReturnType; - cancelUnpaid: ReturnType; -} +class Orders extends HttpClient { + list: typeof list; + getOne: typeof getOne; + submit: typeof submit; + submitExpress: typeof submitExpress; + sendToProduction: typeof sendToProduction; + calculateShipping: typeof calculateShipping; + cancelUnpaid: typeof cancelUnpaid; -class Orders implements IOrders { - list: ReturnType; - getOne: ReturnType; - submit: ReturnType; - submitExpress: ReturnType; - sendToProduction: ReturnType; - calculateShipping: ReturnType; - cancelUnpaid: ReturnType; + constructor(config: PrintifyConfig) { + super(config); - constructor(fetchData: FetchDataFn, shopId: string) { - this.list = list(fetchData, shopId); - this.getOne = getOne(fetchData, shopId); - this.submit = submit(fetchData, shopId); - this.submitExpress = submitExpress(fetchData, shopId); - this.sendToProduction = sendToProduction(fetchData, shopId); - this.calculateShipping = calculateShipping(fetchData, shopId); - this.cancelUnpaid = cancelUnpaid(fetchData, shopId); + this.list = list.bind(this); + this.getOne = getOne.bind(this); + this.submit = submit.bind(this); + this.submitExpress = submitExpress.bind(this); + this.sendToProduction = sendToProduction.bind(this); + this.calculateShipping = calculateShipping.bind(this); + this.cancelUnpaid = cancelUnpaid.bind(this); } } diff --git a/src/v1/orders/list.ts b/src/v1/orders/list.ts index 68bc238..a507850 100644 --- a/src/v1/orders/list.ts +++ b/src/v1/orders/list.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Order } from '../types'; export interface ListOrdersResponse { @@ -23,18 +22,16 @@ export interface ListOrdersResponse { * printify.orders.list({ status: "fulfilled" }); * printify.orders.list({ sku: "168699843" }); */ -const list = - (fetchData: FetchDataFn, shopId: string) => - (options: { page?: number; limit?: number; status?: string; sku?: string } = {}): Promise => { - const { page, limit, status, sku } = options; - const queryParams = new URLSearchParams({ - ...(page !== undefined && { page: page.toString() }), - ...(limit !== undefined && { limit: limit.toString() }), - ...(status !== undefined && { status }), - ...(sku !== undefined && { sku }), - }).toString(); +const list = function (this: method, options: { page?: number; limit?: number; status?: string; sku?: string } = {}): Promise { + const { page, limit, status, sku } = options; + const queryParams = new URLSearchParams({ + ...(page !== undefined && { page: page.toString() }), + ...(limit !== undefined && { limit: limit.toString() }), + ...(status !== undefined && { status }), + ...(sku !== undefined && { sku }), + }).toString(); - return fetchData(`/v1/shops/${shopId}/orders.json${queryParams ? `?${queryParams}` : ''}`, { method: 'GET' }); - }; + return this.request(`/v1/shops/${this.shopId}/orders.json${queryParams ? `?${queryParams}` : ''}`, { method: 'GET' }); +}; export default list; diff --git a/src/v1/orders/sendToProduction.ts b/src/v1/orders/sendToProduction.ts index 4b3d1ca..843623e 100644 --- a/src/v1/orders/sendToProduction.ts +++ b/src/v1/orders/sendToProduction.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Address, LineItem } from '../types'; export type SendOrderToProductionResponse = { @@ -30,12 +29,10 @@ export type SendOrderToProductionResponse = { * await printify.orders.sendToProduction(orderId); * // Expected response: { id: "5d65c6ac01b403000a5d24d3", ... } */ -const sendToProduction = - (fetchData: FetchDataFn, shopId: string) => - (orderId: string): Promise => { - return fetchData(`/v1/shops/${shopId}/orders/${orderId}/send_to_production.json`, { - method: 'POST', - }); - }; +const sendToProduction = function (this: method, orderId: string): Promise { + return this.request(`/v1/shops/${this.shopId}/orders/${orderId}/send_to_production.json`, { + method: 'POST', + }); +}; export default sendToProduction; diff --git a/src/v1/orders/submit.ts b/src/v1/orders/submit.ts index a367c38..a36bd9c 100644 --- a/src/v1/orders/submit.ts +++ b/src/v1/orders/submit.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Address, NewLineItem } from '../types'; export interface SubmitOrderData { @@ -47,13 +46,11 @@ export type SubmitOrderResponse = { * const response = await printify.orders.submit(data); * // Expected response: { id: "5a96f649b2439217d070f507" } */ -const submit = - (fetchData: FetchDataFn, shopId: string) => - (data: SubmitOrderData): Promise => { - return fetchData(`/v1/shops/${shopId}/orders.json`, { - method: 'POST', - data: JSON.stringify(data), - }); - }; +const submit = function (this: method, data: SubmitOrderData): Promise { + return this.request(`/v1/shops/${this.shopId}/orders.json`, { + method: 'POST', + data: JSON.stringify(data), + }); +}; export default submit; diff --git a/src/v1/orders/submitExpress.ts b/src/v1/orders/submitExpress.ts index 835bad7..79cb79c 100644 --- a/src/v1/orders/submitExpress.ts +++ b/src/v1/orders/submitExpress.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Address, ExpressLineItem, LineItem } from '../types'; export interface SubmitExpressData { @@ -60,13 +59,11 @@ export type SubmitExpressOrderResponse = { * ] * } */ -const submitExpress = - (fetchData: FetchDataFn, shopId: string) => - (data: SubmitExpressData): Promise => { - return fetchData(`/v1/shops/${shopId}/express.json`, { - method: 'POST', - data: JSON.stringify(data), - }); - }; +const submitExpress = function (this: method, data: SubmitExpressData): Promise { + return this.request(`/v1/shops/${this.shopId}/express.json`, { + method: 'POST', + data: JSON.stringify(data), + }); +}; export default submitExpress; diff --git a/src/v1/products/create.ts b/src/v1/products/create.ts index 2f15bd9..fafc206 100644 --- a/src/v1/products/create.ts +++ b/src/v1/products/create.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { NewProduct, Product } from '../types'; export type CreateProductResponse = Product; @@ -45,13 +44,11 @@ export type CreateProductResponse = Product; * // "print_areas": [], * // } */ -const create = - (fetchData: FetchDataFn, shopId: string) => - (data: NewProduct): Promise => { - return fetchData(`/v1/shops/${shopId}/products.json`, { - method: 'POST', - data: JSON.stringify(data), - }); - }; +const create = function (this: method, data: NewProduct): Promise { + return this.request(`/v1/shops/${this.shopId}/products.json`, { + method: 'POST', + data: JSON.stringify(data), + }); +}; export default create; diff --git a/src/v1/products/deleteOne.ts b/src/v1/products/deleteOne.ts index 275e8a4..9cefcad 100644 --- a/src/v1/products/deleteOne.ts +++ b/src/v1/products/deleteOne.ts @@ -1,5 +1,3 @@ -import { FetchDataFn } from '../../client'; - /** * Delete a product * @@ -11,12 +9,10 @@ import { FetchDataFn } from '../../client'; * await printify.products.deleteOne(productId); * // Expected response: {} */ -const deleteOne = - (fetchData: FetchDataFn, shopId: string) => - (productId: string): Promise => { - return fetchData(`/v1/shops/${shopId}/products/${productId}.json`, { - method: 'DELETE', - }); - }; +const deleteOne = function (this: method, productId: string): Promise { + return this.request(`/v1/shops/${this.shopId}/products/${productId}.json`, { + method: 'DELETE', + }); +}; export default deleteOne; diff --git a/src/v1/products/getOne.ts b/src/v1/products/getOne.ts index b364cca..99cfacd 100644 --- a/src/v1/products/getOne.ts +++ b/src/v1/products/getOne.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Product } from '../types'; export type GetProductResponse = Product; @@ -28,12 +27,10 @@ export type GetProductResponse = Product; * "print_areas": [], * } */ -const getOne = - (fetchData: FetchDataFn, shopId: string) => - (productId: string): Promise => { - return fetchData(`/v1/shops/${shopId}/products/${productId}.json`, { - method: 'GET', - }); - }; +const getOne = function (this: method, productId: string): Promise { + return this.request(`/v1/shops/${this.shopId}/products/${productId}.json`, { + method: 'GET', + }); +}; export default getOne; diff --git a/src/v1/products/index.ts b/src/v1/products/index.ts index e6e77f7..46fc6b5 100644 --- a/src/v1/products/index.ts +++ b/src/v1/products/index.ts @@ -1,4 +1,5 @@ -import { FetchDataFn } from '../../client'; +import { PrintifyConfig } from '../../client'; +import HttpClient from '../../http'; import create from './create'; import deleteOne from './deleteOne'; import getOne from './getOne'; @@ -9,39 +10,29 @@ import setPublishFailed from './setPublishFailed'; import setPublishSucceeded from './setPublishSucceeded'; import updateOne from './updateOne'; -export interface IProducts { - create: ReturnType; - deleteOne: ReturnType; - getOne: ReturnType; - list: ReturnType; - notifyUnpublished: ReturnType; - publishOne: ReturnType; - setPublishFailed: ReturnType; - setPublishSucceeded: ReturnType; - updateOne: ReturnType; -} +class Products extends HttpClient { + create: typeof create; + deleteOne: typeof deleteOne; + getOne: typeof getOne; + list: typeof list; + notifyUnpublished: typeof notifyUnpublished; + publishOne: typeof publishOne; + setPublishFailed: typeof setPublishFailed; + setPublishSucceeded: typeof setPublishSucceeded; + updateOne: typeof updateOne; -class Products implements IProducts { - create: ReturnType; - deleteOne: ReturnType; - getOne: ReturnType; - list: ReturnType; - notifyUnpublished: ReturnType; - publishOne: ReturnType; - setPublishFailed: ReturnType; - setPublishSucceeded: ReturnType; - updateOne: ReturnType; + constructor(config: PrintifyConfig) { + super(config); - constructor(fetchData: FetchDataFn, shopId: string) { - this.create = create(fetchData, shopId); - this.deleteOne = deleteOne(fetchData, shopId); - this.getOne = getOne(fetchData, shopId); - this.list = list(fetchData, shopId); - this.notifyUnpublished = notifyUnpublished(fetchData, shopId); - this.publishOne = publishOne(fetchData, shopId); - this.setPublishFailed = setPublishFailed(fetchData, shopId); - this.setPublishSucceeded = setPublishSucceeded(fetchData, shopId); - this.updateOne = updateOne(fetchData, shopId); + this.create = create.bind(this); + this.deleteOne = deleteOne.bind(this); + this.getOne = getOne.bind(this); + this.list = list.bind(this); + this.notifyUnpublished = notifyUnpublished.bind(this); + this.publishOne = publishOne.bind(this); + this.setPublishFailed = setPublishFailed.bind(this); + this.setPublishSucceeded = setPublishSucceeded.bind(this); + this.updateOne = updateOne.bind(this); } } diff --git a/src/v1/products/list.ts b/src/v1/products/list.ts index 58edf3e..6fe02b5 100644 --- a/src/v1/products/list.ts +++ b/src/v1/products/list.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Product } from '../types'; export type ListProductsResponse = { @@ -34,19 +33,17 @@ export type ListProductsResponse = { * // "total": 22 * // } */ -const list = - (fetchData: FetchDataFn, shopId: string) => - (options: { page?: number; limit?: number } = {}): Promise => { - const { page, limit } = options; - const queryParams = new URLSearchParams({ - ...(page !== undefined && { page: page.toString() }), - ...(limit !== undefined && { limit: limit.toString() }), - }).toString(); +const list = function (this: method, options: { page?: number; limit?: number } = {}): Promise { + const { page, limit } = options; + const queryParams = new URLSearchParams({ + ...(page !== undefined && { page: page.toString() }), + ...(limit !== undefined && { limit: limit.toString() }), + }).toString(); - const query = queryParams.toString() ? `?${queryParams.toString()}` : ''; - return fetchData(`/v1/shops/${shopId}/products.json${query}`, { - method: 'GET', - }); - }; + const query = queryParams.toString() ? `?${queryParams.toString()}` : ''; + return this.request(`/v1/shops/${this.shopId}/products.json${query}`, { + method: 'GET', + }); +}; export default list; diff --git a/src/v1/products/notifyUnpublished.ts b/src/v1/products/notifyUnpublished.ts index 9f47ca8..7eae0de 100644 --- a/src/v1/products/notifyUnpublished.ts +++ b/src/v1/products/notifyUnpublished.ts @@ -1,5 +1,3 @@ -import { FetchDataFn } from '../../client'; - /** - Notify that a product has been unpublished - @@ -11,12 +9,10 @@ import { FetchDataFn } from '../../client'; - await printify.products.notifyUnpublished(productId); - // Expected response: {} */ -const notifyUnpublished = - (fetchData: FetchDataFn, shopId: string) => - (productId: string): Promise => { - return fetchData(`/v1/shops/${shopId}/products/${productId}/unpublish.json`, { - method: 'POST', - }); - }; +const notifyUnpublished = function (this: method, productId: string): Promise { + return this.request(`/v1/shops/${this.shopId}/products/${productId}/unpublish.json`, { + method: 'POST', + }); +}; export default notifyUnpublished; diff --git a/src/v1/products/publishOne.ts b/src/v1/products/publishOne.ts index 2f45ffd..c236255 100644 --- a/src/v1/products/publishOne.ts +++ b/src/v1/products/publishOne.ts @@ -1,5 +1,3 @@ -import { FetchDataFn } from '../../client'; - export interface ProductPublishData { title: boolean; description: boolean; @@ -22,13 +20,11 @@ export interface ProductPublishData { * await printify.products.publishOne('productId', data); * // Expected response: {} */ -const publishOne = - (fetchData: FetchDataFn, shopId: string) => - (productId: string, data: ProductPublishData): Promise => { - return fetchData(`/v1/shops/${shopId}/products/${productId}/publish.json`, { - method: 'POST', - data: JSON.stringify(data), - }); - }; +const publishOne = function (this: method, productId: string, data: ProductPublishData): Promise { + return this.request(`/v1/shops/${this.shopId}/products/${productId}/publish.json`, { + method: 'POST', + data: JSON.stringify(data), + }); +}; export default publishOne; diff --git a/src/v1/products/setPublishFailed.ts b/src/v1/products/setPublishFailed.ts index 965baa7..607eaca 100644 --- a/src/v1/products/setPublishFailed.ts +++ b/src/v1/products/setPublishFailed.ts @@ -1,5 +1,3 @@ -import { FetchDataFn } from '../../client'; - export type PublishFailedData = { reason: string; }; @@ -16,13 +14,11 @@ export type PublishFailedData = { - await printify.products.setPublishFailed('productId', data); - // Expected response: {} */ -const setPublishFailed = - (fetchData: FetchDataFn, shopId: string) => - async (productId: string, data: PublishFailedData): Promise => { - return fetchData(`/v1/shops/${shopId}/products/${productId}/publishing_failed.json`, { - method: 'POST', - data: JSON.stringify(data), - }); - }; +const setPublishFailed = function (this: method, productId: string, data: PublishFailedData): Promise { + return this.request(`/v1/shops/${this.shopId}/products/${productId}/publishing_failed.json`, { + method: 'POST', + data: JSON.stringify(data), + }); +}; export default setPublishFailed; diff --git a/src/v1/products/setPublishSucceeded.ts b/src/v1/products/setPublishSucceeded.ts index 962ccc3..236c73f 100644 --- a/src/v1/products/setPublishSucceeded.ts +++ b/src/v1/products/setPublishSucceeded.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { ExternalProductData } from '../types'; export type PublishSucceededData = { @@ -17,13 +16,11 @@ export type PublishSucceededData = { - await printify.products.setPublishSucceeded('productId', data); - // Expected response: {} */ -const setPublishSucceeded = - (fetchData: FetchDataFn, shopId: string) => - async (productId: string, data: PublishSucceededData): Promise => { - return fetchData(`/v1/shops/${shopId}/products/${productId}/publishing_succeeded.json`, { - method: 'POST', - data: JSON.stringify(data), - }); - }; +const setPublishSucceeded = function (this: method, productId: string, data: PublishSucceededData): Promise { + return this.request(`/v1/shops/${this.shopId}/products/${productId}/publishing_succeeded.json`, { + method: 'POST', + data: JSON.stringify(data), + }); +}; export default setPublishSucceeded; diff --git a/src/v1/products/updateOne.ts b/src/v1/products/updateOne.ts index 4ebe825..14bfcae 100644 --- a/src/v1/products/updateOne.ts +++ b/src/v1/products/updateOne.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Product, UpdateProductData } from '../types'; export type UpdateProductResponse = Product; @@ -30,13 +29,11 @@ export type UpdateProductResponse = Product; * // } */ -const updateOne = - (fetchData: FetchDataFn, shopId: string) => - (productId: string, data: UpdateProductData): Promise => { - return fetchData(`/v1/shops/${shopId}/products/${productId}.json`, { - method: 'PUT', - data: JSON.stringify(data), - }); - }; +const updateOne = function (this: method, productId: string, data: UpdateProductData): Promise { + return this.request(`/v1/shops/${this.shopId}/products/${productId}.json`, { + method: 'PUT', + data: JSON.stringify(data), + }); +}; export default updateOne; diff --git a/src/v1/shops/deleteOne.ts b/src/v1/shops/deleteOne.ts index fbc0e81..b4ffa64 100644 --- a/src/v1/shops/deleteOne.ts +++ b/src/v1/shops/deleteOne.ts @@ -10,7 +10,7 @@ * // Expected response: {} */ const deleteOne = function (this: method, customShopId?: string): Promise { - return this.request(`/v1/shops/${customShopId || this.shopId}/connection.json`, { + return this.request(`/v1/shops/${customShopId ?? this.shopId}/connection.json`, { method: 'DELETE', }); }; diff --git a/src/v1/uploads/archive.ts b/src/v1/uploads/archive.ts index 6ca3ad1..ec2e34b 100644 --- a/src/v1/uploads/archive.ts +++ b/src/v1/uploads/archive.ts @@ -1,5 +1,3 @@ -import { FetchDataFn } from '../../client'; - /** * Archive an uploaded image * @@ -11,10 +9,8 @@ import { FetchDataFn } from '../../client'; * await printify.uploads.archive(imageId); * // Expected response: {} */ -const archive = - (fetchData: FetchDataFn) => - (imageId: string): Promise => { - return fetchData(`/v1/uploads/${imageId}/archive.json`, { method: 'POST' }); - }; +const archive = function (this: method, imageId: string): Promise { + return this.request(`/v1/uploads/${imageId}/archive.json`, { method: 'POST' }); +}; export default archive; diff --git a/src/v1/uploads/getById.ts b/src/v1/uploads/getById.ts index e5afec4..19b8225 100644 --- a/src/v1/uploads/getById.ts +++ b/src/v1/uploads/getById.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { ImageUpload } from '../types'; export type GetUploadByIdResponse = ImageUpload; @@ -24,10 +23,8 @@ export type GetUploadByIdResponse = ImageUpload; * // upload_time: "2020-01-09 07:29:43" * // } */ -const getById = - (fetchData: FetchDataFn) => - (imageId: string): Promise => { - return fetchData(`/v1/uploads/${imageId}.json`, { method: 'GET' }); - }; +const getById = function (this: method, imageId: string): Promise { + return this.request(`/v1/uploads/${imageId}.json`, { method: 'GET' }); +}; export default getById; diff --git a/src/v1/uploads/index.ts b/src/v1/uploads/index.ts index d6a83d4..d0ae938 100644 --- a/src/v1/uploads/index.ts +++ b/src/v1/uploads/index.ts @@ -1,27 +1,23 @@ -import { FetchDataFn } from '../../client'; +import { PrintifyConfig } from '../../client'; +import HttpClient from '../../http'; import archive from './archive'; import getById from './getById'; import list from './list'; import uploadImage from './uploadImage'; -export interface IUploads { - archive: ReturnType; - getById: ReturnType; - list: ReturnType; - uploadImage: ReturnType; -} +class Uploads extends HttpClient { + archive: typeof archive; + getById: typeof getById; + list: typeof list; + uploadImage: typeof uploadImage; -class Uploads implements IUploads { - archive: ReturnType; - getById: ReturnType; - list: ReturnType; - uploadImage: ReturnType; + constructor(config: PrintifyConfig) { + super(config); - constructor(fetchData: FetchDataFn, shopId: string) { - this.archive = archive(fetchData); - this.getById = getById(fetchData); - this.list = list(fetchData); - this.uploadImage = uploadImage(fetchData); + this.archive = archive.bind(this); + this.getById = getById.bind(this); + this.list = list.bind(this); + this.uploadImage = uploadImage.bind(this); } } diff --git a/src/v1/uploads/list.ts b/src/v1/uploads/list.ts index 90b4eae..3ee07a0 100644 --- a/src/v1/uploads/list.ts +++ b/src/v1/uploads/list.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { ImageUpload } from '../types'; export type ListUploadsResponse = { @@ -35,15 +34,13 @@ export type ListUploadsResponse = { * // "total": 2 * // } */ -const list = - (fetchData: FetchDataFn) => - (page?: number, limit?: number): Promise => { - const queryParams = new URLSearchParams(); - if (page !== undefined) queryParams.append('page', page.toString()); - if (limit !== undefined) queryParams.append('limit', limit.toString()); +const list = function (this: method, page?: number, limit?: number): Promise { + const queryParams = new URLSearchParams(); + if (page !== undefined) queryParams.append('page', page.toString()); + if (limit !== undefined) queryParams.append('limit', limit.toString()); - const url = `/v1/uploads.json${queryParams.toString() ? `?${queryParams.toString()}` : ''}`; - return fetchData(url, { method: 'GET' }); - }; + const url = `/v1/uploads.json${queryParams.toString() ? `?${queryParams.toString()}` : ''}`; + return this.request(url, { method: 'GET' }); +}; export default list; diff --git a/src/v1/uploads/uploadImage.ts b/src/v1/uploads/uploadImage.ts index 081f0a9..73ecb0e 100644 --- a/src/v1/uploads/uploadImage.ts +++ b/src/v1/uploads/uploadImage.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { ImageUpload } from '../types'; export interface UploadImageDataUrl { @@ -26,13 +25,11 @@ export type UploadImageResponse = ImageUpload; * const dataBase64 = { file_name: 'image.png', contents: '' }; * const response = await printify.uploads.uploadImage(dataBase64); */ -const uploadImage = - (fetchData: FetchDataFn) => - (data: UploadImageDataUrl | UploadImageDataBase64): Promise => { - return fetchData(`/v1/uploads/images.json`, { - method: 'POST', - data: JSON.stringify(data), - }); - }; +const uploadImage = function (this: method, data: UploadImageDataUrl | UploadImageDataBase64): Promise { + return this.request(`/v1/uploads/images.json`, { + method: 'POST', + data: JSON.stringify(data), + }); +}; export default uploadImage; diff --git a/src/v1/webhooks/create.ts b/src/v1/webhooks/create.ts index e84b177..be3c409 100644 --- a/src/v1/webhooks/create.ts +++ b/src/v1/webhooks/create.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { NewWebhook } from '../types'; export type CreateWebhookResponse = { @@ -25,13 +24,11 @@ export type CreateWebhookResponse = { * // "id": "5cb87a8cd490a2ccb256cec4" * // } */ -const create = - (fetchData: FetchDataFn, shopId: string) => - (data: NewWebhook): Promise => { - return fetchData(`/v1/shops/${shopId}/webhooks.json`, { - method: 'POST', - data: JSON.stringify(data), - }); - }; +const create = function (this: method, data: NewWebhook): Promise { + return this.request(`/v1/shops/${this.shopId}/webhooks.json`, { + method: 'POST', + data: JSON.stringify(data), + }); +}; export default create; diff --git a/src/v1/webhooks/deleteOne.ts b/src/v1/webhooks/deleteOne.ts index 62cada9..86a0e31 100644 --- a/src/v1/webhooks/deleteOne.ts +++ b/src/v1/webhooks/deleteOne.ts @@ -1,5 +1,3 @@ -import { FetchDataFn } from '../../client'; - export type DeleteWebhookResponse = { id: string; }; @@ -17,12 +15,10 @@ export type DeleteWebhookResponse = { * // "id": "5cb87a8cd490a2ccb256cec4" * // } */ -const deleteOne = - (fetchData: FetchDataFn, shopId: string) => - (webhookId: string): Promise => { - return fetchData(`/v1/shops/${shopId}/webhooks/${webhookId}.json`, { - method: 'DELETE', - }); - }; +const deleteOne = function (this: method, webhookId: string): Promise { + return this.request(`/v1/shops/${this.shopId}/webhooks/${webhookId}.json`, { + method: 'DELETE', + }); +}; export default deleteOne; diff --git a/src/v1/webhooks/index.ts b/src/v1/webhooks/index.ts index 1e0d311..ae2b23a 100644 --- a/src/v1/webhooks/index.ts +++ b/src/v1/webhooks/index.ts @@ -1,27 +1,23 @@ -import { FetchDataFn } from '../../client'; +import { PrintifyConfig } from '../../client'; +import HttpClient from '../../http'; import list from './list'; import create from './create'; import updateOne from './updateOne'; import deleteOne from './deleteOne'; -export interface IWebhooks { - list: ReturnType; - create: ReturnType; - updateOne: ReturnType; - deleteOne: ReturnType; -} +class Webhooks extends HttpClient { + list: typeof list; + create: typeof create; + updateOne: typeof updateOne; + deleteOne: typeof deleteOne; -class Webhooks implements IWebhooks { - list: ReturnType; - create: ReturnType; - updateOne: ReturnType; - deleteOne: ReturnType; + constructor(config: PrintifyConfig) { + super(config); - constructor(fetchData: FetchDataFn, shopId: string) { - this.list = list(fetchData, shopId); - this.create = create(fetchData, shopId); - this.updateOne = updateOne(fetchData, shopId); - this.deleteOne = deleteOne(fetchData, shopId); + this.list = list.bind(this); + this.create = create.bind(this); + this.updateOne = updateOne.bind(this); + this.deleteOne = deleteOne.bind(this); } } diff --git a/src/v1/webhooks/list.ts b/src/v1/webhooks/list.ts index a39f3d7..6a14c11 100644 --- a/src/v1/webhooks/list.ts +++ b/src/v1/webhooks/list.ts @@ -1,4 +1,3 @@ -import { FetchDataFn } from '../../client'; import { Webhook } from '../types'; export type ListWebhooksResponse = Webhook[]; @@ -16,8 +15,8 @@ export type ListWebhooksResponse = Webhook[]; * // { "topic": "order:updated", "url": "https://example.com/webhooks/order/updated", "shop_id": "1", "id": "5cb87a8cd490a2ccb256cec5" } * // ] */ -const list = (fetchData: FetchDataFn, shopId: string) => (): Promise => { - return fetchData(`/v1/shops/${shopId}/webhooks.json`, { +const list = function (this: method): Promise { + return this.request(`/v1/shops/${this.shopId}/webhooks.json`, { method: 'GET', }); }; diff --git a/src/v1/webhooks/updateOne.ts b/src/v1/webhooks/updateOne.ts index 4867136..ab99e8d 100644 --- a/src/v1/webhooks/updateOne.ts +++ b/src/v1/webhooks/updateOne.ts @@ -1,5 +1,3 @@ -import { FetchDataFn } from '../../client'; - export type UpdateWebhookResponse = { topic: string; url: string; @@ -25,13 +23,11 @@ export type UpdateWebhookResponse = { * // "id": "5cb87a8cd490a2ccb256cec4" * // } */ -const updateOne = - (fetchData: FetchDataFn, shopId: string) => - (webhookId: string, data: { url: string }): Promise => { - return fetchData(`/v1/shops/${shopId}/webhooks/${webhookId}.json`, { - method: 'PUT', - data: JSON.stringify(data), - }); - }; +const updateOne = function (this: method, webhookId: string, data: { url: string }): Promise { + return this.request(`/v1/shops/${this.shopId}/webhooks/${webhookId}.json`, { + method: 'PUT', + data: JSON.stringify(data), + }); +}; export default updateOne; diff --git a/src/v2/catalog/getShippingListInfo.ts b/src/v2/catalog/getShippingListInfo.ts new file mode 100644 index 0000000..bea792e --- /dev/null +++ b/src/v2/catalog/getShippingListInfo.ts @@ -0,0 +1,56 @@ +export interface ShippingInfo { + type: string; + id: string; + attributes: { + name: string; + }; +} + +export type GetShippingListInfoResponse = ShippingInfo[]; + +/** + * Retrieve available shipping list information + * + * @param {string} blueprintId - The ID of the blueprint + * @param {string} printProviderId - The ID of the print provider + * @returns {Promise} + * + * @example + * await printify.v2.catalog.getShippingListInfo('3', '8'); + * // Expected response: [ + * // [ + * // { + * // "type": "shipping_method", + * // "id": "1", + * // "attributes": { + * // "name": "standard" + * // } + * // }, + * // { + * // "type": "shipping_method", + * // "id": "2", + * // "attributes": { + * // "name": "priority" + * // } + * // }, + * // { + * // "type": "shipping_method", + * // "id": "3", + * // "attributes": { + * // "name": "express" + * // } + * // }, + * // { + * // "type": "shipping_method", + * // "id": "4", + * // "attributes": { + * // "name": "economy" + * // } + * // } + * // ] + */ +const getShippingListInfo = function (this: method, blueprintId: string, printProviderId: string): Promise { + return this.request(`/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping.json`, { method: 'GET' }); +}; + +export default getShippingListInfo; diff --git a/src/v2/catalog/index.ts b/src/v2/catalog/index.ts index d632a11..1824ea7 100644 --- a/src/v2/catalog/index.ts +++ b/src/v2/catalog/index.ts @@ -1,16 +1,13 @@ -// import { FetchDataFn } from '../../client'; -// import getSomething from './getSomething'; +import { PrintifyConfig } from '../../client'; +import HttpClient from '../../http'; +import getShippingListInfo from './getShippingListInfo'; -// TODO v1.3.0 -export interface ICatalogV2 { - // getSomething: ReturnType; -} - -class CatalogV2 implements ICatalogV2 { - // getSomething: ReturnType; +class CatalogV2 extends HttpClient { + getShippingListInfo: typeof getShippingListInfo; - constructor() { - // this.getSomething = getSomething(fetchData); + constructor(config: PrintifyConfig) { + super(config); + this.getShippingListInfo = getShippingListInfo.bind(this); } } diff --git a/tests/catalog.test.ts b/tests/catalog.test.ts index f49b411..fb53b74 100644 --- a/tests/catalog.test.ts +++ b/tests/catalog.test.ts @@ -1,7 +1,7 @@ import Printify from '../src/client'; import { assertAxiosCall } from './testUtils'; -describe('Catalog', () => { +describe('Catalog V1', () => { let printify: Printify; beforeAll(() => { @@ -74,4 +74,19 @@ describe('Catalog', () => { describe('Catalog V2', () => { // TODO v1.3.0 + let printify: Printify; + + beforeAll(() => { + printify = new Printify({ shopId: '123456', accessToken: 'mockAccessToken' }); + }); + + it('should handle the getShippingListInfo endpoint', async () => { + // Act + const blueprintId = '3'; + const printProviderId = '8'; + await printify.v2.catalog.getShippingListInfo(blueprintId, printProviderId); + + // Assert + assertAxiosCall('get', `/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping.json`); + }); }); diff --git a/tests/http.test.ts b/tests/http.test.ts new file mode 100644 index 0000000..a8abad3 --- /dev/null +++ b/tests/http.test.ts @@ -0,0 +1,169 @@ +import axios from 'axios'; +import { assertAxiosCall } from './testUtils'; +import HttpClient from '../src/http'; + +jest.mock('axios', () => ({ + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), + patch: jest.fn(), +})); + +describe('HttpClient', () => { + const shopId = 'testShopId'; + const accessToken = 'mockAccessToken'; + + beforeAll(() => { + global.console.log = jest.fn(); + global.console.error = jest.fn(); + }); + + beforeEach(() => { + (axios.get as jest.Mock).mockReset(); + (axios.post as jest.Mock).mockReset(); + (axios.put as jest.Mock).mockReset(); + (axios.delete as jest.Mock).mockReset(); + (axios.patch as jest.Mock).mockReset(); + (global.console.log as jest.Mock).mockReset(); + (global.console.error as jest.Mock).mockReset(); + }); + + it('should initialize with provided config', () => { + const http: HttpClient = new HttpClient({ shopId, accessToken }); + + expect(http.shopId).toBe(shopId); + expect(typeof http.request).toBe('function'); + }); + + it('request() should handle successful response', async () => { + const http: HttpClient = new HttpClient({ shopId, accessToken }); + + const mockResponse = { success: 'true' }; + const mockedAxios = axios as jest.Mocked; + mockedAxios.get.mockResolvedValueOnce({ data: mockResponse }); + + const url = '/test-url'; + const result = await http.request(url); + + expect(result).toEqual(mockResponse); + assertAxiosCall('get', url); + }); + + it('request() should throw error for failed response', async () => { + const http: HttpClient = new HttpClient({ shopId, accessToken }); + + (axios as unknown as jest.Mock).mockResolvedValueOnce({ + response: { + status: 404, + statusText: 'Not Found', + }, + }); + + const url = '/test-url'; + + await expect(http.request(url)).rejects.toThrow('Printify SDK Error'); + }); + + it('request() should rethrow errors from axios', async () => { + const http: HttpClient = new HttpClient({ shopId, accessToken }); + + const errorMessage = 'Network Error'; + (axios as unknown as jest.Mock).mockRejectedValueOnce(new Error(errorMessage)); + + const url = '/test-url'; + + await expect(http.request(url)).rejects.toThrow('Printify SDK Error'); + }); + + it('should log the request by default', async () => { + const http: HttpClient = new HttpClient({ shopId, accessToken }); + + const method = 'GET'; + const url = '/test-url'; + + (axios.get as jest.Mock).mockResolvedValueOnce({ response: { status: 200, statusText: 'Hello, world!' } }); + await http.request(url); + + expect(console.log).toHaveBeenCalledWith(`Requesting ${method.toUpperCase()} https://${http['host']}${url}`); + }); + + it('should log the request when enableLogging is true', async () => { + const http: HttpClient = new HttpClient({ shopId, accessToken, enableLogging: true }); + + const method = 'GET'; + const url = '/test-url'; + + (axios.get as jest.Mock).mockResolvedValueOnce({ response: { status: 200, statusText: 'Hello, world!' } }); + await http.request(url); + + expect(console.log).toHaveBeenCalledWith(`Requesting ${method.toUpperCase()} https://${http['host']}${url}`); + }); + + it('should not log the request when enableLogging is false', async () => { + const http: HttpClient = new HttpClient({ shopId, accessToken, enableLogging: false }); + const url = '/test-url'; + + (axios.get as jest.Mock).mockResolvedValueOnce({ response: { status: 200, statusText: 'Hello, world!' } }); + await http.request(url); + + expect(console.log).not.toHaveBeenCalled(); + }); + + it('should log the error when enableLogging is default', async () => { + const http: HttpClient = new HttpClient({ shopId, accessToken }); + (axios.get as jest.Mock).mockRejectedValueOnce({ response: { status: 500, statusText: 'Server Error' } }); + + try { + await http.request('/test-url'); + } catch (error) { + expect(console.error).toHaveBeenCalledWith(`Printify SDK Error`); + } + }); + + it('should log the error when enableLogging is true', async () => { + const http: HttpClient = new HttpClient({ shopId, accessToken, enableLogging: true }); + (axios.get as jest.Mock).mockRejectedValueOnce({ response: { status: 500, statusText: 'Server Error' } }); + + try { + await http.request('/test-url'); + } catch (error) { + expect(console.error).toHaveBeenCalledWith(`Printify SDK Error`); + } + }); + + it('should not log the error when enableLogging is false', async () => { + const http: HttpClient = new HttpClient({ shopId, accessToken, enableLogging: false }); + + (axios.get as jest.Mock).mockRejectedValueOnce({ response: { status: 500, statusText: 'Server Error' } }); + + try { + await http.request('/test-url'); + } catch (error) { + expect(console.error).not.toHaveBeenCalled(); + } + }); + + it('should log error details when AxiosError is thrown', async () => { + const http: HttpClient = new HttpClient({ shopId, accessToken }); + + const axiosError = new Error('Request failed') as any; + axiosError.isAxiosError = true; + axiosError.toJSON = () => {}; + axiosError.response = { + status: 404, + statusText: 'Not Found', + data: {}, + }; + axiosError.config = {}; + axiosError.code = 'ERR_BAD_REQUEST'; + + (axios.get as jest.Mock).mockRejectedValueOnce(axiosError); + + const url = '/test-url'; + + await expect(http.request(url)).rejects.toThrow('Printify SDK Error: 404 Not Found - Requested URL: https://' + http['host'] + url); + + expect(console.error).toHaveBeenCalledWith(`Printify SDK Error: 404 Not Found - Requested URL: https://${http['host']}${url}`); + }); +}); diff --git a/tests/orders.test.ts b/tests/orders.test.ts index 4c1dc53..730b846 100644 --- a/tests/orders.test.ts +++ b/tests/orders.test.ts @@ -1,7 +1,7 @@ import Printify from '../src/client'; import { assertAxiosCall } from './testUtils'; -describe('Orders', () => { +describe('Orders V1', () => { let printify: Printify; beforeAll(() => { diff --git a/tests/printify.test.ts b/tests/printify.test.ts index 7d132e5..80929df 100644 --- a/tests/printify.test.ts +++ b/tests/printify.test.ts @@ -1,68 +1,81 @@ -import axios from 'axios'; -import Printify from '../src/client'; -import { assertAxiosCall } from './testUtils'; +// @ts-nocheck +import Printify from '../src/index'; +import Catalog from '../src/v1/catalog'; +import Orders from '../src/v1/orders'; +import Products from '../src/v1/products'; +import Shops from '../src/v1/shops'; +import Uploads from '../src/v1/uploads'; +import Webhooks from '../src/v1/webhooks'; -jest.mock('axios', () => ({ - get: jest.fn(), - post: jest.fn(), - put: jest.fn(), - delete: jest.fn(), - patch: jest.fn(), -})); +jest.mock('../src/v1/catalog'); +jest.mock('../src/v1/orders'); +jest.mock('../src/v1/products'); +jest.mock('../src/v1/shops'); +jest.mock('../src/v1/uploads'); +jest.mock('../src/v1/webhooks'); -describe('Printify', () => { +describe('PrintifyClient', () => { let printify: Printify; const shopId = 'testShopId'; const accessToken = 'mockAccessToken'; + const config = { accessToken, shopId }; beforeEach(() => { - (axios.get as jest.Mock).mockReset(); - (axios.post as jest.Mock).mockReset(); - (axios.put as jest.Mock).mockReset(); - (axios.delete as jest.Mock).mockReset(); - (axios.patch as jest.Mock).mockReset(); - - printify = new Printify({ shopId, accessToken }); + printify = new Printify(config); }); it('should initialize with provided config', () => { expect(printify.shopId).toBe(shopId); + expect(printify.catalog).toBeInstanceOf(Catalog); + expect(printify.orders).toBeInstanceOf(Orders); + expect(printify.products).toBeInstanceOf(Products); + expect(printify.shops).toBeInstanceOf(Shops); + expect(printify.uploads).toBeInstanceOf(Uploads); + expect(printify.webhooks).toBeInstanceOf(Webhooks); }); - it('fetchData should handle successful response', async () => { - const mockResponse = { success: 'true' }; - // Mock a successful axios response - const mockedAxios = axios as jest.Mocked; - mockedAxios.get.mockResolvedValueOnce({ data: mockResponse }); + it('should initialize without shopId if not provided', () => { + const printifyWithoutShopId = new Printify({ accessToken }); + expect(printifyWithoutShopId.shopId).toBeUndefined(); + }); - const url = '/test-url'; - const result = await printify['fetchData'](url); + it('should not allow direct access to accessToken', () => { + expect(printify.accessToken).toBeUndefined(); + expect(printify.shops.accessToken).toBeUndefined(); + }); - expect(result).toEqual(mockResponse); - assertAxiosCall('get', url); + it('should initialize catalog with the correct config', () => { + expect(printify.catalog).toBeInstanceOf(Catalog); + expect(Catalog).toHaveBeenCalledWith(config); }); - it('fetchData should throw error for failed response', async () => { - // Mock a failed axios response (error) - (axios as unknown as jest.Mock).mockResolvedValueOnce({ - response: { - status: 404, - statusText: 'Not Found', - }, - }); + it('should initialize orders with the correct config', () => { + expect(printify.orders).toBeInstanceOf(Orders); + expect(Orders).toHaveBeenCalledWith(config); + }); - const url = '/test-url'; + it('should initialize products with the correct config', () => { + expect(printify.products).toBeInstanceOf(Products); + expect(Products).toHaveBeenCalledWith(config); + }); - await expect(printify['fetchData'](url)).rejects.toThrow('Printify SDK Unknown Error'); + it('should initialize shops with the correct config', () => { + expect(printify.shops).toBeInstanceOf(Shops); + expect(Shops).toHaveBeenCalledWith(config); }); - it('fetchData should rethrow errors from axios', async () => { - const errorMessage = 'Network Error'; - // Mock axios to throw an error - (axios as unknown as jest.Mock).mockRejectedValueOnce(new Error(errorMessage)); + it('should initialize uploads with the correct config', () => { + expect(printify.uploads).toBeInstanceOf(Uploads); + expect(Uploads).toHaveBeenCalledWith(config); + }); - const url = '/test-url'; + it('should initialize webhooks with the correct config', () => { + expect(printify.webhooks).toBeInstanceOf(Webhooks); + expect(Webhooks).toHaveBeenCalledWith(config); + }); - await expect(printify['fetchData'](url)).rejects.toThrow('Printify SDK Unknown Error'); + it('should handle missing accessToken gracefully', () => { + const printifyMissingToken = () => new Printify({ shopId }); + expect(printifyMissingToken).toThrowError('accessToken is required'); }); }); diff --git a/tests/products.test.ts b/tests/products.test.ts index b4d8ca9..af2cd68 100644 --- a/tests/products.test.ts +++ b/tests/products.test.ts @@ -1,7 +1,7 @@ import Printify from '../src/client'; import { assertAxiosCall } from './testUtils'; -describe('Products', () => { +describe('Products V1', () => { let printify: Printify; beforeAll(() => { diff --git a/tests/shops.test.ts b/tests/shops.test.ts index 46df31a..897ade1 100644 --- a/tests/shops.test.ts +++ b/tests/shops.test.ts @@ -1,7 +1,7 @@ import Printify from '../src/client'; import { assertAxiosCall } from './testUtils'; -describe('Shops', () => { +describe('Shops V1', () => { let printify: Printify; beforeAll(() => { diff --git a/tests/uploads.test.ts b/tests/uploads.test.ts index 2bb3466..ebcc1e3 100644 --- a/tests/uploads.test.ts +++ b/tests/uploads.test.ts @@ -1,7 +1,7 @@ import Printify from '../src/client'; import { assertAxiosCall } from './testUtils'; -describe('Uploads', () => { +describe('Uploads V1', () => { let printify: Printify; beforeAll(() => { diff --git a/tests/webhooks.test.ts b/tests/webhooks.test.ts index c769109..6f47d32 100644 --- a/tests/webhooks.test.ts +++ b/tests/webhooks.test.ts @@ -1,7 +1,7 @@ import Printify from '../src/client'; import { assertAxiosCall } from './testUtils'; -describe('Webhooks', () => { +describe('Webhooks V1', () => { let printify: Printify; beforeAll(() => { From c63bb4dafa9bcff6bda4fa3d8a1abcbfcede9249 Mon Sep 17 00:00:00 2001 From: Spencer Lepine <60903378+spencerlepine@users.noreply.github.com> Date: Sun, 5 Jan 2025 11:58:01 +0000 Subject: [PATCH 3/7] add v2 endpoints for beta testing --- CHANGELOG.md | 5 + docs/API.md | 122 ++++++++++++++++++++++ examples/development/.gitignore | 1 - src/v2/catalog/getEconomyShippingInfo.ts | 23 ++++ src/v2/catalog/getExpressShippingInfo.ts | 23 ++++ src/v2/catalog/getPriorityShippingInfo.ts | 23 ++++ src/v2/catalog/getShippingListInfo.ts | 1 - src/v2/catalog/getStandardShippingInfo.ts | 47 +++++++++ src/v2/catalog/index.ts | 12 +++ src/v2/types.ts | 28 ++++- tests/catalog.test.ts | 41 +++++++- 11 files changed, 320 insertions(+), 6 deletions(-) delete mode 100644 examples/development/.gitignore create mode 100644 src/v2/catalog/getEconomyShippingInfo.ts create mode 100644 src/v2/catalog/getExpressShippingInfo.ts create mode 100644 src/v2/catalog/getPriorityShippingInfo.ts create mode 100644 src/v2/catalog/getStandardShippingInfo.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e499a9e..f4c66b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.0] - 2025-01-XX + +- TODO v1.3.0 +- Added support for V2 endpoints + ## [1.2.0] - 2025-01-03 - Organized shared types and fixed bundle export diff --git a/docs/API.md b/docs/API.md index 87d3bb7..3930407 100644 --- a/docs/API.md +++ b/docs/API.md @@ -2,6 +2,7 @@ - [Shops](#shops) - [Catalog](#catalog) +- [Catalog V2](#catalog-v2) - [Products](#products) - [Orders](#orders) - [Uploads](#uploads) @@ -468,6 +469,127 @@ await printify.catalog.getProvider(printProviderId); +### Catalog V2 + +#### `printify.v2.catalog.getShippingListInfo()` + +- `GET /v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping.json` +- **Description:** Retrieve available shipping list information + +```js +await printify.v2.catalog.getShippingListInfo('3', '8'); +``` + +
+ View Response + +```json +[ + { + "type": "shipping_method", + "id": "1", + "attributes": { + "name": "standard" + } + }, + { + "type": "shipping_method", + "id": "2", + "attributes": { + "name": "priority" + } + }, + { + "type": "shipping_method", + "id": "3", + "attributes": { + "name": "express" + } + }, + { + "type": "shipping_method", + "id": "4", + "attributes": { + "name": "economy" + } + } +] +``` + +
+ +#### `printify.v2.catalog.getStandardShippingInfo(blueprintId, printProviderId)` + +- `GET /v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/standard.json` +- **Description:** Retrieve standard shipping method information + +```js +await printify.v2.catalog.getStandardShippingInfo('3', '8'); +``` + +
+ View Response + +```json +// TODO v1.3.0 +``` + +
+ +#### `printify.v2.catalog.getPriorityShippingInfo(blueprintId, printProviderId)` + +- `GET /v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/priority.json` +- **Description:** Retrieve priority shipping method information + +```js +await printify.v2.catalog.getPriorityShippingInfo('3', '8'); +``` + +
+ View Response + +```json +// TODO v1.3.0 +``` + +
+ +#### `printify.v2.catalog.getExpressShippingInfo(blueprintId, printProviderId)` + +- `GET /v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/express.json` +- **Description:** Retrieve express shipping method information + +```js +await printify.v2.catalog.getExpressShippingInfo('3', '8'); +``` + +
+ View Response + +```json +// TODO v1.3.0 +``` + +
+ +#### `printify.v2.catalog.getEconomyShippingInfo(blueprintId, printProviderId)` + +- `GET /v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/economy.json` +- **Description:** Retrieve economy shipping method information + +```js +await printify.v2.catalog.getEconomyShippingInfo('3', '8'); +``` + +
+ View Response + +```json +// TODO v1.3.0 +``` + +
+ ### Products #### `printify.products.list()` diff --git a/examples/development/.gitignore b/examples/development/.gitignore deleted file mode 100644 index f9602e9..0000000 --- a/examples/development/.gitignore +++ /dev/null @@ -1 +0,0 @@ -app.js \ No newline at end of file diff --git a/src/v2/catalog/getEconomyShippingInfo.ts b/src/v2/catalog/getEconomyShippingInfo.ts new file mode 100644 index 0000000..2ce5dc5 --- /dev/null +++ b/src/v2/catalog/getEconomyShippingInfo.ts @@ -0,0 +1,23 @@ +import { ShippingInfoSpecific } from '../types'; + +export type GetEconomyShippingInfoResponse = ShippingInfoSpecific; + +/** + * Retrieve economy shipping method information + * + * @param {string} blueprintId - The ID of the blueprint + * @param {string} printProviderId - The ID of the print provider + * @returns {Promise} + * + * @example + * await printify.v2.catalog.getEconomyShippingInfo('3', '8'); + * // Expected response: [ + * // TODO v1.3.0 + */ +const getEconomyShippingInfo = function (this: method, blueprintId: string, printProviderId: string): Promise { + return this.request(`/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/economy.json`, { + method: 'GET', + }); +}; + +export default getEconomyShippingInfo; diff --git a/src/v2/catalog/getExpressShippingInfo.ts b/src/v2/catalog/getExpressShippingInfo.ts new file mode 100644 index 0000000..1a0ffd8 --- /dev/null +++ b/src/v2/catalog/getExpressShippingInfo.ts @@ -0,0 +1,23 @@ +import { ShippingInfoSpecific } from '../types'; + +export type GetExpressShippingInfoResponse = ShippingInfoSpecific; + +/** + * Retrieve express shipping method information + * + * @param {string} blueprintId - The ID of the blueprint + * @param {string} printProviderId - The ID of the print provider + * @returns {Promise} + * + * @example + * await printify.v2.catalog.getExpressShippingInfo('3', '8'); + * // Expected response: [ + * // TODO v1.3.0 + */ +const getExpressShippingInfo = function (this: method, blueprintId: string, printProviderId: string): Promise { + return this.request(`/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/express.json`, { + method: 'GET', + }); +}; + +export default getExpressShippingInfo; diff --git a/src/v2/catalog/getPriorityShippingInfo.ts b/src/v2/catalog/getPriorityShippingInfo.ts new file mode 100644 index 0000000..b7308b0 --- /dev/null +++ b/src/v2/catalog/getPriorityShippingInfo.ts @@ -0,0 +1,23 @@ +import { ShippingInfoSpecific } from '../types'; + +export type GetPriorityShippingInfoResponse = ShippingInfoSpecific; + +/** + * Retrieve priority shipping method information + * + * @param {string} blueprintId - The ID of the blueprint + * @param {string} printProviderId - The ID of the print provider + * @returns {Promise} + * + * @example + * await printify.v2.catalog.getPriorityShippingInfo('3', '8'); + * // Expected response: [ + * // TODO v1.3.0 + */ +const getPriorityShippingInfo = function (this: method, blueprintId: string, printProviderId: string): Promise { + return this.request(`/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/priority.json`, { + method: 'GET', + }); +}; + +export default getPriorityShippingInfo; diff --git a/src/v2/catalog/getShippingListInfo.ts b/src/v2/catalog/getShippingListInfo.ts index bea792e..1971ae1 100644 --- a/src/v2/catalog/getShippingListInfo.ts +++ b/src/v2/catalog/getShippingListInfo.ts @@ -18,7 +18,6 @@ export type GetShippingListInfoResponse = ShippingInfo[]; * @example * await printify.v2.catalog.getShippingListInfo('3', '8'); * // Expected response: [ - * // [ * // { * // "type": "shipping_method", * // "id": "1", diff --git a/src/v2/catalog/getStandardShippingInfo.ts b/src/v2/catalog/getStandardShippingInfo.ts new file mode 100644 index 0000000..4b2825e --- /dev/null +++ b/src/v2/catalog/getStandardShippingInfo.ts @@ -0,0 +1,47 @@ +import { ShippingInfoSpecific } from '../types'; + +export type GetStandardShippingInfoResponse = ShippingInfoSpecific; + +/** + * Retrieve standard shipping method information + * + * @param {string} blueprintId - The ID of the blueprint + * @param {string} printProviderId - The ID of the print provider + * @returns {Promise} + * + * @example + * await printify.v2.catalog.getShippingListInfo('3', '8'); + * // Expected response: [ + * // { + * // "type": "variant_shipping_standard_us", + * // "id": "23494", + * // "attributes": { + * // "shippingType": "standard", + * // "country": { + * // "code": "US" + * // }, + * // "variantId": 23494, + * // "shippingPlanId": "65a7c0825b50fcd56a018e02", + * // "handlingTime": { + * // "from": 4, + * // "to": 8 + * // }, + * // "shippingCost": { + * // "firstItem": { + * // "amount": 399, + * // "currency": "USD" + * // }, + * // "additionalItems": { + * // "amount": 219, + * // "currency": "USD" + * // } + * // } + * // } + * // } + * // ] + */ +const getStandardShippingInfo = function (this: method, blueprintId: string, printProviderId: string): Promise { + return this.request(`/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/standard.json`, { method: 'GET' }); +}; + +export default getStandardShippingInfo; diff --git a/src/v2/catalog/index.ts b/src/v2/catalog/index.ts index 1824ea7..bf0d091 100644 --- a/src/v2/catalog/index.ts +++ b/src/v2/catalog/index.ts @@ -1,13 +1,25 @@ import { PrintifyConfig } from '../../client'; import HttpClient from '../../http'; +import getEconomyShippingInfo from './getEconomyShippingInfo'; +import getExpressShippingInfo from './getExpressShippingInfo'; +import getPriorityShippingInfo from './getPriorityShippingInfo'; import getShippingListInfo from './getShippingListInfo'; +import getStandardShippingInfo from './getStandardShippingInfo'; class CatalogV2 extends HttpClient { getShippingListInfo: typeof getShippingListInfo; + getStandardShippingInfo: typeof getStandardShippingInfo; + getPriorityShippingInfo: typeof getPriorityShippingInfo; + getExpressShippingInfo: typeof getExpressShippingInfo; + getEconomyShippingInfo: typeof getEconomyShippingInfo; constructor(config: PrintifyConfig) { super(config); this.getShippingListInfo = getShippingListInfo.bind(this); + this.getStandardShippingInfo = getStandardShippingInfo.bind(this); + this.getPriorityShippingInfo = getPriorityShippingInfo.bind(this); + this.getExpressShippingInfo = getExpressShippingInfo.bind(this); + this.getEconomyShippingInfo = getEconomyShippingInfo.bind(this); } } diff --git a/src/v2/types.ts b/src/v2/types.ts index aae4523..aeb9f7e 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -1,5 +1,27 @@ // Catalog -// TODO v1.3.0 -export interface ReplaceMe { - example: any; +export interface ShippingInfoSpecific { + type: string; + id: string; + attributes: { + shippingType: string; + country: { + code: string; + }; + variantId: number; + shippingPlanId: string; + handlingTime: { + from: number; + to: number; + }; + shippingCost: { + firstItem: { + amount: number; + currency: string; + }; + additionalItems: { + amount: number; + currency: string; + }; + }; + }; } diff --git a/tests/catalog.test.ts b/tests/catalog.test.ts index fb53b74..b6ba76a 100644 --- a/tests/catalog.test.ts +++ b/tests/catalog.test.ts @@ -73,7 +73,6 @@ describe('Catalog V1', () => { }); describe('Catalog V2', () => { - // TODO v1.3.0 let printify: Printify; beforeAll(() => { @@ -89,4 +88,44 @@ describe('Catalog V2', () => { // Assert assertAxiosCall('get', `/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping.json`); }); + + it('should handle the getStandardShippingInfo endpoint', async () => { + // Act + const blueprintId = '3'; + const printProviderId = '8'; + await printify.v2.catalog.getStandardShippingInfo(blueprintId, printProviderId); + + // Assert + assertAxiosCall('get', `/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/standard.json`); + }); + + it('should handle the getPriorityShippingInfo endpoint', async () => { + // Act + const blueprintId = '3'; + const printProviderId = '8'; + await printify.v2.catalog.getPriorityShippingInfo(blueprintId, printProviderId); + + // Assert + assertAxiosCall('get', `/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/priority.json`); + }); + + it('should handle the getExpressShippingInfo endpoint', async () => { + // Act + const blueprintId = '3'; + const printProviderId = '8'; + await printify.v2.catalog.getExpressShippingInfo(blueprintId, printProviderId); + + // Assert + assertAxiosCall('get', `/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/express.json`); + }); + + it('should handle the getEconomyShippingInfo endpoint', async () => { + // Act + const blueprintId = '3'; + const printProviderId = '8'; + await printify.v2.catalog.getEconomyShippingInfo(blueprintId, printProviderId); + + // Assert + assertAxiosCall('get', `/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/economy.json`); + }); }); From 589ce452dffc02aad309f25106d18d2043e11d66 Mon Sep 17 00:00:00 2001 From: Spencer Lepine <60903378+spencerlepine@users.noreply.github.com> Date: Sun, 5 Jan 2025 12:01:35 +0000 Subject: [PATCH 4/7] remove README note --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 33f4172..02c8d44 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ The Printify Node SDK provides convenient access to the Printify API from applic Guidelines and source endpoints can be found here: [developers.printify.com](https://developers.printify.com). -> 📢 Note: This SDK currently supports V1 API endpoints only. A 2.0.0 release is planned once the majority of V2 endpoints have been migrated. - ## Documentation See the [`printify-sdk-js` API docs](./docs/API.md) for Node.js From eb67938d88c73519bb624e7c6f01ee7988259551 Mon Sep 17 00:00:00 2001 From: Spencer Lepine <60903378+spencerlepine@users.noreply.github.com> Date: Sun, 5 Jan 2025 12:05:51 +0000 Subject: [PATCH 5/7] update examples --- examples/typescript/app.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/typescript/app.ts b/examples/typescript/app.ts index df48a32..fa028b0 100644 --- a/examples/typescript/app.ts +++ b/examples/typescript/app.ts @@ -7,8 +7,16 @@ const printify = new Printify({ enableLogging: true, }); +// v1 endpoint (async () => { const result: ListWebhooksResponse = await printify.webhooks.list(); const webhook: Webhook = result[0]; console.log(webhook); // { "topic": "order:created", "url": "https://example.com/webhooks/order/created", "shop_id": "1", "id": "5cb87a8cd490a2ccb256cec4" } })(); + +// TODO v1.3.0 +// v2 endpoint +(async () => { + const shippingInfo = await printify.v2.catalog.getShippingListInfo('1', '2'); + console.log(shippingInfo); +})(); From cb3d8f7537250246abd1db8fb1112870fb35da1d Mon Sep 17 00:00:00 2001 From: Spencer Lepine <60903378+spencerlepine@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:15:06 +0000 Subject: [PATCH 6/7] logging --- README.md | 20 ++++++++++---------- examples/development/app.ts | 14 -------------- examples/development/package.json | 12 ------------ examples/development/tsconfig.json | 10 ---------- examples/typescript/app.ts | 13 +++++-------- src/http.ts | 8 +++++--- src/index.ts | 1 + src/v2/catalog/getShippingListInfo.ts | 8 +------- src/v2/types.ts | 8 ++++++++ tests/http.test.ts | 8 ++++---- 10 files changed, 34 insertions(+), 68 deletions(-) delete mode 100644 examples/development/app.ts delete mode 100644 examples/development/package.json delete mode 100644 examples/development/tsconfig.json diff --git a/README.md b/README.md index 02c8d44..fc8f10d 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,11 @@ Guidelines and source endpoints can be found here: [developers.printify.com](htt ## Documentation -See the [`printify-sdk-js` API docs](./docs/API.md) for Node.js +See the [`API.md](./docs/API.md) docs - [Shops](./docs/API.md#shops) - `printify.shops.*` - [Catalog](./docs/API.md#catalog) - `printify.catalog.*` +- [Catalog V2](./docs/API.md#catalog-v2) - `printify.v2.catalog.*` - [Products](./docs/API.md#products) - `printify.products.*` - [Orders](./docs/API.md#orders) - `printify.orders.*` - [Uploads](./docs/API.md#uploads) - `printify.uploads.*` @@ -49,17 +50,15 @@ console.log(orders); // { current_page: 1, data: [{ id: "5a9", address_to: {}, l ### Usage with TypeScript ```typescript -import Printify from 'printify-sdk-js'; -import type { ListWebhooksResponse, Webhook } from 'printify-sdk-js'; +import Printify, { Webhook } from 'printify-sdk-js'; const printify = new Printify({ accessToken: process.env.PRINTIFY_API_TOKEN, shopId: '123456', }); -const result: ListWebhooksResponse = await printify.webhooks.list(); -const webhook: Webhook = result[0]; -console.log(webhook); // { "topic": "order:created", "url": "https://example.com/webhooks/order/created", "shop_id": "1", "id": "5cb87a8cd490a2ccb256cec4" } +const webhooks: Webhook[] = await printify.webhooks.list(); +console.log(webhooks[0]); // { "topic": "order:created", "url": "https://example.com/webhooks/order/created", "shop_id": "1", "id": "5cb87a8cd490a2ccb256cec4" } ``` ### Usage with CommonJS @@ -96,7 +95,7 @@ const printify = new Printify({ | --------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `accessToken` | `null` | The API access token for authenticating requests. Generate one at [Printify API](https://printify.com/app/account/api). | | `shopId` | `null` | (optional) Your personal shop ID. Can be found using `printify.shops.list()`. | -| `enableLogging` | `true` | (optional) Enables logging of API requests and responses. Enabled by default. | +| `enableLogging` | `true` | (optional) Enables logging of API requests and errors. Enabled by default. | | `host` | `'api.printify.com'` | (optional) The host for API requests. | | `timeout` | `5000` | (optional) Request timeout in ms. | @@ -111,10 +110,11 @@ yarn test ```sh # (optional) test the bundle locally +cd examples/typescript && yarn && cd ../../ yarn build -mv dist examples/development -cd examples/development -yarn && yarn start +rm -rf examples/typescript/node_modules/printify-sdk-js/dist && mv dist examples/typescript/node_modules/printify-sdk-js +cd examples/typescript +yarn start ``` ## Contributing diff --git a/examples/development/app.ts b/examples/development/app.ts deleted file mode 100644 index c0d090e..0000000 --- a/examples/development/app.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-expect-error - doesn't automatically detect "dist/index.d.mts" -import Printify from './dist/index.cjs.js'; -import type { Webhook } from './dist/index.d.mts'; - -const printify = new Printify({ - shopId: '123456', - accessToken: process.env.PRINTIFY_API_TOKEN, - enableLogging: true, -}); - -(async () => { - const result: Webhook[] = await printify.webhooks.list(); - console.log(result); -})(); diff --git a/examples/development/package.json b/examples/development/package.json deleted file mode 100644 index f1ca0af..0000000 --- a/examples/development/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "printify-development-testing", - "scripts": { - "start": "tsc && node app.js" - }, - "dependencies": { - "printify-sdk-js": "*" - }, - "devDependencies": { - "typescript": "^5.5.4" - } -} diff --git a/examples/development/tsconfig.json b/examples/development/tsconfig.json deleted file mode 100644 index 3ffa57b..0000000 --- a/examples/development/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "target": "es2016", - "module": "commonjs", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true - } -} diff --git a/examples/typescript/app.ts b/examples/typescript/app.ts index fa028b0..6a073fb 100644 --- a/examples/typescript/app.ts +++ b/examples/typescript/app.ts @@ -1,5 +1,4 @@ -import Printify from 'printify-sdk-js'; -import type { ListWebhooksResponse, Webhook } from 'printify-sdk-js'; +import Printify, { Webhook, ShippingInfo } from 'printify-sdk-js'; const printify = new Printify({ shopId: '123456', @@ -9,14 +8,12 @@ const printify = new Printify({ // v1 endpoint (async () => { - const result: ListWebhooksResponse = await printify.webhooks.list(); - const webhook: Webhook = result[0]; - console.log(webhook); // { "topic": "order:created", "url": "https://example.com/webhooks/order/created", "shop_id": "1", "id": "5cb87a8cd490a2ccb256cec4" } + const webhooks: Webhook[] = await printify.webhooks.list(); + console.log(webhooks[0]); // { "topic": "order:created", "url": "https://example.com/webhooks/order/created", "shop_id": "1", "id": "5cb87a8cd490a2ccb256cec4" } })(); -// TODO v1.3.0 // v2 endpoint (async () => { - const shippingInfo = await printify.v2.catalog.getShippingListInfo('1', '2'); - console.log(shippingInfo); + const shippingInfo: ShippingInfo[] = await printify.v2.catalog.getShippingListInfo('1', '2'); + console.log(shippingInfo); // [ { type: 'shipping_method', id: '2', attributes: [Object] } ] })(); diff --git a/src/http.ts b/src/http.ts index a87f93e..3a6ba25 100644 --- a/src/http.ts +++ b/src/http.ts @@ -36,7 +36,7 @@ class HttpClient { private logRequest(method: string, url: string) { if (this.enableLogging) { - console.log(`Requesting ${method.toUpperCase()} ${this.baseUrl}${url}`); + console.log(`Request: ${method.toUpperCase()} ${this.baseUrl}${url}`); } } @@ -81,10 +81,12 @@ class HttpClient { } catch (error: any) { let message = 'Printify SDK Error'; if ((error as AxiosError).isAxiosError) { - message = `Printify SDK Error: ${error.response?.status} ${error.response?.statusText} - Requested URL: ${this.baseUrl}${url}`; + message = `Printify SDK: ${error.response?.status} ${error.response?.statusText} - Requested URL: ${this.baseUrl}${url}`; } this.logError(message); - throw new Error(message); + const printifyError = new Error(message); + printifyError.stack = undefined; + throw printifyError; } } } diff --git a/src/index.ts b/src/index.ts index 27e422d..93ac897 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import Printify from './client'; export * from './v1/types'; +export * from './v2/types'; export default Printify; diff --git a/src/v2/catalog/getShippingListInfo.ts b/src/v2/catalog/getShippingListInfo.ts index 1971ae1..c4ddcd9 100644 --- a/src/v2/catalog/getShippingListInfo.ts +++ b/src/v2/catalog/getShippingListInfo.ts @@ -1,10 +1,4 @@ -export interface ShippingInfo { - type: string; - id: string; - attributes: { - name: string; - }; -} +import { ShippingInfo } from '../types'; export type GetShippingListInfoResponse = ShippingInfo[]; diff --git a/src/v2/types.ts b/src/v2/types.ts index aeb9f7e..9d84f24 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -1,4 +1,12 @@ // Catalog +export interface ShippingInfo { + type: string; + id: string; + attributes: { + name: string; + }; +} + export interface ShippingInfoSpecific { type: string; id: string; diff --git a/tests/http.test.ts b/tests/http.test.ts index a8abad3..f8a5ff5 100644 --- a/tests/http.test.ts +++ b/tests/http.test.ts @@ -85,7 +85,7 @@ describe('HttpClient', () => { (axios.get as jest.Mock).mockResolvedValueOnce({ response: { status: 200, statusText: 'Hello, world!' } }); await http.request(url); - expect(console.log).toHaveBeenCalledWith(`Requesting ${method.toUpperCase()} https://${http['host']}${url}`); + expect(console.log).toHaveBeenCalledWith(`Request: ${method.toUpperCase()} https://${http['host']}${url}`); }); it('should log the request when enableLogging is true', async () => { @@ -97,7 +97,7 @@ describe('HttpClient', () => { (axios.get as jest.Mock).mockResolvedValueOnce({ response: { status: 200, statusText: 'Hello, world!' } }); await http.request(url); - expect(console.log).toHaveBeenCalledWith(`Requesting ${method.toUpperCase()} https://${http['host']}${url}`); + expect(console.log).toHaveBeenCalledWith(`Request: ${method.toUpperCase()} https://${http['host']}${url}`); }); it('should not log the request when enableLogging is false', async () => { @@ -162,8 +162,8 @@ describe('HttpClient', () => { const url = '/test-url'; - await expect(http.request(url)).rejects.toThrow('Printify SDK Error: 404 Not Found - Requested URL: https://' + http['host'] + url); + await expect(http.request(url)).rejects.toThrow('Printify SDK: 404 Not Found - Requested URL: https://' + http['host'] + url); - expect(console.error).toHaveBeenCalledWith(`Printify SDK Error: 404 Not Found - Requested URL: https://${http['host']}${url}`); + expect(console.error).toHaveBeenCalledWith(`Printify SDK: 404 Not Found - Requested URL: https://${http['host']}${url}`); }); }); From 6d0be7f99b29e0102498c1f97b383dd2378641d0 Mon Sep 17 00:00:00 2001 From: Spencer Lepine <60903378+spencerlepine@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:31:32 +0000 Subject: [PATCH 7/7] v2 endpoints --- CHANGELOG.md | 20 +-- README.md | 2 +- docs/API.md | 204 ++++++++++++++++------ package.json | 2 +- src/v2/catalog/getEconomyShippingInfo.ts | 29 ++- src/v2/catalog/getExpressShippingInfo.ts | 29 ++- src/v2/catalog/getPriorityShippingInfo.ts | 29 ++- 7 files changed, 251 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4c66b3..2bf5843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,29 +4,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.3.0] - 2025-01-XX +## [1.3.0] - 2025-01-06 -- TODO v1.3.0 -- Added support for V2 endpoints +- Added support for V2 endpoints. +- Removed verbose stack trace from errors. ## [1.2.0] - 2025-01-03 -- Organized shared types and fixed bundle export -- Improved configuration object +- Organized shared types and fixed bundle export. +- Improved configuration object. ## [1.1.0] - 2024-11-08 -- Replaced `fetch` with `axios` for improved security -- Applied minor internal refactors for better maintainability +- Replaced `fetch` with `axios` for enhanced security. +- Refactored internal code for better maintainability. ## [1.0.2] - 2024-10-24 -Improved error log for easier debugging +- Added error logging for easier debugging. ## [1.0.1] - 2024-07-29 -Documentation update for TypeScript support +- Updated documentation to include TypeScript support. ## [1.0.0] - 2024-07-26 -Initial release for Printify API v1 +- Initial release for Printify API v1. diff --git a/README.md b/README.md index fc8f10d..7db2c27 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Printify SDK for TypeScript (Node.js) +# Printify SDK for Node.js (TypeScript) [![NPM Version](https://img.shields.io/npm/v/printify-sdk-js)](https://www.npmjs.com/package/printify-sdk-js) ![Coverage](https://img.shields.io/badge/Coverage-100%25-brightgreen.svg) ![MIT license](https://img.shields.io/badge/License-MIT-blue.svg) diff --git a/docs/API.md b/docs/API.md index 3930407..d32a439 100644 --- a/docs/API.md +++ b/docs/API.md @@ -531,7 +531,34 @@ await printify.v2.catalog.getStandardShippingInfo('3', '8'); View Response ```json -// TODO v1.3.0 +[ + { + "type": "variant_shipping_standard_us", + "id": "23494", + "attributes": { + "shippingType": "standard", + "country": { + "code": "US" + }, + "variantId": 23494, + "shippingPlanId": "65a7c0825b50fcd56a018e02", + "handlingTime": { + "from": 4, + "to": 8 + }, + "shippingCost": { + "firstItem": { + "amount": 399, + "currency": "USD" + }, + "additionalItems": { + "amount": 219, + "currency": "USD" + } + } + } + } +] ``` @@ -549,7 +576,34 @@ await printify.v2.catalog.getPriorityShippingInfo('3', '8'); View Response ```json -// TODO v1.3.0 +[ + { + "type": "variant_shipping_priority_us", + "id": "23494", + "attributes": { + "shippingType": "priority", + "country": { + "code": "US" + }, + "variantId": 23494, + "shippingPlanId": "65a7c0825b50fcd56a018e02", + "handlingTime": { + "from": 4, + "to": 8 + }, + "shippingCost": { + "firstItem": { + "amount": 399, + "currency": "USD" + }, + "additionalItems": { + "amount": 219, + "currency": "USD" + } + } + } + } +] ``` @@ -567,7 +621,34 @@ await printify.v2.catalog.getExpressShippingInfo('3', '8'); View Response ```json -// TODO v1.3.0 +[ + { + "type": "variant_shipping_express_us", + "id": "23494", + "attributes": { + "shippingType": "express", + "country": { + "code": "US" + }, + "variantId": 23494, + "shippingPlanId": "65a7c0825b50fcd56a018e02", + "handlingTime": { + "from": 4, + "to": 8 + }, + "shippingCost": { + "firstItem": { + "amount": 399, + "currency": "USD" + }, + "additionalItems": { + "amount": 219, + "currency": "USD" + } + } + } + } +] ``` @@ -585,7 +666,34 @@ await printify.v2.catalog.getEconomyShippingInfo('3', '8'); View Response ```json -// TODO v1.3.0 +[ + { + "type": "variant_shipping_economy_us", + "id": "23494", + "attributes": { + "shippingType": "economy", + "country": { + "code": "US" + }, + "variantId": 23494, + "shippingPlanId": "65a7c0825b50fcd56a018e02", + "handlingTime": { + "from": 4, + "to": 8 + }, + "shippingCost": { + "firstItem": { + "amount": 399, + "currency": "USD" + }, + "additionalItems": { + "amount": 219, + "currency": "USD" + } + } + } + } +] ``` @@ -1474,52 +1582,50 @@ await printify.orders.submitExpress(data); View Response ```json -{ - "data": [ - { - "type": "order", - "id": "5a96f649b2439217d070f508", - "attributes": { - "fulfilment_type": "express", - "line_items": [ - { - "product_id": "5b05842f3921c9547531758d", - "quantity": 1, - "variant_id": 12359, - "print_provider_id": 5, - "cost": 2200, - "shipping_cost": 799, - "status": "pending", - "metadata": { "title": "T-shirt", "price": 2200, "variant_label": "Blue / S", "sku": "168699843", "country": "United States" }, - "sent_to_production_at": "2023-10-18 13:24:28+00:00", - "fulfilled_at": null - } - ] - } - }, - { - "type": "order", - "id": "5a96f649b2439597d020a9b4", - "attributes": { - "fulfilment_type": "ordinary", - "line_items": [ - { - "product_id": "5b05842f3921c34764fa478bc", - "quantity": 1, - "variant_id": 17887, - "print_provider_id": 5, - "cost": 1050, - "shipping_cost": 400, - "status": "pending", - "metadata": { "title": "Mug 11oz", "price": 1050, "variant_label": "11oz", "sku": "168699843", "country": "United States" }, - "sent_to_production_at": "2023-10-18 13:24:28+00:00", - "fulfilled_at": null - } - ] - } +[ + { + "type": "order", + "id": "5a96f649b2439217d070f508", + "attributes": { + "fulfilment_type": "express", + "line_items": [ + { + "product_id": "5b05842f3921c9547531758d", + "quantity": 1, + "variant_id": 12359, + "print_provider_id": 5, + "cost": 2200, + "shipping_cost": 799, + "status": "pending", + "metadata": { "title": "T-shirt", "price": 2200, "variant_label": "Blue / S", "sku": "168699843", "country": "United States" }, + "sent_to_production_at": "2023-10-18 13:24:28+00:00", + "fulfilled_at": null + } + ] } - ] -} + }, + { + "type": "order", + "id": "5a96f649b2439597d020a9b4", + "attributes": { + "fulfilment_type": "ordinary", + "line_items": [ + { + "product_id": "5b05842f3921c34764fa478bc", + "quantity": 1, + "variant_id": 17887, + "print_provider_id": 5, + "cost": 1050, + "shipping_cost": 400, + "status": "pending", + "metadata": { "title": "Mug 11oz", "price": 1050, "variant_label": "11oz", "sku": "168699843", "country": "United States" }, + "sent_to_production_at": "2023-10-18 13:24:28+00:00", + "fulfilled_at": null + } + ] + } + } +] ``` diff --git a/package.json b/package.json index 90a9e23..88b0bf5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "printify-sdk-js", - "version": "1.3.0-beta.1", + "version": "1.3.0", "description": "Node.js SDK for the Printify API.", "author": "Spencer Lepine ", "license": "MIT", diff --git a/src/v2/catalog/getEconomyShippingInfo.ts b/src/v2/catalog/getEconomyShippingInfo.ts index 2ce5dc5..91f08d9 100644 --- a/src/v2/catalog/getEconomyShippingInfo.ts +++ b/src/v2/catalog/getEconomyShippingInfo.ts @@ -12,7 +12,34 @@ export type GetEconomyShippingInfoResponse = ShippingInfoSpecific; * @example * await printify.v2.catalog.getEconomyShippingInfo('3', '8'); * // Expected response: [ - * // TODO v1.3.0 + * // [ + * // { + * // "type": "variant_shipping_economy_us", + * // "id": "23494", + * // "attributes": { + * // "shippingType": "economy", + * // "country": { + * // "code": "US" + * // }, + * // "variantId": 23494, + * // "shippingPlanId": "65a7c0825b50fcd56a018e02", + * // "handlingTime": { + * // "from": 4, + * // "to": 8 + * // }, + * // "shippingCost": { + * // "firstItem": { + * // "amount": 399, + * // "currency": "USD" + * // }, + * // "additionalItems": { + * // "amount": 219, + * // "currency": "USD" + * // } + * // } + * // } + * // } + * // ] */ const getEconomyShippingInfo = function (this: method, blueprintId: string, printProviderId: string): Promise { return this.request(`/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/economy.json`, { diff --git a/src/v2/catalog/getExpressShippingInfo.ts b/src/v2/catalog/getExpressShippingInfo.ts index 1a0ffd8..80b8f9c 100644 --- a/src/v2/catalog/getExpressShippingInfo.ts +++ b/src/v2/catalog/getExpressShippingInfo.ts @@ -12,7 +12,34 @@ export type GetExpressShippingInfoResponse = ShippingInfoSpecific; * @example * await printify.v2.catalog.getExpressShippingInfo('3', '8'); * // Expected response: [ - * // TODO v1.3.0 + * // [ + * // { + * // "type": "variant_shipping_standard_us", + * // "id": "23494", + * // "attributes": { + * // "shippingType": "standard", + * // "country": { + * // "code": "US" + * // }, + * // "variantId": 23494, + * // "shippingPlanId": "65a7c0825b50fcd56a018e02", + * // "handlingTime": { + * // "from": 4, + * // "to": 8 + * // }, + * // "shippingCost": { + * // "firstItem": { + * // "amount": 399, + * // "currency": "USD" + * // }, + * // "additionalItems": { + * // "amount": 219, + * // "currency": "USD" + * // } + * // } + * // } + * // } + * // ] */ const getExpressShippingInfo = function (this: method, blueprintId: string, printProviderId: string): Promise { return this.request(`/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/express.json`, { diff --git a/src/v2/catalog/getPriorityShippingInfo.ts b/src/v2/catalog/getPriorityShippingInfo.ts index b7308b0..3a50dfa 100644 --- a/src/v2/catalog/getPriorityShippingInfo.ts +++ b/src/v2/catalog/getPriorityShippingInfo.ts @@ -12,7 +12,34 @@ export type GetPriorityShippingInfoResponse = ShippingInfoSpecific; * @example * await printify.v2.catalog.getPriorityShippingInfo('3', '8'); * // Expected response: [ - * // TODO v1.3.0 + * // [ + * // { + * // "type": "variant_shipping_priority_us", + * // "id": "23494", + * // "attributes": { + * // "shippingType": "priority", + * // "country": { + * // "code": "US" + * // }, + * // "variantId": 23494, + * // "shippingPlanId": "65a7c0825b50fcd56a018e02", + * // "handlingTime": { + * // "from": 4, + * // "to": 8 + * // }, + * // "shippingCost": { + * // "firstItem": { + * // "amount": 399, + * // "currency": "USD" + * // }, + * // "additionalItems": { + * // "amount": 219, + * // "currency": "USD" + * // } + * // } + * // } + * // } + * // ] */ const getPriorityShippingInfo = function (this: method, blueprintId: string, printProviderId: string): Promise { return this.request(`/v2/catalog/blueprints/${blueprintId}/print_providers/${printProviderId}/shipping/priority.json`, {