@sommpicks/sommpicks-shopify 24.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/Logger.ts +18 -0
  2. package/README.md +258 -0
  3. package/addTypings.sh +2 -0
  4. package/bitbucket-pipelines.yml +38 -0
  5. package/index.ts +132 -0
  6. package/package.json +57 -0
  7. package/publish.sh +20 -0
  8. package/services/CacheWrapper.ts +30 -0
  9. package/services/CountryCodeService.ts +507 -0
  10. package/shopify/ShopifyAppService.ts +109 -0
  11. package/shopify/ShopifyAssetService.ts +20 -0
  12. package/shopify/ShopifyBillingService.ts +73 -0
  13. package/shopify/ShopifyCartTrasnformationService.ts +207 -0
  14. package/shopify/ShopifyCollectionService.ts +523 -0
  15. package/shopify/ShopifyCustomerService.ts +472 -0
  16. package/shopify/ShopifyDeliveryCustomisationService.ts +220 -0
  17. package/shopify/ShopifyDiscountService.ts +131 -0
  18. package/shopify/ShopifyDraftOrderService.ts +125 -0
  19. package/shopify/ShopifyFulfillmentService.ts +41 -0
  20. package/shopify/ShopifyFunctionsProductDiscountsService.ts +166 -0
  21. package/shopify/ShopifyInventoryService.ts +415 -0
  22. package/shopify/ShopifyLocationService.ts +29 -0
  23. package/shopify/ShopifyOrderRefundsService.ts +138 -0
  24. package/shopify/ShopifyOrderRiskService.ts +19 -0
  25. package/shopify/ShopifyOrderService.ts +1143 -0
  26. package/shopify/ShopifyPageService.ts +62 -0
  27. package/shopify/ShopifyProductService.ts +772 -0
  28. package/shopify/ShopifyShippingZonesService.ts +37 -0
  29. package/shopify/ShopifyShopService.ts +101 -0
  30. package/shopify/ShopifyTemplateService.ts +30 -0
  31. package/shopify/ShopifyThemeService.ts +33 -0
  32. package/shopify/ShopifyUtils.ts +56 -0
  33. package/shopify/ShopifyWebhookService.ts +110 -0
  34. package/shopify/base/APIVersion.ts +4 -0
  35. package/shopify/base/AbstractService.ts +152 -0
  36. package/shopify/base/ErrorHelper.ts +24 -0
  37. package/shopify/errors/InspiraShopifyCustomError.ts +7 -0
  38. package/shopify/errors/InspiraShopifyError.ts +15 -0
  39. package/shopify/errors/InspiraShopifyUnableToReserveInventoryError.ts +7 -0
  40. package/shopify/helpers/ShopifyProductServiceHelper.ts +450 -0
  41. package/shopify/product/ShopifyProductCountService.ts +110 -0
  42. package/shopify/product/ShopifyProductListService.ts +333 -0
  43. package/shopify/product/ShopifyProductMetafieldsService.ts +405 -0
  44. package/shopify/product/ShopifyProductPublicationsService.ts +112 -0
  45. package/shopify/product/ShopifyVariantService.ts +584 -0
  46. package/shopify/router/ShopifyMandatoryRouter.ts +37 -0
  47. package/shopify/router/ShopifyRouter.ts +85 -0
  48. package/shopify/router/ShopifyRouterBis.ts +85 -0
  49. package/shopify/router/ShopifyRouterBisBis.ts +85 -0
  50. package/shopify/router/ShopifyRouterBisBisBis.ts +85 -0
  51. package/shopify/router/ShopifyRouterBisBisBisBis.ts +85 -0
  52. package/shopify/router/WebhookSkipMiddleware.ts +73 -0
  53. package/shopify/router/services/CryptoService.ts +26 -0
  54. package/shopify/router/services/HmacValidator.ts +36 -0
  55. package/shopify/router/services/OauthService.ts +17 -0
  56. package/shopify/router/services/RestUtils.ts +13 -0
  57. package/shopify/router/services/rateLimiter/MemoryStores.ts +46 -0
  58. package/shopify/router/services/rateLimiter/StoreRateLimiter.ts +46 -0
  59. package/test/README.md +223 -0
  60. package/test/router/ShopifyRouter.test.ts +71 -0
  61. package/test/router/WebhookSkipMiddleware.test.ts +86 -0
  62. package/test/router/services/HmacValidator.test.ts +24 -0
  63. package/test/router/services/RestUtils.test.ts +13 -0
  64. package/test/router/services/rateLimiter/StoreRateLimiter.test.ts +62 -0
  65. package/test/services/CacheWrapper.test.ts +30 -0
  66. package/test/shopify/ShopifyOrderService.test.ts +29 -0
  67. package/test/shopify/ShopifyProductService.test.ts +118 -0
  68. package/test/shopify/ShopifyWebhookService.test.ts +105 -0
  69. package/tsconfig.json +10 -0
  70. package/typings/axios.d.ts +8 -0
  71. package/typings/index.d.ts +1682 -0
@@ -0,0 +1,333 @@
1
+ import { AxiosInstance } from 'axios';
2
+ import { print } from 'graphql';
3
+ import gql from 'graphql-tag';
4
+ import { Logger } from '../../Logger';
5
+ import InspiraShopifyError from '../errors/InspiraShopifyError';
6
+ import * as moment from 'moment';
7
+ import { ShopifyProductServiceHelper } from '../helpers/ShopifyProductServiceHelper';
8
+ import { AbstractService } from '../base/AbstractService';
9
+
10
+ export class ShopifyProductListService extends AbstractService {
11
+
12
+ private shopifyProductServiceHelper: ShopifyProductServiceHelper;
13
+
14
+ constructor(private axiosInstance: AxiosInstance) {
15
+ super();
16
+ this.shopifyProductServiceHelper = new ShopifyProductServiceHelper();
17
+ }
18
+
19
+ /**
20
+ *
21
+ * @param numOfProducts number or all if you want them all
22
+ * @param filter by title, handle published Status and createdDaysAgo
23
+ * @param daysAgo number or all if no filter
24
+ * @param options by default it loads all data
25
+ * @returns {IProduct} List of products
26
+ */
27
+ public get = async ( numOfProducts: number | 'all',
28
+ filter: { title?: string; handle?: string; publishedStatus: 'any' | 'published' | 'unpublished'; createdDaysAgo: number | 'all', productType?: string; vendor?: string; },
29
+ options: { add_variants?: boolean; add_weight?: boolean; add_body_html?: boolean; add_title?: boolean; add_images?: boolean; add_tags?: boolean; add_options?: boolean; }): Promise<IProduct[]> => {
30
+ try {
31
+ Logger.info(`ShopifyProductListService - get -> Get products with criteria filter: ${JSON.stringify(filter)} and numOfProducts ${numOfProducts}`);
32
+ const products: IProduct[] = [];
33
+ const queryParams = this.buildProductsQuery(filter.publishedStatus, filter.createdDaysAgo, 'all', filter.handle, filter.title, filter.vendor, filter.productType, null);
34
+ let nextCursor = '';
35
+ Logger.info(`ShopifyProductListService - get -> First products query going in ... Query: ${queryParams}`);
36
+ while (nextCursor !== null) {
37
+ if (numOfProducts !== 'all' && products.length >= numOfProducts) {
38
+ break;
39
+ }
40
+ const productsQuery = this.buildProductsGraphQL(numOfProducts, queryParams, nextCursor, options);
41
+ Logger.info(`ShopifyProductListService - get -> Products graphQL query going in ${productsQuery}`);
42
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(gql`${productsQuery}`) }, { query_cost: 800 });
43
+ if (response && response.data && response.data.data && response.data.data.products) {
44
+ const responseData: ShopifygraphQl.IProductResponseFrame = response.data.data;
45
+ Logger.info(`ShopifyProductListService -> Products graphQL pageInfo ${JSON.stringify(responseData?.products?.pageInfo)}`);
46
+ if (responseData.products.pageInfo.hasNextPage) {
47
+ nextCursor = responseData.products.pageInfo.endCursor;
48
+ } else {
49
+ nextCursor = null;
50
+ }
51
+ for (const product of responseData.products.edges) {
52
+ this.shopifyProductServiceHelper.addProduct(products, product.node);
53
+ }
54
+ } else {
55
+ throw new InspiraShopifyError({ message: JSON.stringify(response.data.errors) });
56
+ }
57
+
58
+ }
59
+ Logger.info(`ShopifyProductListService - get -> ${products.length} Products returning`);
60
+ return products;
61
+ } catch (error) { this.logErrorAndThrow(error); }
62
+ };
63
+
64
+
65
+ /**
66
+ *
67
+ * @param {number[]} ids product ids to look (Max of 100)
68
+ * @param options by default it loads all data
69
+ * @returns {IProduct} List of products
70
+ */
71
+ public getByIds = async (ids: number[],
72
+ options: { add_variants?: boolean; add_weight?: boolean; add_body_html?: boolean; add_title?: boolean; add_images?: boolean; add_tags?: boolean; add_options?: boolean; }): Promise<IProduct[]> => {
73
+ try {
74
+ Logger.info(`ShopifyProductListService - getByIds -> Get products ids: ${JSON.stringify(ids)}`);
75
+ const products: IProduct[] = [];
76
+ const productsQuery = this.buildProductsGraphQL(ids.length, `${ids.map(id => `(id:${id})`).join(' OR ')}`, null, options);
77
+ Logger.info(`ShopifyProductListService - getByIds -> Products graphQL query going in ${productsQuery}`);
78
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(gql`${productsQuery}`) }, { query_cost: 800 });
79
+ if (response && response.data && response.data.data && response.data.data.products) {
80
+ const responseData: ShopifygraphQl.IProductResponseFrame = response.data.data;
81
+ for (const product of responseData.products.edges) {
82
+ this.shopifyProductServiceHelper.addProduct(products, product.node);
83
+ }
84
+ } else {
85
+ throw new InspiraShopifyError({ message: JSON.stringify(response.data.errors) });
86
+ }
87
+ Logger.info(`ShopifyProductListService - getByIds -> ${products.length} Products returning`);
88
+ return products;
89
+ } catch (error) { this.logErrorAndThrow(error); }
90
+ };
91
+
92
+
93
+ /**
94
+ *
95
+ * @param {string } cursor sent null to start from begining
96
+ * @param daysAgo number or all if no filter
97
+ * @param options by default it loads all data
98
+ * @returns {IProduct} List of products
99
+ */
100
+ public getInBatch = async (cursor: string,
101
+ filter: { title?: string; handle?: string; publishedStatus: 'any' | 'published' | 'unpublished'; createdDaysAgo: number | 'all', updatedDaysAgo: number | 'all', productType?: string; vendor?: string; collectionID: number | null },
102
+ options: { add_variants?: boolean; add_weight?: boolean; add_body_html?: boolean; add_title?: boolean; add_images?: boolean; add_tags?: boolean; add_options?: boolean; }): Promise<IProductBatch> => {
103
+ try {
104
+ Logger.info(`ShopifyProductListService - getInBatch -> Get products with criteria : ${JSON.stringify(filter)}`);
105
+ const productsInBatch: IProductBatch = { products: [], nextCursor: null };
106
+ const queryParams = this.buildProductsQuery(filter.publishedStatus, filter.createdDaysAgo, filter.updatedDaysAgo, filter.handle, filter.title, filter.vendor, filter.productType, filter.collectionID);
107
+
108
+ Logger.info(`ShopifyProductListService - getInBatch -> First products query going in ... Query: ${queryParams}`);
109
+
110
+ const productsQuery = this.buildProductsGraphQL('all', queryParams, cursor, options);
111
+
112
+ Logger.info(`ShopifyProductListService - getInBatch -> Products graphQL query going in ${productsQuery}`);
113
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(gql`${productsQuery}`) }, { query_cost: 800 });
114
+ if (response && response.data && response.data.data && response.data.data.products) {
115
+ const responseData: ShopifygraphQl.IProductResponseFrame = response.data.data;
116
+ Logger.info(`ShopifyProductListService -> Products graphQL pageInfo ${JSON.stringify(responseData?.products?.pageInfo)}`);
117
+ if (responseData.products.pageInfo.hasNextPage) {
118
+ productsInBatch.nextCursor = responseData.products.pageInfo.endCursor;
119
+ } else {
120
+ productsInBatch.nextCursor = null;
121
+ }
122
+ for (const product of responseData.products.edges) {
123
+ this.shopifyProductServiceHelper.addProduct(productsInBatch.products, product.node);
124
+ }
125
+ } else {
126
+ throw new InspiraShopifyError({ message: JSON.stringify(response.data.errors) });
127
+ }
128
+ Logger.info(`ShopifyProductListService - getInBatch -> ${productsInBatch.products.length} Products returning`);
129
+ return productsInBatch;
130
+ } catch (error) { this.logErrorAndThrow(error); }
131
+ };
132
+
133
+ /**
134
+ *
135
+ * @param id of the product
136
+ * @param options by default it loads all data
137
+ * @returns {IProduct} product
138
+ */
139
+ public getById = async (id: number,
140
+ options: { add_variants?: boolean; add_weight?: boolean; add_body_html?: boolean; add_title?: boolean; add_images?: boolean; add_tags?: boolean; add_options?: boolean; }): Promise<IProduct> => {
141
+ try {
142
+ Logger.info(`ShopifyProductListService - getById -> Get product with id ${id}`);
143
+ let productToReturn: IProduct = null;
144
+
145
+ const productQuery = `{ product(id: "${this.getGraphProductIdFromId(id)}") {
146
+ id
147
+ ${options.add_body_html === false ? '' : 'descriptionHtml'}
148
+ ${options.add_title === false ? '' : 'title'}
149
+ vendor
150
+ productType
151
+ createdAt
152
+ updatedAt
153
+ publishedAt
154
+ handle
155
+ status
156
+ ${options.add_images === false ? '' : 'images(first: 20) { edges { node { id url } } }'}
157
+ ${options.add_tags === false ? '' : 'tags'}
158
+ ${options.add_options === false ? '' : 'options { name position optionValues { name } }'}
159
+ ${options.add_variants === false ? '' : `variants(first: 150) {
160
+ edges {
161
+ node {
162
+ id
163
+ price
164
+ sku
165
+ availableForSale
166
+ inventoryQuantity
167
+ inventoryPolicy
168
+ taxable
169
+ ${options.add_images === false ? '' : 'media(first:1) { nodes { id } }'}
170
+ ${options.add_options === false ? '' : 'selectedOptions { name value }'}
171
+ inventoryItem {
172
+ id
173
+ ${options.add_weight === false ? '' : 'measurement { weight { unit value } }'}
174
+ tracked
175
+ unitCost {
176
+ amount
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }` }
182
+ }
183
+ }`;
184
+ Logger.info(`ShopifyProductListService - getById -> Product graphQL query going in ${productQuery}`);
185
+ const response = await this.axiosInstance.post('/graphql.json', { query: print(gql`${productQuery}`) }, { query_cost: 80 });
186
+ if (response && response.data && response.data.data && response.data.data.product) {
187
+ const responseProduct: ShopifygraphQl.IGenericProductResponse = response.data.data.product;
188
+ productToReturn = {
189
+ id: this.getIdFromGraphId(responseProduct.id),
190
+ body_html: responseProduct.descriptionHtml ? responseProduct.descriptionHtml : null,
191
+ published: responseProduct.publishedAt ? true : false,
192
+ title: responseProduct.title ? responseProduct.title : null,
193
+ created_at: responseProduct.createdAt,
194
+ images: responseProduct.images && responseProduct.images.edges && responseProduct.images.edges.length > 0 ? responseProduct.images.edges.map(im => ({ id: this.getIdFromGraphId(im.node.id), src: im.node.url })) : null,
195
+ product_type: responseProduct.productType,
196
+ published_at: responseProduct.publishedAt,
197
+ updated_at: responseProduct.updatedAt,
198
+ handle: responseProduct.handle,
199
+ status: responseProduct.status,
200
+ vendor: responseProduct.vendor,
201
+ options: responseProduct.options && responseProduct.options.length > 0 ? responseProduct.options.map(op => ({ name: op.name, values: op.optionValues ? op.optionValues.map(v => v.name) : [] })) : [],
202
+ tags: responseProduct.tags ? responseProduct.tags : [],
203
+ variants: responseProduct.variants ? responseProduct.variants.edges.map(variant => ({
204
+ id: this.getIdFromGraphId(variant.node.id),
205
+ barcode: variant.node.barcode ? variant.node.barcode : null,
206
+ price: variant.node.price,
207
+ sku: variant.node.sku,
208
+ inventory_quantity: variant.node.inventoryQuantity,
209
+ inventory_policy: variant.node.inventoryPolicy,
210
+ compare_at_price: variant.node.compareAtPrice,
211
+ taxable: variant.node.taxable,
212
+ weight: variant.node.inventoryItem?.measurement?.weight ? variant.node.inventoryItem?.measurement.weight.value : null,
213
+ weight_unit: variant.node.inventoryItem?.measurement?.weight ? variant.node.inventoryItem?.measurement.weight.unit : null,
214
+ image_id: variant.node.media?.nodes?.length > 0 ? this.getIdFromGraphId(variant.node.media.nodes[0].id) : null,
215
+ inventory_item_id: this.getIdFromGraphId(variant.node.inventoryItem.id),
216
+ option1: variant.node.selectedOptions?.length > 0 ? variant.node.selectedOptions[0].value : null,
217
+ option2: variant.node.selectedOptions?.length > 1 ? variant.node.selectedOptions[1].value : null,
218
+ option3: variant.node.selectedOptions?.length > 2 ? variant.node.selectedOptions[2].value : null,
219
+ } as IVariant)) : []
220
+ };
221
+ }
222
+ Logger.info(`ShopifyProductListService - getById -> ${productToReturn?.id} Products returning`);
223
+ return productToReturn;
224
+ } catch (error) { this.logErrorAndThrow(error); }
225
+ };
226
+
227
+ private buildProductsQuery = (publishedStatus: 'any' | 'published' | 'unpublished', daysAgoCreated: number | 'all', daysAgoUpdated: number | 'all', handle: string, title: string, vendor: string, productType: string, collectionId: number): string => {
228
+ let queryParams = '';
229
+ if (publishedStatus === 'published' || publishedStatus === 'unpublished') {
230
+ queryParams = `published_status:'${publishedStatus}'`;
231
+ }
232
+ if (daysAgoCreated && daysAgoCreated !== 'all') {
233
+ const dayAgoString = moment().add(-daysAgoCreated, 'days').format('YYYY-MM-DDTHH:mm:ss');
234
+ if (queryParams.length > 0) {
235
+ queryParams = `${queryParams} AND created_at:>'${dayAgoString}'`;
236
+ } else {
237
+ queryParams = `created_at:>'${dayAgoString}'`;
238
+ }
239
+ }
240
+ if (daysAgoUpdated && daysAgoUpdated !== 'all') {
241
+ const dayAgoUpdatedString = moment().add(-daysAgoUpdated, 'days').format('YYYY-MM-DDTHH:mm:ss');
242
+ if (queryParams.length > 0) {
243
+ queryParams = `${queryParams} AND updated_at:>'${dayAgoUpdatedString}'`;
244
+ } else {
245
+ queryParams = `updated_at:>'${dayAgoUpdatedString}'`;
246
+ }
247
+ }
248
+ if (handle) {
249
+ if (queryParams.length > 0) {
250
+ queryParams = `${queryParams} AND handle:'${handle}'`;
251
+ } else {
252
+ queryParams = `handle:'${handle}'`;
253
+ }
254
+ }
255
+ if (title) {
256
+ if (queryParams.length > 0) {
257
+ queryParams = `${queryParams} AND title:'${title}'`;
258
+ } else {
259
+ queryParams = `title:'${title}'`;
260
+ }
261
+ }
262
+ if (vendor) {
263
+ if (queryParams.length > 0) {
264
+ queryParams = `${queryParams} AND vendor:'${vendor}'`;
265
+ } else {
266
+ queryParams = `vendor:'${vendor}'`;
267
+ }
268
+ }
269
+ if (productType) {
270
+ if (queryParams.length > 0) {
271
+ queryParams = `${queryParams} AND product_type:'${productType}'`;
272
+ } else {
273
+ queryParams = `product_type:'${productType}'`;
274
+ }
275
+ }
276
+ if (collectionId) {
277
+ if (queryParams.length > 0) {
278
+ queryParams = `${queryParams} AND collection_id:'${collectionId}'`;
279
+ } else {
280
+ queryParams = `collection_id:'${collectionId}'`;
281
+ }
282
+ }
283
+ return queryParams;
284
+ };
285
+
286
+ private buildProductsGraphQL = (numOfProducts: number | 'all', queryParams: string, nextCursor: string, options: { add_variants?: boolean; add_weight?: boolean; add_body_html?: boolean; add_title?: boolean; add_images?: boolean; add_tags?: boolean; add_options?: boolean; }): string => {
287
+ const productsQuery = `{ products(first: ${numOfProducts !== 'all' && numOfProducts < 100 ? numOfProducts : 100} ${queryParams !== '' ? `, query: "${queryParams}"` : ''} ${nextCursor !== '' && nextCursor !== null ? `, after: "${nextCursor}"` : ''}) {
288
+ pageInfo { endCursor hasNextPage }
289
+ edges {
290
+ node {
291
+ id
292
+ ${options.add_body_html === false ? '' : 'descriptionHtml'}
293
+ ${options.add_title === false ? '' : 'title'}
294
+ vendor
295
+ productType
296
+ createdAt
297
+ updatedAt
298
+ publishedAt
299
+ handle
300
+ status
301
+ ${options.add_images === false ? '' : 'images(first: 20) { edges { node { id url } } }'}
302
+ ${options.add_tags === false ? '' : 'tags'}
303
+ ${options.add_options === false ? '' : 'options { name position optionValues { name } }'}
304
+ ${options.add_variants === false ? '' : `variants(first: 150) {
305
+ edges {
306
+ node {
307
+ id
308
+ price
309
+ sku
310
+ availableForSale
311
+ inventoryQuantity
312
+ inventoryPolicy
313
+ taxable
314
+ compareAtPrice
315
+ ${options.add_images === false ? '' : 'image { id url }'}
316
+ ${options.add_options === false ? '' : 'selectedOptions { name value }'}
317
+ inventoryItem {
318
+ id
319
+ ${options.add_weight === false ? '' : 'measurement { weight { unit value } }'}
320
+ tracked
321
+ unitCost {
322
+ amount
323
+ }
324
+ }
325
+ }
326
+ }
327
+ }` }
328
+ }
329
+ }
330
+ } }`;
331
+ return productsQuery;
332
+ };
333
+ }